From 91d0b2c00ff96b1190839eab95bc0b726b2e594d 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 001/113] add function for servicegrup downtimes --- monitoring/nagios.py | 53 +++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 9219766b86a..3d46d74ef48 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,11 +187,12 @@ 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'] command = module.params['command'] - + ################################################################## # Required args per action: # downtime = (minutes, service, host) @@ -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'] @@ -356,7 +374,7 @@ class Nagios(object): notif_str = "[%s] %s" % (entry_time, cmd) if host is not None: notif_str += ";%s" % host - + if svc is not None: notif_str += ";%s" % svc @@ -784,42 +802,42 @@ class Nagios(object): return return_str_list else: return "Fail: could not write to the command file" - + def silence_nagios(self): """ This command is used to disable notifications for all hosts and services in nagios. - + This is a 'SHUT UP, NAGIOS' command """ cmd = 'DISABLE_NOTIFICATIONS' self._write_command(self._fmt_notif_str(cmd)) - + def unsilence_nagios(self): """ This command is used to enable notifications for all hosts and services in nagios. - + This is a 'OK, NAGIOS, GO'' command """ cmd = 'ENABLE_NOTIFICATIONS' self._write_command(self._fmt_notif_str(cmd)) - + def nagios_cmd(self, cmd): """ This sends an arbitrary command to nagios - + It prepends the submitted time and appends a \n - + You just have to provide the properly formatted command """ - + pre = '[%s]' % int(time.time()) - + post = '\n' cmdstr = '%s %s %s' % (pre, cmd, post) self._write_command(cmdstr) - + def act(self): """ Figure out what you want to do from ansible, and then do the @@ -835,6 +853,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': @@ -859,13 +880,13 @@ class Nagios(object): services=self.services) elif self.action == 'silence_nagios': self.silence_nagios() - + elif self.action == 'unsilence_nagios': self.unsilence_nagios() - + elif self.action == 'command': self.nagios_cmd(self.command) - + # wtf? else: self.module.fail_json(msg="unknown action specified: '%s'" % \ From b0af1be84acf92da61787c38e2fa80d53f46263b 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 002/113] 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 3d46d74ef48..0044fbd77a4 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 @@ -853,7 +862,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 ebda36bb5054fc577a422de6062c24e3083cafcd 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 003/113] improved docs --- monitoring/nagios.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 0044fbd77a4..1ddde5b5b1c 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 0fa856d467eb839de41a377cf36ce722062fe810 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 004/113] fix bugs --- monitoring/nagios.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index 1ddde5b5b1c..f0904a44e9c 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'] @@ -867,11 +867,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 1e3645a9e3ef63a8bfb9bcc71e586058be3fcf28 Mon Sep 17 00:00:00 2001 From: Nicolas Brisac Date: Fri, 14 Nov 2014 17:09:24 +0100 Subject: [PATCH 005/113] 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 a49aa8c3a49..5500bae0573 100644 --- a/system/ufw.py +++ b/system/ufw.py @@ -113,6 +113,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 = ''' @@ -162,6 +167,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 @@ -175,6 +184,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']), @@ -238,10 +248,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 ca94781d5c29f78f6a380a024821ba8360b67b78 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 25 Nov 2014 15:50:27 -0600 Subject: [PATCH 006/113] 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 cf54dc46b49adfccf377d646727d922cf7c7d659 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Wed, 26 Nov 2014 21:32:16 -0600 Subject: [PATCH 007/113] 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 c60441fddd7433c9d258b3837be1669f4d73e725 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Thu, 4 Dec 2014 15:50:48 -0600 Subject: [PATCH 008/113] 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 797d8893d65a8a214c68c82be3ccec11462077ff Mon Sep 17 00:00:00 2001 From: Jason Holland Date: Tue, 25 Nov 2014 14:43:47 -0600 Subject: [PATCH 009/113] 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 65410536eef..109828772c1 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 1011565282715e943062cbb17d395f86738f3626 Mon Sep 17 00:00:00 2001 From: Jason Holland Date: Thu, 4 Dec 2014 16:15:23 -0600 Subject: [PATCH 010/113] 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 109828772c1..7df1d6d8c9e 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 cb46aab3d1444b732095bf7a4f5a8e36fa26d50d Mon Sep 17 00:00:00 2001 From: Giovanni Tirloni Date: Thu, 22 Jan 2015 09:13:12 -0500 Subject: [PATCH 011/113] 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 14b62bb32ae122ca7d5fcb2f05c149da33e904c7 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Tue, 17 Feb 2015 16:56:15 +1100 Subject: [PATCH 012/113] 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 148e25a5011..c1f450b2e0f 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 671571b1e1b4527ae3355abedbac6c34b3c51f7f Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Tue, 17 Feb 2015 17:13:27 +1100 Subject: [PATCH 013/113] 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 c1f450b2e0f..86130f02103 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 fa2df8c7d5e63b5db4282c8a4e081c9711b95d5b Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Wed, 18 Feb 2015 10:42:07 +1100 Subject: [PATCH 014/113] 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 86130f02103..c502bfc5ce8 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 19b0c838192f49a4704e118fdd9457fd905df817 Mon Sep 17 00:00:00 2001 From: Matthew Landauer Date: Wed, 18 Feb 2015 12:14:58 +1100 Subject: [PATCH 015/113] 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 c502bfc5ce8..b6320d65e6c 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 8084671e33296043c47a362018f32a83d471e691 Mon Sep 17 00:00:00 2001 From: Kevin Klinemeier Date: Sun, 15 Mar 2015 21:42:35 -0700 Subject: [PATCH 016/113] 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 aef5792772d267d243ba17f7451735fb4dc1f291 Mon Sep 17 00:00:00 2001 From: Todd Zullinger Date: Wed, 18 Mar 2015 15:07:56 -0400 Subject: [PATCH 017/113] 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 c564e712b04..497d0bc19f7 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -51,6 +51,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. @@ -84,6 +89,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 }} @@ -175,6 +184,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), minutes=dict(default=30), cmdfile=dict(default=which_cmdfile()), @@ -258,6 +268,7 @@ class Nagios(object): self.module = module self.action = kwargs['action'] self.author = kwargs['author'] + self.comment = kwargs['comment'] self.host = kwargs['host'] self.minutes = int(kwargs['minutes']) self.cmdfile = kwargs['cmdfile'] @@ -293,7 +304,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. @@ -326,6 +337,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 e5f4aa4dea1505e520eb62e24b90b64c81605ef9 Mon Sep 17 00:00:00 2001 From: Solomon Gifford Date: Tue, 31 Mar 2015 16:43:40 -0400 Subject: [PATCH 018/113] \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 3a3cf4dfff1..ecf8b33b607 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 6121af6b8ec26ebcd8e67da3f78819365fd61c1c Mon Sep 17 00:00:00 2001 From: Solomon Gifford Date: Thu, 9 Apr 2015 14:03:14 -0400 Subject: [PATCH 019/113] 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 ecf8b33b607..d8b98f595eb 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 70ae77a365d954ed2cbf08947f165917e9ae1a37 Mon Sep 17 00:00:00 2001 From: Solomon Gifford Date: Thu, 9 Apr 2015 14:22:24 -0400 Subject: [PATCH 020/113] #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 d8b98f595eb..10cf62cd9a0 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 658e7300ad966c16c1440da498d945c7d15539c8 Mon Sep 17 00:00:00 2001 From: Benjamin Albrecht Date: Tue, 14 Apr 2015 20:56:36 +0200 Subject: [PATCH 021/113] 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 93248897051..1ac14361e09 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. @@ -368,7 +368,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 f7961bd227f6edee3d940c55db420f3fa35af26e Mon Sep 17 00:00:00 2001 From: NewGyu Date: Wed, 29 Apr 2015 23:59:16 +0900 Subject: [PATCH 022/113] 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 023/113] 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 024/113] 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 69c0a6360bfbbf6356f92cdfcdadce2600e80c22 Mon Sep 17 00:00:00 2001 From: fdupoux Date: Sat, 9 May 2015 14:06:58 +0100 Subject: [PATCH 025/113] 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 d9be9e7dc70..49bd713e16d 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -83,6 +83,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 = [] @@ -95,6 +97,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( @@ -107,6 +120,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'] @@ -187,7 +207,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 a0ef5e4a5973f850fdcc019896ede93fe8595675 Mon Sep 17 00:00:00 2001 From: Quentin Stafford-Fraser Date: Sun, 10 May 2015 20:40:50 +0100 Subject: [PATCH 026/113] 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 027/113] 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 028/113] 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 029/113] 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 030/113] 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 031/113] 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 032/113] 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 033/113] 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 034/113] 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 6539add9d1177e471d3f7b6eb8b03c02d75608a3 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 035/113] 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 73f4e2dd061d4c6f4adc80134bb2450139024916 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 036/113] 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 77479882a4d67b101e844ef78a26795a17988fbb 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 037/113] 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 8009bdfe77691532abe5ab37027be36fa45ab811 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 038/113] 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 c034d080936a58f9233cf2b8a556abad017ab5c1 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 039/113] 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 fa2f250f14925b495d068110c66f9cad69b1450a Mon Sep 17 00:00:00 2001 From: Sebastian Kornehl Date: Tue, 19 May 2015 15:05:31 +0200 Subject: [PATCH 040/113] 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 1d6a98dc9c3..a3ac92a03bb 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 257d8ea2b12edf8f87af4d9b59dcf474a399e0c5 Mon Sep 17 00:00:00 2001 From: Ernst Kuschke Date: Wed, 20 May 2015 16:34:21 +0200 Subject: [PATCH 041/113] 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 7505065daa0a76cad46de423771e96cdc726ba4f Mon Sep 17 00:00:00 2001 From: Christian Thiemann Date: Sun, 24 May 2015 02:05:38 +0200 Subject: [PATCH 042/113] 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 141dda9978a197801a503347c8bd611cb368dda8 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Wed, 27 May 2015 20:54:26 +0200 Subject: [PATCH 043/113] 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 f6dee55ee81d7a17e5efc94f5399183be555181f Mon Sep 17 00:00:00 2001 From: fdupoux Date: Thu, 28 May 2015 19:46:53 +0100 Subject: [PATCH 044/113] 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 49bd713e16d..14bab6926e4 100644 --- a/system/lvol.py +++ b/system/lvol.py @@ -125,7 +125,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 5b401cfcc30cb84dcf19a4c05b5a0791303d8378 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 28 May 2015 16:23:27 -0400 Subject: [PATCH 045/113] 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 046/113] 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 047/113] 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 048/113] 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 4939df305b9f49e7135657b23950213b036c12a2 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Fri, 29 May 2015 10:07:00 +0200 Subject: [PATCH 049/113] 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 4da1a5de9e72af210563fe8c8fffe352f22f4be8 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:24:34 +0200 Subject: [PATCH 050/113] 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 17504f0a268094e3b2ee4832435ebe80e34e167c Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:26:00 +0200 Subject: [PATCH 051/113] 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 a425c413be6671921a04806624674b9daea2b0c2 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:28:06 +0200 Subject: [PATCH 052/113] 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 506b4c46724fefdae42c44ffadba2118767e6069 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 00:46:20 +0200 Subject: [PATCH 053/113] 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 f31c7d9b055a56a81c126ce80506bd2634d1e0ba Mon Sep 17 00:00:00 2001 From: mlamatr Date: Fri, 29 May 2015 23:18:44 -0400 Subject: [PATCH 054/113] 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 6643ea5825457faabebe134757cc3cd59653b1ba Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 11:03:32 +0200 Subject: [PATCH 055/113] 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 16c70f96943e43b1af37f40317b7809d2cfc12f6 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 11:05:03 +0200 Subject: [PATCH 056/113] 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 35dd0025aac52b6896cfad467c5b1e03593464c6 Mon Sep 17 00:00:00 2001 From: Q Date: Sat, 30 May 2015 23:01:52 +1000 Subject: [PATCH 057/113] 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 2189af8c9572fbae280c7b9dfa9878894d08314b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 11:05:36 +0200 Subject: [PATCH 058/113] 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 6c29a181c8a0d4bb95614f64ef908c319547c395 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 18:28:41 +0200 Subject: [PATCH 059/113] 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 d20fa0477ebab3ecf06f37771831e500b20ab8ad Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sat, 30 May 2015 22:54:56 +0200 Subject: [PATCH 060/113] 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 f731bcc2d58c5f8cdf244e15b19c1e93f62fb2b9 Mon Sep 17 00:00:00 2001 From: fdupoux Date: Sun, 31 May 2015 12:38:45 +0100 Subject: [PATCH 061/113] 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 4690237b7b2989c23c52ce551859f3442c9b5ac3 Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Mon, 1 Jun 2015 08:59:50 -0400 Subject: [PATCH 062/113] 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 223694ccf270da8b1b5a5d61bc05771c111dda1c Mon Sep 17 00:00:00 2001 From: Greg DeKoenigsberg Date: Mon, 1 Jun 2015 12:07:23 -0400 Subject: [PATCH 063/113] 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 a3ac92a03bb..1d6a98dc9c3 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 4b35db4932daa1d583ffbdc5b67be00c2cbe7a2f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Jun 2015 12:31:20 -0400 Subject: [PATCH 064/113] added version added --- monitoring/nagios.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monitoring/nagios.py b/monitoring/nagios.py index bfa498496e6..a1ba1be3f54 100644 --- a/monitoring/nagios.py +++ b/monitoring/nagios.py @@ -52,6 +52,7 @@ options: required: false default: Ansible comment: + version_added: "2.0" description: - Comment for C(downtime) action. required: false From 3861904b02b72b3a3f1fe044603a483855953d6a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Jun 2015 12:36:49 -0400 Subject: [PATCH 065/113] 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 1cc0b4c9e648db83bee4dea19327e7713888f8bc Mon Sep 17 00:00:00 2001 From: David Wittman Date: Tue, 21 Oct 2014 16:56:13 -0500 Subject: [PATCH 066/113] [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 307424c69419a57d7be50b2a85fb109293f5d91f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Jun 2015 15:27:55 -0400 Subject: [PATCH 067/113] 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 61aab829ed4801c3c86ae10a5142400fd2e67d0f Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Mon, 1 Jun 2015 15:15:37 -0500 Subject: [PATCH 068/113] 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 858f9e3601f58dc16ede3056dc4aeff4f8da7cb0 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Mon, 1 Jun 2015 15:31:56 -0500 Subject: [PATCH 069/113] 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 070/113] 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 071/113] 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 072/113] 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 073/113] 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 074/113] 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 075/113] 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 076/113] 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 077/113] 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 078/113] 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 079/113] 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 080/113] 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 081/113] 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 082/113] 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 083/113] 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 084/113] 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 085/113] 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 086/113] 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 087/113] 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 088/113] 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 089/113] 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 090/113] 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 091/113] 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 092/113] 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 093/113] 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 094/113] 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 095/113] 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 096/113] 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 097/113] 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 098/113] 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 099/113] 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 100/113] 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 101/113] 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 102/113] 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 103/113] 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 104/113] 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 105/113] 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 106/113] 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 107/113] 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 108/113] 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 109/113] 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 110/113] 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 5f5577e110aec5f44f6544fc3ecbbeaf2230a025 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Sun, 12 Apr 2015 23:09:45 +0200 Subject: [PATCH 111/113] 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 112/113] 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 8311854fa6a93b10da38c83ad5d62269337e5feb Mon Sep 17 00:00:00 2001 From: whiter Date: Tue, 16 Jun 2015 12:21:37 +1000 Subject: [PATCH 113/113] 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()