From a55035c55874ac66351672039e94dd44349d5c7b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 12 Oct 2014 00:20:00 +0200 Subject: [PATCH 001/245] 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 002/245] 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 @@ notes: ''' 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 @@ class MacDefaults(object): ) 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 @@ class MacDefaults(object): 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 @@ class MacDefaults(object): # 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 @@ class MacDefaults(object): # 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 @@ class MacDefaults(object): 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 @@ class MacDefaults(object): # 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 003/245] 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 @@ class OSXDefaults(object): 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 @@ class OSXDefaults(object): 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 555ff23434aa5810f035963c6197596edba14836 Mon Sep 17 00:00:00 2001 From: Billy Kimble Date: Mon, 12 Jan 2015 14:13:08 -0800 Subject: [PATCH 004/245] 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 cb46aab3d1444b732095bf7a4f5a8e36fa26d50d Mon Sep 17 00:00:00 2001 From: Giovanni Tirloni Date: Thu, 22 Jan 2015 09:13:12 -0500 Subject: [PATCH 005/245] 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 93248897051..cd4c017c303 100644 --- a/system/zfs.py +++ b/system/zfs.py @@ -250,7 +250,7 @@ class Zfs(object): 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 @@ class Zfs(object): 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 @@ class Zfs(object): 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 46f53724f0005411a6e2526aaef2ada3fc6d6af9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Fri, 13 Feb 2015 13:37:16 -0500 Subject: [PATCH 006/245] 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 65a1129ef9800c2f094f07b84677d33b762337cb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 23 Feb 2015 14:18:35 -0600 Subject: [PATCH 007/245] 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 @@ description: 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 @@ description: 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 @@ description: 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 @@ description: 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 @@ description: 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 008/245] 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 009/245] 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 @@ except ImportError: 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 @@ except ImportError: 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 @@ except ImportError: 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 @@ except ImportError: 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 8084671e33296043c47a362018f32a83d471e691 Mon Sep 17 00:00:00 2001 From: Kevin Klinemeier Date: Sun, 15 Mar 2015 21:42:35 -0700 Subject: [PATCH 010/245] 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 5d38dd4c31d..b481345fab9 100644 --- a/monitoring/datadog_event.py +++ b/monitoring/datadog_event.py @@ -71,7 +71,7 @@ datadog_event: title="Testing from ansible" text="Test!" priority="low" # 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 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 011/245] 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 012/245] 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 @@ class MavenDownloader: 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 a79772deb126e262db82a2ee6f172f39d2b71e95 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 3 May 2015 20:58:21 +0100 Subject: [PATCH 013/245] 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 014/245] 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 @@ 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 +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 @@ options: description: - The name of the application required: true - default: null state: description: @@ -65,7 +64,6 @@ options: ''' 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 @@ 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 +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 @@ options: description: - The name of the database required: true - default: null state: description: @@ -61,7 +60,6 @@ EXAMPLES = ''' 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 @@ 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 +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 @@ options: description: - The name of the domain required: true - default: null state: description: @@ -50,7 +49,6 @@ options: 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 @@ 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 +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 @@ options: description: - The name of the mailbox required: true - default: null mailbox_password: description: @@ -48,7 +47,6 @@ options: 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 @@ 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 +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 @@ options: description: - The name of the website required: true - default: null state: description: @@ -83,7 +82,6 @@ EXAMPLES = ''' 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 a0ef5e4a5973f850fdcc019896ede93fe8595675 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 20:40:50 +0100 Subject: [PATCH 015/245] 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 @@ 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: 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 @@ 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: 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 @@ 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: 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 @@ 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: 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 @@ 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: 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 016/245] 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 017/245] 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 @@ options: 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 @@ options: 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 @@ options: 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 838cd4123bcf573c99cf7e4c54a671bf136139f7 Mon Sep 17 00:00:00 2001 From: Lorenzo Luconi Trombacchi Date: Tue, 12 May 2015 10:56:22 +0200 Subject: [PATCH 018/245] 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 8772d22b2d8..fcb55587c2e 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -77,7 +77,7 @@ def main(): # 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 55b9ab277493eac80cdf0eafcfde28149d60452f Mon Sep 17 00:00:00 2001 From: Lorenzo Luconi Trombacchi Date: Tue, 12 May 2015 10:58:47 +0200 Subject: [PATCH 019/245] 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 fcb55587c2e..69a0eed11c9 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -119,7 +119,7 @@ def main(): 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 1f9f9a549ecbde78efdc0cafa25dd589f64b8a68 Mon Sep 17 00:00:00 2001 From: Lorenzo Luconi Trombacchi Date: Tue, 12 May 2015 11:07:52 +0200 Subject: [PATCH 020/245] 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 69a0eed11c9..f20ba706bea 100644 --- a/monitoring/monit.py +++ b/monitoring/monit.py @@ -86,7 +86,8 @@ def main(): 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 main(): 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 9b32a5d8bf345b7cee3609573c0ebcdba69f8b2f Mon Sep 17 00:00:00 2001 From: Chris Long Date: Tue, 12 May 2015 22:10:53 +1000 Subject: [PATCH 021/245] 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 022/245] 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 @@ description: 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 @@ options: 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 @@ options: required: False default: None description: - - master Date: Fri, 15 May 2015 01:09:49 +1000 Subject: [PATCH 023/245] 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ tenant_ip: "192.168.200.21/23" - 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 024/245] 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 025/245] 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 026/245] 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 027/245] 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 028/245] 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 @@ options: - Supports * as wildcard required: true default: null - aliases: [] state: description: - State that the task should become @@ -44,7 +43,6 @@ options: - 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 029/245] 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 @@ options: - 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 030/245] 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 @@ options: - 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 5b401cfcc30cb84dcf19a4c05b5a0791303d8378 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 May 2015 16:23:27 -0400 Subject: [PATCH 031/245] 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 032/245] 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 033/245] 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 034/245] 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 35dd0025aac52b6896cfad467c5b1e03593464c6 Mon Sep 17 00:00:00 2001 From: Q Date: Sat, 30 May 2015 23:01:52 +1000 Subject: [PATCH 035/245] 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 @@ options: 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 1cc0b4c9e648db83bee4dea19327e7713888f8bc Mon Sep 17 00:00:00 2001 From: David Wittman Date: Tue, 21 Oct 2014 16:56:13 -0500 Subject: [PATCH 036/245] [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 @@ options: - 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 @@ EXAMPLES = ''' # 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 858f9e3601f58dc16ede3056dc4aeff4f8da7cb0 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Mon, 1 Jun 2015 15:31:56 -0500 Subject: [PATCH 037/245] 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 @@ options: - 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 @@ notes: 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 ffdb8d9eb479b6b47090d4a9173f920a76facbbd Mon Sep 17 00:00:00 2001 From: Q Date: Tue, 2 Jun 2015 13:32:22 +1000 Subject: [PATCH 038/245] 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 @@ options: 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 1c3afeadfc3a68173b652a8c0bf08646cf3ac1ab Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Tue, 2 Jun 2015 09:25:55 +0100 Subject: [PATCH 039/245] 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 739defc595bb7769aef1627e09be50784b14189e Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 16:26:32 +0600 Subject: [PATCH 040/245] 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 282393c27b57ea63d649531847674f1863860648 Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 18:21:36 +0600 Subject: [PATCH 041/245] 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 5da651212ff84c250a3782830e3fb2ca48003dd6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 2 Jun 2015 08:37:45 -0400 Subject: [PATCH 042/245] 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 1d6a98dc9c3..90cbccc9593 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 b8df0da2308cafb18d9a492615888df4d596ce7f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 2 Jun 2015 08:48:20 -0400 Subject: [PATCH 043/245] 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 @@ options: type: "int" default: "0" backup: + version_added: "2.0" description: - passes --backup --version-control=numbered to patch, producing numbered backup copies From af7463e46e6d24777ea2934ddf05d8710c16de73 Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 22:26:32 +0600 Subject: [PATCH 044/245] 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 @@ options: - 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 51f9225754c42007ba9633ea6ab63765048fe054 Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 22:29:19 +0600 Subject: [PATCH 045/245] 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 @@ 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 + - allows you to upload/delete templates in Proxmox VE cluster version_added: "2.0" options: api_host: @@ -76,7 +76,7 @@ options: timeout: description: - timeout for operations - default: 300 + default: 30 required: false type: integer force: @@ -88,7 +88,7 @@ options: 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 @@ EXAMPLES = ''' # 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 e337b67cf12f382cf9420fa4d5c7a7ab5b9cad88 Mon Sep 17 00:00:00 2001 From: Sergei Antipov Date: Tue, 2 Jun 2015 22:53:47 +0600 Subject: [PATCH 046/245] 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 @@ options: - 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 198e77e5fb519f92c72ca6ab514bbea922d30248 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 2 Jun 2015 14:11:51 -0400 Subject: [PATCH 047/245] 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 @@ options: 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 328b133a33446123558d34a0e35164b23929a11e Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Wed, 3 Jun 2015 01:57:15 +0300 Subject: [PATCH 048/245] 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 @@ options: 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 a0905a9d5ecf9101288080324483fb8ca56f87ba Mon Sep 17 00:00:00 2001 From: Etienne CARRIERE Date: Wed, 3 Jun 2015 08:22:18 +0200 Subject: [PATCH 049/245] 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' ''' -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 050/245] 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 ab8de7a3e7f6b62119b3c65f74f96ea06ab3572f Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 4 Jun 2015 01:25:08 +0300 Subject: [PATCH 051/245] 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 @@ class Bower(object): 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 @@ class Bower(object): 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 052/245] 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 @@ 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."] @@ -63,7 +60,6 @@ options: 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 80b1b3add239c58582bc71576a5666d81580bff0 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Thu, 4 Jun 2015 22:17:16 +0100 Subject: [PATCH 053/245] 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 @@ options: 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 @@ EXAMPLES = ''' 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 054/245] 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 @@ 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 +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 @@ 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 +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 @@ 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 +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 @@ 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 +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 @@ 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 +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 055/245] 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 537562217fbc7645a9771efb2f7bd051c948077a Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 6 Jun 2015 09:13:11 +0200 Subject: [PATCH 056/245] 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 057/245] 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 @@ options: - 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 @@ EXAMPLES = ''' # 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 058/245] 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 059/245] 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 @@ options: 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 @@ author: Monty Taylor ''' 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 060/245] 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 @@ options: - 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 @@ EXAMPLES = ''' 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 061/245] 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 @@ description: - 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 062/245] 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 @@ module: pear 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 063/245] 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 064/245] 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 @@ class AnsibleCloudStackProject(AnsibleCloudStack): 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 065/245] 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 @@ class AnsibleCloudStackProject(AnsibleCloudStack): 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 066/245] 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 067/245] 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 @@ options: - 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 @@ import ssl 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 ef7381f24636a350dd7bd0d061634fd2203d1b61 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Tue, 9 Jun 2015 12:58:45 -0400 Subject: [PATCH 068/245] 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 @@ description: - "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 069/245] 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 @@ options: 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 070/245] 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 @@ options: 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 071/245] 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 072/245] 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 073/245] 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 @@ options: 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 074/245] 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 @@ EXAMPLES = """ 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 075/245] 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 @@ EXAMPLES = """ 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 076/245] 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 @@ EXAMPLES = """ 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 @@ class CloudTrailManager: 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 077/245] 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 078/245] 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 @@ module: nmcli 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ options: - 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 @@ options: - 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 d3b3d7ff3c249de062df93a533a568f0681cce3c Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Sat, 13 Jun 2015 13:56:26 -0500 Subject: [PATCH 079/245] 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 @@ class LxcContainerManagement(object): 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 080/245] 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 081/245] 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 @@ EXAMPLES = ''' ''' -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 082/245] 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 @@ except ImportError: 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 083/245] 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 5f5577e110aec5f44f6544fc3ecbbeaf2230a025 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 12 Apr 2015 23:09:45 +0200 Subject: [PATCH 084/245] 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 085/245] 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 @@ options: - 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 086/245] 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 087/245] 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 088/245] 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 @@ class Nagios(object): 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 @@ class Nagios(object): 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 089/245] 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 @@ options: 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 @@ EXAMPLES = ''' # 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 @@ class Nagios(object): 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 090/245] 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 @@ options: 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 091/245] 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 @@ class Nagios(object): 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 @@ class Nagios(object): 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 092/245] 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 @@ options: - 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 @@ ufw: rule=allow interface=eth0 direction=in proto=udp src=1.2.3.5 from_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 + +# 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 main(): 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 093/245] 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 094/245] 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 095/245] 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 096/245] 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 main(): # 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 main(): 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 097/245] 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 main(): # - 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 098/245] 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 @@ class Zfs(object): 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 @@ class Zfs(object): 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 @@ class Zfs(object): 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 099/245] 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 100/245] 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 101/245] 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 102/245] 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 @@ class DME2: 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 @@ class DME2: 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 103/245] 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 @@ datadog_event: title="Testing from ansible" text="Test!" priority="low" # 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 104/245] 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 @@ options: 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 @@ EXAMPLES = ''' # 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 @@ class Nagios(object): 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 @@ class Nagios(object): 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 @@ class Nagios(object): 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 105/245] \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 106/245] 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 @@ else: # 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 107/245] #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 @@ options: 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 108/245] 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 @@ options: 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 109/245] 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 @@ class MavenDownloader: 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 110/245] 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 111/245] 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 @@ 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 +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 @@ options: description: - The name of the application required: true - default: null state: description: @@ -65,7 +64,6 @@ options: ''' 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 @@ 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 +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 @@ options: description: - The name of the database required: true - default: null state: description: @@ -61,7 +60,6 @@ EXAMPLES = ''' 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 @@ 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 +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 @@ options: description: - The name of the domain required: true - default: null state: description: @@ -50,7 +49,6 @@ options: 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 @@ 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 +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 @@ options: description: - The name of the mailbox required: true - default: null mailbox_password: description: @@ -48,7 +47,6 @@ options: 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 @@ 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 +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 @@ options: description: - The name of the website required: true - default: null state: description: @@ -83,7 +82,6 @@ EXAMPLES = ''' 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 112/245] 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 @@ import re 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 113/245] 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 @@ 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: 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 @@ 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: 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 @@ 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: 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 @@ 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: 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 @@ 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: 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 114/245] 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 115/245] 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 @@ options: 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 @@ options: 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 @@ options: 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 116/245] 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 main(): # 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 117/245] 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 main(): 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 118/245] 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 main(): 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 main(): 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 119/245] 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 120/245] 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 @@ description: 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 @@ options: 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 @@ options: required: False default: None description: - - master Date: Fri, 15 May 2015 01:09:49 +1000 Subject: [PATCH 121/245] 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ tenant_ip: "192.168.200.21/23" - 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 @@ class Nmcli(object): 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 122/245] 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 @@ author: '"Taneli Leppä (@rosmo)" ' 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 @@ EXAMPLES = """ 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 123/245] 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 124/245] 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 @@ author: '"Taneli Leppä (@rosmo)" ' 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 @@ EXAMPLES = """ 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 125/245] 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 126/245] 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 127/245] 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 128/245] 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 129/245] 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 130/245] 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 @@ options: 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 @@ EXAMPLES = ''' import os import re -import sys try: import firewall.config @@ -90,14 +89,9 @@ try: 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 131/245] 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 132/245] 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 133/245] 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 134/245] 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 135/245] 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 136/245] 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 137/245] 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 @@ tags: 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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 138/245] 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 @@ hypervisor: 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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 139/245] 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 @@ DOCUMENTATION = ''' 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ extends_documentation_fragment: cloudstack ''' 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 @@ EXAMPLES = ''' - Sync Integration - Storage Integration - # For changing a running instance, use the 'force' parameter - local_action: module: cs_instance @@ -191,7 +198,6 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ password: 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 @@ default_ip: 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 140/245] 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 @@ options: 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 @@ EXAMPLES = ''' public_port: 80 private_port: 8080 - # forward SSH and open firewall - local_action: module: cs_portforward @@ -127,7 +127,6 @@ EXAMPLES = ''' private_port: 22 open_firewall: true - # forward DNS traffic, but do not open firewall - local_action: module: cs_portforward @@ -138,7 +137,6 @@ EXAMPLES = ''' protocol: udp open_firewall: true - # remove ssh port forwarding - local_action: module: cs_portforward @@ -161,26 +159,26 @@ protocol: 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 @@ vm_guest_ip: 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 141/245] 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 142/245] 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 143/245] 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 @@ local_action: email: john.doe@example.com domain: CUSTOMERS - # Lock an existing account in domain 'CUSTOMERS' local_action: module: cs_account @@ -116,7 +115,6 @@ local_action: domain: CUSTOMERS state: locked - # Disable an existing account in domain 'CUSTOMERS' local_action: module: cs_account @@ -124,7 +122,6 @@ local_action: domain: CUSTOMERS state: disabled - # Enable an existing account in domain 'CUSTOMERS' local_action: module: cs_account @@ -132,7 +129,6 @@ local_action: 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' - tcp - udp - # Ensure firewall rule is removed - local_action: module: cs_firewall @@ -136,7 +134,6 @@ EXAMPLES = ''' cidr: 17.0.0.0/8 state: absent - # Allow all outbound traffic - local_action: module: cs_firewall @@ -144,7 +141,6 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' - 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 144/245] 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 @@ options: 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 145/245] 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 @@ EXAMPLES = ''' 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 146/245] 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 @@ options: 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 147/245] 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 148/245] 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 149/245] 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 150/245] 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 151/245] 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 @@ options: 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 152/245] 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 @@ options: 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 @@ options: 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 153/245] [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 @@ options: - 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 @@ EXAMPLES = ''' # 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 154/245] 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 155/245] 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 @@ EXAMPLES = """ 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 156/245] 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 @@ options: - 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 @@ notes: 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 157/245] 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 @@ options: 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 158/245] 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 159/245] 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 160/245] 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 161/245] 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 162/245] 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 @@ options: 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 163/245] 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 @@ options: - 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 164/245] 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 @@ 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 + - allows you to upload/delete templates in Proxmox VE cluster version_added: "2.0" options: api_host: @@ -76,7 +76,7 @@ options: timeout: description: - timeout for operations - default: 300 + default: 30 required: false type: integer force: @@ -88,7 +88,7 @@ options: 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 @@ EXAMPLES = ''' # 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 165/245] 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 @@ options: - 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 166/245] 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 @@ options: 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 167/245] 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 @@ options: 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 168/245] 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 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' ''' -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 169/245] 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 170/245] 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 @@ class Bower(object): 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 @@ class Bower(object): 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 171/245] 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 @@ 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."] @@ -63,7 +60,6 @@ options: 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 172/245] 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 @@ options: 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 @@ EXAMPLES = ''' 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 173/245] 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 @@ 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 +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 @@ 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 +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 @@ 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 +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 @@ 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 +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 @@ 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 +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 174/245] 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 175/245] 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 176/245] 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 @@ options: - 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 @@ EXAMPLES = ''' # 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 177/245] 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 178/245] 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 @@ options: 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 @@ author: Monty Taylor ''' 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 179/245] 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 @@ options: - 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 @@ EXAMPLES = ''' 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 180/245] 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 @@ description: - 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 181/245] 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 @@ module: pear 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 182/245] 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 183/245] 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 @@ class AnsibleCloudStackProject(AnsibleCloudStack): 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 184/245] 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 @@ class AnsibleCloudStackProject(AnsibleCloudStack): 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 185/245] 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 186/245] 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 @@ options: - 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 @@ import ssl 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 187/245] 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 @@ description: - "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 188/245] 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 @@ options: 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 189/245] 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 @@ options: 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 190/245] 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 191/245] 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 192/245] 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 @@ options: 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 193/245] 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 @@ EXAMPLES = """ 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 194/245] 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 @@ EXAMPLES = """ 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 195/245] 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 @@ EXAMPLES = """ 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 @@ class CloudTrailManager: 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 196/245] 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 197/245] 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 @@ module: nmcli 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ options: - 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 @@ options: - 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 198/245] 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 199/245] 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 @@ options: - 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 200/245] 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 @@ short_description: manage CloudTrail creation and deletion 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 201/245] 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 @@ short_description: gets the default administrator password for ec2 windows insta 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 202/245] 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 @@ options: 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 203/245] 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 204/245] 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 @@ 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 +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 @@ short_description: Manages account on Apache CloudStack based clouds. 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 @@ short_description: Manages affinity groups on Apache CloudStack based clouds. 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 @@ short_description: Manages firewall rules on Apache CloudStack based clouds. 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 @@ short_description: Manages instances and virtual machines on Apache CloudStack b 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 @@ short_description: Manages instance groups on Apache CloudStack based clouds. 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 @@ short_description: Manages ISOs images on Apache CloudStack based clouds. 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 @@ short_description: Manages networks on Apache CloudStack based clouds. 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 @@ short_description: Manages port forwarding rules on Apache CloudStack based clou 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 @@ 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)" ' +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 @@ short_description: Manages security groups on Apache CloudStack based clouds. 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 @@ short_description: Manages security group rules on Apache CloudStack based cloud 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 @@ description: - 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 @@ 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)" ' +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 @@ short_description: Manages VM snapshots on Apache CloudStack based clouds. 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 @@ options: 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 @@ short_description: Manage LXC Containers 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 @@ requirements: - "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 @@ short_description: Manage VMware vSphere Datacenters 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 @@ requirements: - 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 @@ requirements: - 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 @@ requirements: - 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 @@ requirements: - 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 @@ 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)" ' +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 @@ 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: '"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 @@ description: 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 @@ short_description: Manage MySQL replication 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 @@ DOCUMENTATION = ''' --- 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 @@ short_description: Manage the state of policies in RabbitMQ. 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 @@ DOCUMENTATION = ''' --- 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 @@ short_description: Manage boundary meters 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 @@ description: - "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 @@ description: - "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 @@ options: 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 @@ options: 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 @@ DOCUMENTATION = ''' --- 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 @@ DOCUMENTATION = ''' --- 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 @@ short_description: Create Zabbix maintenance windows 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 @@ version_added: 1.8 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 @@ version_added: 1.8 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 @@ version_added: 1.8 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 @@ options: 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 @@ short_description: "Collect facts from F5 BIG-IP devices" 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 @@ short_description: "Manages F5 BIG-IP LTM http monitors" 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 @@ short_description: "Manages F5 BIG-IP LTM tcp monitors" 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 @@ short_description: "Manages F5 BIG-IP LTM nodes" 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 @@ short_description: "Manages F5 BIG-IP LTM pools" 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 @@ 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" -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 @@ examples: # 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 @@ DOCUMENTATION = ''' --- 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 @@ DOCUMENTATION = ''' --- 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 @@ options: # 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 @@ DOCUMENTATION = ''' --- 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 @@ options: 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 @@ short_description: Send a SMS via nexmo 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 @@ options: 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 @@ options: 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 @@ 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 -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 @@ short_description: Send Amazon Simple Notification Service (SNS) messages 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 @@ options: (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 @@ options: - 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 @@ short_description: Manage bower packages with bower 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 @@ examples: 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 @@ description: - 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 @@ short_description: Manage node.js packages with npm 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 @@ notes: [] 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 @@ DOCUMENTATION = ''' --- 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 @@ import re 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 @@ from urllib2 import Request, urlopen, URLError 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 @@ import syslog 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 @@ description: 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 @@ options: 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 @@ description: - 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 @@ options: 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 @@ description: - 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 @@ options: 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 @@ import re 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 @@ options: 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 @@ description: - 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 @@ options: 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 @@ notes: 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ import re 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 @@ options: 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 @@ short_description: Add or remove kernel modules 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 @@ description: - 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 @@ options: - 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 @@ DOCUMENTATION = ''' --- 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 @@ options: 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 @@ options: 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 @@ options: - (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 205/245] 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 206/245] 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 207/245] 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 @@ class OSXDefaults(object): # 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 208/245] 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 209/245] 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 @@ options: 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 210/245] 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 211/245] 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 @@ options: - 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 @@ examples: # 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 @@ author: "Ravi Bhure (@ravibhure)" ''' 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 @@ class HAProxy(object): 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 @@ class HAProxy(object): 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 @@ class HAProxy(object): 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 @@ class HAProxy(object): 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 @@ class HAProxy(object): 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 @@ class HAProxy(object): 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 212/245] 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 @@ options: - 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 @@ class HAProxy(object): 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 213/245] 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 214/245] 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 5e5eec1806e406127484e18492f4c1d6b45a6341 Mon Sep 17 00:00:00 2001 From: Andrew Udvare Date: Thu, 18 Jun 2015 15:59:46 -0700 Subject: [PATCH 215/245] --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 6b8c462d6605341318279a9ab11cc6843642e230 Mon Sep 17 00:00:00 2001 From: Will Thames Date: Fri, 19 Jun 2015 12:40:56 +1000 Subject: [PATCH 216/245] 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 217/245] 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 218/245] 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 219/245] 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 220/245] 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 221/245] 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 222/245] 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 @@ options: 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 @@ EXAMPLES = """ 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 @@ EXAMPLES = """ 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 223/245] 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 @@ 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" +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 @@ options: 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 @@ 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 + 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 @@ 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 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 224/245] 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 @@ options: default: 'yes' choices: ['yes', 'no'] -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [] ''' EXAMPLES = ''' @@ -72,6 +71,8 @@ EXAMPLES = ''' 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 @@ options: choices: ['yes', 'no'] version_added: 1.5.1 -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [] ''' EXAMPLES = ''' @@ -83,6 +82,8 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ options: default: 'yes' choices: ['yes', 'no'] -requirements: [ "urllib", "urllib2" ] +requirements: [] author: "Nandor Sivok (@dominis)" ''' @@ -99,7 +99,7 @@ ansible host -m netscaler -a "nsc_host=nsc.example.com user=apiuser password=api 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 @@ notes: - 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 @@ EXAMPLES = ''' # 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 @@ options: choices: ['yes', 'no'] version_added: 1.5.1 -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [ ] ''' EXAMPLES = ''' @@ -104,6 +103,8 @@ EXAMPLES = ''' 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 @@ EXAMPLES = ''' 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 @@ options: version_added: 1.6.0 -# informational: requirements for nodes -requirements: [ urllib, urllib2 ] +requirements: [ ] author: "WAKAYAMA Shirou (@shirou), BOURDEL Paul (@pb8226)" ''' @@ -75,6 +74,8 @@ EXAMPLES = ''' # 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 @@ EXAMPLES = """ 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 @@ EXAMPLES = ''' # ======================================= # 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 @@ EXAMPLES = ''' # ======================================= # 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 225/245] 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 226/245] 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 @@ description: version_added: "1.6" author: "Seth Edwards (@sedward)" requirements: - - urllib2 - base64 options: user: @@ -107,11 +106,7 @@ EXAMPLES = ''' ''' -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 @@ EXAMPLES = ''' # ======================================= # 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 @@ EXAMPLES = ''' # ======================================= # 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 @@ options: description: - message body required: true -requirements: [ urllib, urllib2, json ] +requirements: [ json ] author: "Takashi Someda (@tksmd)" ''' @@ -33,15 +33,9 @@ EXAMPLES = ''' - 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 227/245] 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 228/245] 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 268104fca321a777e279ed20d252e43da23a2b9a Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sat, 20 Jun 2015 21:24:36 +1000 Subject: [PATCH 229/245] 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 230/245] 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 ac09e609146c3f8c8ef46dc22ab75834aa5d20dc Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Sun, 21 Jun 2015 08:40:57 +1000 Subject: [PATCH 231/245] 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 75e1e9fcda109b223487de752356066035059ae7 Mon Sep 17 00:00:00 2001 From: Eike Frost Date: Tue, 28 Apr 2015 19:41:54 +0200 Subject: [PATCH 232/245] 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 @@ options: 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 @@ EXAMPLES = ''' ip: 10.xx.xx.xx dns: "" port: 12345 + proxy: a.zabbix.proxy ''' import logging @@ -174,21 +179,25 @@ class Host(object): 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 @@ class Host(object): 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 @@ class Host(object): # 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 @@ class Host(object): 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 1a914128f6d172da7ea349d6b070758e1ebbff9c Mon Sep 17 00:00:00 2001 From: Alan Loi Date: Mon, 22 Jun 2015 20:23:11 +1000 Subject: [PATCH 233/245] 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 0ad12cdcf4e5d4ed90b506917ee5083b1910b0e2 Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Mon, 22 Jun 2015 20:09:54 +0200 Subject: [PATCH 234/245] 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 @@ options: 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 @@ import time 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 235/245] 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 @@ DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock" RECV_SIZE = 1024 ACTION_CHOICES = ['enabled', 'disabled'] WAIT_RETRIES=25 -WAIT_INTERVAL=1 +WAIT_INTERVAL=5 ###################################################################### class TimeoutException(Exception): From d8063b913ee49f03236c30a3d90b6e106c949f3f Mon Sep 17 00:00:00 2001 From: jpic Date: Tue, 23 Jun 2015 19:36:43 +0200 Subject: [PATCH 236/245] 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 @@ try: 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 237/245] 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 @@ class LxcContainerManagement(object): 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 238/245] 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 239/245] 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 @@ description: 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 @@ description: 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 @@ description: 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 @@ description: 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 @@ description: 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 f1e3260b3f97e37ae70788b42f089dd53f591b99 Mon Sep 17 00:00:00 2001 From: Arnaud Dematte Date: Tue, 21 Apr 2015 14:48:44 +0200 Subject: [PATCH 240/245] 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 @@ options: - 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 955bf92ff892a7359a045e1ddb3b29b7809a230b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 25 Jun 2015 06:53:28 -0700 Subject: [PATCH 241/245] 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 @@ options: - 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 242/245] 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 243/245] 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 @@ description: - 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 @@ description: 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 @@ options: 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 60b5ae35b30d4c2a2b2d337ac413864d6df8251a Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 26 Jun 2015 14:23:35 +0200 Subject: [PATCH 244/245] 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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 245/245] 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 @@ options: 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): 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)