From d914b3fa849c332ec7f606388a47ab9ecaaeed51 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Wed, 6 Apr 2016 09:13:28 +0000 Subject: [PATCH 01/33] Add os_keystone_domain_facts module This module gathers one or more OpenStack domains facts --- cloud/openstack/os_keystone_domain_facts.py | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 cloud/openstack/os_keystone_domain_facts.py diff --git a/cloud/openstack/os_keystone_domain_facts.py b/cloud/openstack/os_keystone_domain_facts.py new file mode 100644 index 00000000000..5df2f2b7977 --- /dev/null +++ b/cloud/openstack/os_keystone_domain_facts.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_keystone_domain_facts +short_description: Retrieve facts about one or more OpenStack domains +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Retrieve facts about a one or more OpenStack domains +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - Name or ID of the domain + required: true + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + default: None +''' + +EXAMPLES = ''' +# Gather facts about previously created domain +- os_keystone_domain_facts: + cloud: awesomecloud +- debug: var=openstack_domains + +# Gather facts about a previously created domain by name +- os_keystone_domain_facts: + cloud: awesomecloud + name: demodomain +- debug: var=openstack_domains + +# Gather facts about a previously created domain with filter +- os_keystone_domain_facts + cloud: awesomecloud + name: demodomain + filters: + enabled: False +- debug: var=openstack_domains +''' + + +RETURN = ''' +openstack_domains: + description: has all the OpenStack facts about domains + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: string + name: + description: Name given to the domain. + returned: success + type: string + description: + description: Description of the domain. + returned: success + type: string + enabled: + description: Flag to indicate if the domain is enabled. + returned: success + type: bool +''' + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + module_kwargs = openstack_module_kwargs( + mutually_exclusive=[ + ['name', 'filters'], + ] + ) + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + name = module.params['name'] + filters = module.params['filters'] + + opcloud = shade.operator_cloud(**module.params) + + if name: + # Let's suppose user is passing domain ID + try: + domains = cloud.get_domain(name) + except: + domains = opcloud.search_domains(filters={'name': name}) + + else: + domains = opcloud.search_domains(filters) + + module.exit_json(changed=False, ansible_facts=dict( + openstack_domains=domains)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 34045fddb13f6ffd80810a41d3d9786558afff69 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Wed, 6 Apr 2016 10:22:19 +0000 Subject: [PATCH 02/33] Add os_user_facts module This module gather facts about one or more OpenStack users --- cloud/openstack/os_user_facts.py | 172 +++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 cloud/openstack/os_user_facts.py diff --git a/cloud/openstack/os_user_facts.py b/cloud/openstack/os_user_facts.py new file mode 100644 index 00000000000..db8cebe4757 --- /dev/null +++ b/cloud/openstack/os_user_facts.py @@ -0,0 +1,172 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_user_facts +short_description: Retrieve facts about one or more OpenStack users +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Retrieve facts about a one or more OpenStack users +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - Name or ID of the user + required: true + domain: + description: + - Name or ID of the domain containing the user if the cloud supports domains + required: false + default: None + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + default: None +''' + +EXAMPLES = ''' +# Gather facts about previously created users +- os_user_facts: + cloud: awesomecloud +- debug: var=openstack_users + +# Gather facts about a previously created user by name +- os_user_facts: + cloud: awesomecloud + name: demouser +- debug: var=openstack_users + +# Gather facts about a previously created user in a specific domain +- os_user_facts + cloud: awesomecloud + name: demouser + domain: admindomain +- debug: var=openstack_users + +# Gather facts about a previously created user in a specific domain + with filter +- os_user_facts + cloud: awesomecloud + name: demouser + domain: admindomain + filters: + enabled: False +- debug: var=openstack_users +''' + + +RETURN = ''' +openstack_users: + description: has all the OpenStack facts about users + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: string + name: + description: Name given to the user. + returned: success + type: string + enabled: + description: Flag to indicate if the user is enabled + returned: success + type: bool + domain_id: + description: Domain ID containing the user + returned: success + type: string + default_project_id: + description: Default project ID of the user + returned: success + type: string + email: + description: Email of the user + returned: success + type: string + username: + description: Username of the user + returned: success + type: string +''' + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + + module = AnsibleModule(argument_spec) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + name = module.params['name'] + domain = module.params['domain'] + filters = module.params['filters'] + + opcloud = shade.operator_cloud(**module.params) + + if domain: + try: + # We assume admin is passing domain id + dom = opcloud.get_domain(domain)['id'] + domain = dom + except: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + dom = opcloud.search_domains(filters={'name': domain}) + if dom: + domain = dom[0]['id'] + else: + module.fail_json(msg='Domain name or ID does not exist') + + if not filters: + filters = {} + + filters['domain_id'] = domain + + users = opcloud.search_users(name, + filters) + module.exit_json(changed=False, ansible_facts=dict( + openstack_users=users)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From c03e77a63a37ac7ab9ad12c3e42e633547aa1688 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Sun, 10 Apr 2016 13:33:48 +0200 Subject: [PATCH 03/33] strip whitespace from key and value before inserting it into the config before the following would produce four entries: container_config: - "lxc.network.flags=up" - "lxc.network.flags =up" - "lxc.network.flags= up" - "lxc.network.flags = up" let's strip the whitespace and insert only one "lxc.network.flags = up" into the final config Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ea4952f6b03..3cbff3314e8 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -745,6 +745,8 @@ class LxcContainerManagement(object): config_change = False for key, value in parsed_options: + key = key.strip() + value = value.strip() new_entry = '%s = %s\n' % (key, value) for option_line in container_config: # Look for key in config From 8db3a639837e836dd02030c5177301bb699e5de7 Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Sun, 10 Apr 2016 13:37:00 +0200 Subject: [PATCH 04/33] fix handling of config options that share the same prefix container_config: - "lxc.network.ipv4.gateway=auto" - "lxc.network.ipv4=192.0.2.1" might try to override lxc.network.ipv4.gateway in the second entry as both start with "lxc.network.ipv4". use a regular expression to find a line that contains (optional) whitespace and an = after the key. Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index 3cbff3314e8..d19101dd208 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -424,6 +424,8 @@ lxc_container: sample: True """ +import re + try: import lxc except ImportError: @@ -748,9 +750,10 @@ class LxcContainerManagement(object): key = key.strip() value = value.strip() new_entry = '%s = %s\n' % (key, value) + keyre = re.compile(r'%s(\s+)?=' % key) for option_line in container_config: # Look for key in config - if option_line.startswith(key): + if keyre.match(option_line): _, _value = option_line.split('=', 1) config_value = ' '.join(_value.split()) line_index = container_config.index(option_line) From 2d78c23dc0dab96dd6a5edcb7afbe9730e072d89 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 11 Apr 2016 20:01:27 +0200 Subject: [PATCH 05/33] cloudstack: cs_template: fix cross_zones template removal --- cloud/cloudstack/cs_template.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 8690a6e1756..c61b0a990cc 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -89,8 +89,8 @@ options: default: false cross_zones: description: - - Whether the template should be syned across zones. - - Only used if C(state) is present. + - Whether the template should be syned or removed across zones. + - Only used if C(state) is present or absent. required: false default: false project: @@ -220,6 +220,7 @@ EXAMPLES = ''' - local_action: module: cs_template name: systemvm-4.2 + cross_zones: yes state: absent ''' @@ -560,7 +561,9 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): args = {} args['id'] = template['id'] - args['zoneid'] = self.get_zone(key='id') + + if not self.module.params.get('cross_zones'): + args['zoneid'] = self.get_zone(key='id') if not self.module.check_mode: res = self.cs.deleteTemplate(**args) @@ -620,6 +623,7 @@ def main(): required_together=required_together, mutually_exclusive = ( ['url', 'vm'], + ['zone', 'cross_zones'], ), supports_check_mode=True ) From 0b9c8213adc758243f03c6e90c389530d288563b Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 11 Apr 2016 20:01:36 +0200 Subject: [PATCH 06/33] cloudstack: fix doc, display_text not required --- cloud/cloudstack/cs_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index c61b0a990cc..537d83e3c2e 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -168,7 +168,7 @@ options: display_text: description: - Display text of the template. - required: true + required: false default: null state: description: From 1d0df46475e6194eedaa52622e778e0fee5c7378 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 11 Apr 2016 20:02:03 +0200 Subject: [PATCH 07/33] cloudstack: cs_template: fix state=extracted * url arg is optional but we enforced it * url is in a required together, but args only relevant while registering --- cloud/cloudstack/cs_template.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/cloud/cloudstack/cs_template.py b/cloud/cloudstack/cs_template.py index 537d83e3c2e..e53c8e286e4 100644 --- a/cloud/cloudstack/cs_template.py +++ b/cloud/cloudstack/cs_template.py @@ -470,6 +470,12 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): def register_template(self): + required_params = [ + 'format', + 'url', + 'hypervisor', + ] + self.module.fail_on_missing_params(required_params=required_params) template = self.get_template() if not template: self.result['changed'] = True @@ -537,9 +543,6 @@ class AnsibleCloudStackTemplate(AnsibleCloudStack): args['mode'] = self.module.params.get('mode') args['zoneid'] = self.get_zone(key='id') - if not args['url']: - self.module.fail_json(msg="Missing required arguments: url") - self.result['changed'] = True if not self.module.check_mode: @@ -613,14 +616,9 @@ def main(): poll_async = dict(type='bool', default=True), )) - required_together = cs_required_together() - required_together.extend([ - ['format', 'url', 'hypervisor'], - ]) - module = AnsibleModule( argument_spec=argument_spec, - required_together=required_together, + required_together=cs_required_together(), mutually_exclusive = ( ['url', 'vm'], ['zone', 'cross_zones'], From 98514ace6e9261e37caa5211b5f83e8e195d99cb Mon Sep 17 00:00:00 2001 From: Evgeni Golov Date: Tue, 12 Apr 2016 07:17:12 +0200 Subject: [PATCH 08/33] do not set LXC default config this was accidentally re-introduced in 7120fb4b Signed-off-by: Evgeni Golov --- cloud/lxc/lxc_container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/lxc/lxc_container.py b/cloud/lxc/lxc_container.py index ea4952f6b03..678360afaec 100644 --- a/cloud/lxc/lxc_container.py +++ b/cloud/lxc/lxc_container.py @@ -1684,7 +1684,6 @@ def main(): ), config=dict( type='path', - default='/etc/lxc/default.conf' ), vg_name=dict( type='str', From 6785f3b424c34ade1cf6c6fd23da764f6e278480 Mon Sep 17 00:00:00 2001 From: stoned Date: Tue, 12 Apr 2016 07:21:28 +0200 Subject: [PATCH 09/33] =?UTF-8?q?cpanm:=20search=20both=20its=20stderr=20a?= =?UTF-8?q?nd=20its=20stdout=20for=20the=20message=20'is=20up=20t=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note that since cpanm version 1.6926 its messages are sent to stdout when previously they were sent to stderr. Also there is no need to initialize out_cpanm and err_cpanm and check for their truthiness as module.run_command() and str.find() take care of that. --- packaging/language/cpanm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packaging/language/cpanm.py b/packaging/language/cpanm.py index 919677466ab..769ea5f02fa 100644 --- a/packaging/language/cpanm.py +++ b/packaging/language/cpanm.py @@ -202,7 +202,6 @@ def main(): installed = _is_package_installed(module, name, locallib, cpanm, version) if not installed: - out_cpanm = err_cpanm = '' cmd = _build_cmd_line(name, from_path, notest, locallib, mirror, mirror_only, installdeps, cpanm, use_sudo) rc_cpanm, out_cpanm, err_cpanm = module.run_command(cmd, check_rc=False) @@ -210,7 +209,7 @@ def main(): if rc_cpanm != 0: module.fail_json(msg=err_cpanm, cmd=cmd) - if err_cpanm and 'is up to date' not in err_cpanm: + if (err_cpanm.find('is up to date') == -1 and out_cpanm.find('is up to date') == -1): changed = True module.exit_json(changed=changed, binary=cpanm, name=name) From 2dbfdaa88b447d6599543a3daa7b2023fff8598f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 12 Apr 2016 08:13:24 +0200 Subject: [PATCH 10/33] Remove dead code (#1303) The review on https://github.com/ansible/ansible-modules-extras/pull/1303 show the problem was already fixed, so we just need to remove the code. --- system/firewalld.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/system/firewalld.py b/system/firewalld.py index 29054f37702..2638ff759e8 100644 --- a/system/firewalld.py +++ b/system/firewalld.py @@ -328,14 +328,6 @@ def main(): timeout = module.params['timeout'] interface = module.params['interface'] - ## Check for firewalld running - try: - if fw.connected == False: - module.fail_json(msg='firewalld service must be running') - except AttributeError: - module.fail_json(msg="firewalld connection can't be established,\ - version likely too old. Requires firewalld >= 2.0.11") - modification_count = 0 if service != None: modification_count += 1 From 01a15f8a0b94f1a09901880d90e2901a209bf08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Szczygie=C5=82?= Date: Tue, 12 Apr 2016 11:11:33 +0200 Subject: [PATCH 11/33] VMware datacenter module shouldn't hold pyvmomi context in Ansible module object (#1568) * VMware datacenter module rewritten to don't hold pyvmomi context and objects in Ansible module object fixed exceptions handling added datacenter destroy result, moved checks changed wrong value wrong value again... need some sleep * check_mode fixes * state defaults to present, default changed to true * module check fixes --- cloud/vmware/vmware_datacenter.py | 97 +++++++++++++------------------ 1 file changed, 39 insertions(+), 58 deletions(-) diff --git a/cloud/vmware/vmware_datacenter.py b/cloud/vmware/vmware_datacenter.py index aa85782bbbe..77685616e51 100644 --- a/cloud/vmware/vmware_datacenter.py +++ b/cloud/vmware/vmware_datacenter.py @@ -25,9 +25,9 @@ short_description: Manage VMware vSphere Datacenters description: - Manage VMware vSphere Datacenters version_added: 2.0 -author: "Joseph Callen (@jcpowermac)" +author: "Joseph Callen (@jcpowermac), Kamil Szczygiel (@kamsz)" notes: - - Tested on vSphere 5.5 + - Tested on vSphere 6.0 requirements: - "python >= 2.6" - PyVmomi @@ -54,7 +54,7 @@ options: description: - If the datacenter should be present or absent choices: ['present', 'absent'] - required: True + default: present extends_documentation_fragment: vmware.documentation ''' @@ -64,7 +64,7 @@ EXAMPLES = ''' local_action: > vmware_datacenter hostname="{{ ansible_ssh_host }}" username=root password=vmware - datacenter_name="datacenter" + datacenter_name="datacenter" state=present ''' try: @@ -74,18 +74,28 @@ except ImportError: HAS_PYVMOMI = False -def state_create_datacenter(module): - datacenter_name = module.params['datacenter_name'] - content = module.params['content'] - changed = True - datacenter = None +def get_datacenter(context, module): + try: + datacenter_name = module.params.get('datacenter_name') + datacenter = find_datacenter_by_name(context, datacenter_name) + return datacenter + except vmodl.RuntimeFault as runtime_fault: + module.fail_json(msg=runtime_fault.msg) + except vmodl.MethodFault as method_fault: + module.fail_json(msg=method_fault.msg) - folder = content.rootFolder + +def create_datacenter(context, module): + datacenter_name = module.params.get('datacenter_name') + folder = context.rootFolder try: - if not module.check_mode: - datacenter = folder.CreateDatacenter(name=datacenter_name) - module.exit_json(changed=changed, result=str(datacenter)) + datacenter = get_datacenter(context, module) + if not datacenter: + changed = True + if not module.check_mode: + folder.CreateDatacenter(name=datacenter_name) + module.exit_json(changed=changed) except vim.fault.DuplicateName: module.fail_json(msg="A datacenter with the name %s already exists" % datacenter_name) except vim.fault.InvalidName: @@ -99,34 +109,16 @@ def state_create_datacenter(module): module.fail_json(msg=method_fault.msg) -def check_datacenter_state(module): - datacenter_name = module.params['datacenter_name'] - - try: - content = connect_to_api(module) - datacenter = find_datacenter_by_name(content, datacenter_name) - module.params['content'] = content - - if datacenter is None: - return 'absent' - else: - module.params['datacenter'] = datacenter - return 'present' - except vmodl.RuntimeFault as runtime_fault: - module.fail_json(msg=runtime_fault.msg) - except vmodl.MethodFault as method_fault: - module.fail_json(msg=method_fault.msg) - - -def state_destroy_datacenter(module): - datacenter = module.params['datacenter'] - changed = True +def destroy_datacenter(context, module): result = None try: - if not module.check_mode: - task = datacenter.Destroy_Task() - changed, result = wait_for_task(task) + datacenter = get_datacenter(context, module) + if datacenter: + changed = True + if not module.check_mode: + task = datacenter.Destroy_Task() + changed, result = wait_for_task(task) module.exit_json(changed=changed, result=result) except vim.fault.VimFault as vim_fault: module.fail_json(msg=vim_fault.msg) @@ -136,39 +128,28 @@ def state_destroy_datacenter(module): module.fail_json(msg=method_fault.msg) -def state_exit_unchanged(module): - module.exit_json(changed=False) - - def main(): argument_spec = vmware_argument_spec() argument_spec.update( dict( - datacenter_name=dict(required=True, type='str'), - state=dict(required=True, choices=['present', 'absent'], type='str'), - ) + datacenter_name=dict(required=True, type='str'), + state=dict(default='present', choices=['present', 'absent'], type='str') ) + ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) if not HAS_PYVMOMI: module.fail_json(msg='pyvmomi is required for this module') - datacenter_states = { - 'absent': { - 'present': state_destroy_datacenter, - 'absent': state_exit_unchanged, - }, - 'present': { - 'present': state_exit_unchanged, - 'absent': state_create_datacenter, - } - } - desired_state = module.params['state'] - current_state = check_datacenter_state(module) + context = connect_to_api(module) + state = module.params.get('state') - datacenter_states[desired_state][current_state](module) + if state == 'present': + create_datacenter(context, module) + if state == 'absent': + destroy_datacenter(context, module) from ansible.module_utils.basic import * from ansible.module_utils.vmware import * From 38cb5c61305624e8eb795fe4cb7a0c07ed09e40c Mon Sep 17 00:00:00 2001 From: Andrea Scarpino Date: Tue, 12 Apr 2016 14:07:32 +0200 Subject: [PATCH 12/33] The enable parameter is a boolean, then convert to a boolean. (#1607) At the moment, this only works when 'enable' is equals to 'yes' or 'no'. While I'm on it, I also fixed a typo in the example and added a required parameter. --- windows/win_firewall_rule.ps1 | 1 + windows/win_firewall_rule.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/windows/win_firewall_rule.ps1 b/windows/win_firewall_rule.ps1 index 63ac538e376..21f96bcf33f 100644 --- a/windows/win_firewall_rule.ps1 +++ b/windows/win_firewall_rule.ps1 @@ -212,6 +212,7 @@ $action=Get-Attr $params "action" ""; $misArg = '' # Check the arguments if ($enable -ne $null) { + $enable=ConvertTo-Bool $enable; if ($enable -eq $true) { $fwsettings.Add("Enabled", "yes"); } elseif ($enable -eq $false) { diff --git a/windows/win_firewall_rule.py b/windows/win_firewall_rule.py index 03611a60ef4..2f90e2a6730 100644 --- a/windows/win_firewall_rule.py +++ b/windows/win_firewall_rule.py @@ -114,10 +114,11 @@ EXAMPLES = ''' action: win_firewall_rule args: name: smtp - enabled: yes + enable: yes state: present localport: 25 action: allow + direction: In protocol: TCP ''' From fa65f4dc2b44d213e6d3bd23ca1750f6c7d5171d Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 12 Apr 2016 16:27:18 +0200 Subject: [PATCH 13/33] Mark token as no_log, since that's used for auth (#2011) --- notification/hipchat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/hipchat.py b/notification/hipchat.py index eb6b469ffb6..f7543aa5592 100644 --- a/notification/hipchat.py +++ b/notification/hipchat.py @@ -169,7 +169,7 @@ def main(): module = AnsibleModule( argument_spec=dict( - token=dict(required=True), + token=dict(required=True, no_log=True), room=dict(required=True), msg=dict(required=True), msg_from=dict(default="Ansible", aliases=['from']), From 3c9310d6086ee0a924bed6344fd8b07de8e8918b Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 12 Apr 2016 12:25:59 -0400 Subject: [PATCH 14/33] New OpenStack module os_port_facts (#1986) --- cloud/openstack/os_port_facts.py | 225 +++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cloud/openstack/os_port_facts.py diff --git a/cloud/openstack/os_port_facts.py b/cloud/openstack/os_port_facts.py new file mode 100644 index 00000000000..c987fed0c3c --- /dev/null +++ b/cloud/openstack/os_port_facts.py @@ -0,0 +1,225 @@ +#!/usr/bin/python + +# Copyright (c) 2016 IBM +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +module: os_port_facts +short_description: Retrieve facts about ports within OpenStack. +version_added: "2.1" +author: "David Shrewsbury (@Shrews)" +description: + - Retrieve facts about ports from OpenStack. +notes: + - Facts are placed in the C(openstack_ports) variable. +requirements: + - "python >= 2.6" + - "shade" +options: + port: + description: + - Unique name or ID of a port. + required: false + default: null + filters: + description: + - A dictionary of meta data to use for further filtering. Elements + of this dictionary will be matched against the returned port + dictionaries. Matching is currently limited to strings within + the port dictionary, or strings within nested dictionaries. + required: false + default: null +extends_documentation_fragment: openstack +''' + +EXAMPLES = ''' +# Gather facts about all ports +- os_port_facts: + cloud: mycloud + +# Gather facts about a single port +- os_port_facts: + cloud: mycloud + port: 6140317d-e676-31e1-8a4a-b1913814a471 + +# Gather facts about all ports that have device_id set to a specific value +# and with a status of ACTIVE. +- os_port_facts: + cloud: mycloud + filters: + device_id: 1038a010-3a37-4a9d-82ea-652f1da36597 + status: ACTIVE +''' + +RETURN = ''' +openstack_ports: + description: List of port dictionaries. A subset of the dictionary keys + listed below may be returned, depending on your cloud provider. + returned: always, but can be null + type: complex + contains: + admin_state_up: + description: The administrative state of the router, which is + up (true) or down (false). + returned: success + type: boolean + sample: true + allowed_address_pairs: + description: A set of zero or more allowed address pairs. An + address pair consists of an IP address and MAC address. + returned: success + type: list + sample: [] + "binding:host_id": + description: The UUID of the host where the port is allocated. + returned: success + type: string + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + "binding:profile": + description: A dictionary the enables the application running on + the host to pass and receive VIF port-specific + information to the plug-in. + returned: success + type: dict + sample: {} + "binding:vif_details": + description: A dictionary that enables the application to pass + information about functions that the Networking API + provides. + returned: success + type: dict + sample: {"port_filter": true} + "binding:vif_type": + description: The VIF type for the port. + returned: success + type: dict + sample: "ovs" + "binding:vnic_type": + description: The virtual network interface card (vNIC) type that is + bound to the neutron port. + returned: success + type: string + sample: "normal" + device_id: + description: The UUID of the device that uses this port. + returned: success + type: string + sample: "b4bd682d-234a-4091-aa5b-4b025a6a7759" + device_owner: + description: The UUID of the entity that uses this port. + returned: success + type: string + sample: "network:router_interface" + dns_assignment: + description: DNS assignment information. + returned: success + type: list + dns_name: + description: DNS name + returned: success + type: string + sample: "" + extra_dhcp_opts: + description: A set of zero or more extra DHCP option pairs. + An option pair consists of an option value and name. + returned: success + type: list + sample: [] + fixed_ips: + description: The IP addresses for the port. Includes the IP address + and UUID of the subnet. + returned: success + type: list + id: + description: The UUID of the port. + returned: success + type: string + sample: "3ec25c97-7052-4ab8-a8ba-92faf84148de" + ip_address: + description: The IP address. + returned: success + type: string + sample: "127.0.0.1" + mac_address: + description: The MAC address. + returned: success + type: string + sample: "fa:16:30:5f:10:f1" + name: + description: The port name. + returned: success + type: string + sample: "port_name" + network_id: + description: The UUID of the attached network. + returned: success + type: string + sample: "dd1ede4f-3952-4131-aab6-3b8902268c7d" + port_security_enabled: + description: The port security status. The status is enabled (true) or disabled (false). + returned: success + type: boolean + sample: false + security_groups: + description: The UUIDs of any attached security groups. + returned: success + type: list + status: + description: The port status. + returned: success + type: string + sample: "ACTIVE" + tenant_id: + description: The UUID of the tenant who owns the network. + returned: success + type: string + sample: "51fce036d7984ba6af4f6c849f65ef00" +''' + + +def main(): + argument_spec = openstack_full_argument_spec( + port=dict(required=False), + filters=dict(type='dict', required=False), + ) + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + port = module.params.pop('port') + filters = module.params.pop('filters') + + try: + cloud = shade.openstack_cloud(**module.params) + ports = cloud.search_ports(port, filters) + module.exit_json(changed=False, ansible_facts=dict( + openstack_ports=ports)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 30a46ee542767676c12474c73b56763f2912be4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 12 Apr 2016 18:46:02 +0200 Subject: [PATCH 15/33] cloudstack: cs_instance: fix template not found (#2005) Let users decide which filter should be used to find the template. --- cloud/cloudstack/cs_instance.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index 9674b589da4..eeac04162e1 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -87,6 +87,15 @@ options: - Mutually exclusive with C(template) option. required: false default: null + template_filter: + description: + - Name of the filter used to search for the template or iso. + - Used for params C(iso) or C(template) on C(state=present). + required: false + default: 'executable' + choices: [ 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community' ] + aliases: [ 'iso_filter' ] + version_added: '2.1' hypervisor: description: - Name the hypervisor to be used for creating the new instance. @@ -450,7 +459,7 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): if self.template: return self._get_by_key(key, self.template) - args['templatefilter'] = 'executable' + args['templatefilter'] = self.module.params.get('template_filter') templates = self.cs.listTemplates(**args) if templates: for t in templates['template']: @@ -462,7 +471,7 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): elif iso: if self.iso: return self._get_by_key(key, self.iso) - args['isofilter'] = 'executable' + args['isofilter'] = self.module.params.get('template_filter') isos = self.cs.listIsos(**args) if isos: for i in isos['iso']: @@ -913,6 +922,7 @@ def main(): memory = dict(default=None, type='int'), template = dict(default=None), iso = dict(default=None), + template_filter = dict(default="executable", aliases=['iso_filter'], choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']), networks = dict(type='list', aliases=[ 'network' ], default=None), ip_to_networks = dict(type='list', aliases=['ip_to_network'], default=None), ip_address = dict(defaul=None), From 0fa30f8d9323696607a70f8a4287bc787253be65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 12 Apr 2016 18:46:52 +0200 Subject: [PATCH 16/33] cloudstack, cs_firewall: fix network not found error in return results (#2006) Only a small issue in results. In case of type is ingress, we rely on ip address, but in results we also return the network. Resolving the ip address works without zone params. If the ip address is not located in the default zone and zone param is not set, the network won't be found because default zone was used for the network query listing. However since network param is not used for type ingress we skip the return of the network in results. --- cloud/cloudstack/cs_firewall.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cloud/cloudstack/cs_firewall.py b/cloud/cloudstack/cs_firewall.py index 7a6bfb6c093..b2e5a68a7a0 100644 --- a/cloud/cloudstack/cs_firewall.py +++ b/cloud/cloudstack/cs_firewall.py @@ -234,6 +234,7 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): 'icmptype': 'icmp_type', } self.firewall_rule = None + self.network = None def get_firewall_rule(self): @@ -309,10 +310,11 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): return cidr == rule['cidrlist'] - def get_network(self, key=None, network=None): - if not network: - network = self.module.params.get('network') + def get_network(self, key=None): + if self.network: + return self._get_by_key(key, self.network) + network = self.module.params.get('network') if not network: return None @@ -328,6 +330,7 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): for n in networks['network']: if network in [ n['displaytext'], n['name'], n['id'] ]: + self.network = n return self._get_by_key(key, n) break self.module.fail_json(msg="Network '%s' not found" % network) @@ -392,8 +395,8 @@ class AnsibleCloudStackFirewall(AnsibleCloudStack): super(AnsibleCloudStackFirewall, self).get_result(firewall_rule) if firewall_rule: self.result['type'] = self.module.params.get('type') - if 'networkid' in firewall_rule: - self.result['network'] = self.get_network(key='displaytext', network=firewall_rule['networkid']) + if self.result['type'] == 'egress': + self.result['network'] = self.get_network(key='displaytext') return self.result From 50d159fa1fa1fbf097a0270adadb85424d1a5a31 Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Tue, 12 Apr 2016 19:18:12 +0200 Subject: [PATCH 17/33] New module for creating gitlab users (#966) --- source_control/gitlab_user.py | 348 ++++++++++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 source_control/gitlab_user.py diff --git a/source_control/gitlab_user.py b/source_control/gitlab_user.py new file mode 100644 index 00000000000..9f6fc0db2a3 --- /dev/null +++ b/source_control/gitlab_user.py @@ -0,0 +1,348 @@ +#!/usr/bin/python +# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: gitlab_user +short_description: Creates/updates/deletes Gitlab Users +description: + - When the user does not exists in Gitlab, it will be created. + - When the user does exists and state=absent, the user will be deleted. + - When changes are made to user, the user will be updated. +version_added: "2.1" +author: "Werner Dijkerman (@dj-wasabi)" +requirements: + - pyapi-gitlab python module +options: + server_url: + description: + - Url of Gitlab server, with protocol (http or https). + required: true + validate_certs: + description: + - When using https if SSL certificate needs to be verified. + required: false + default: true + aliases: + - verify_ssl + login_user: + description: + - Gitlab user name. + required: false + default: null + login_password: + description: + - Gitlab password for login_user + required: false + default: null + login_token: + description: + - Gitlab token for logging in. + required: false + default: null + name: + description: + - Name of the user you want to create + required: true + username: + description: + - The username of the user. + required: true + password: + description: + - The password of the user. + required: true + email: + description: + - The email that belongs to the user. + required: true + sshkey_name: + description: + - The name of the sshkey + required: false + default: null + sshkey_file: + description: + - The ssh key itself. + required: false + default: null + group: + description: + - Add user as an member to this group. + required: false + default: null + access_level: + description: + - The access level to the group. One of the following can be used. + - guest + - reporter + - developer + - master + - owner + required: false + default: null + state: + description: + - create or delete group. + - Possible values are present and absent. + required: false + default: present + choices: ["present", "absent"] +''' + +EXAMPLES = ''' +- name: "Delete Gitlab User" + local_action: gitlab_user + server_url="http://gitlab.dj-wasabi.local" + validate_certs=false + login_token="WnUzDsxjy8230-Dy_k" + username=myusername + state=absent + +- name: "Create Gitlab User" + local_action: gitlab_user + server_url="https://gitlab.dj-wasabi.local" + validate_certs=true + login_user=dj-wasabi + login_password="MySecretPassword" + name=My Name + username=myusername + password=mysecretpassword + email=me@home.com + sshkey_name=MySSH + sshkey_file=ssh-rsa AAAAB3NzaC1yc... + state=present +''' + +RETURN = '''# ''' + +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except: + HAS_GITLAB_PACKAGE = False + + +class GitLabUser(object): + def __init__(self, module, git): + self._module = module + self._gitlab = git + + def addToGroup(self, group_id, user_id, access_level): + if access_level == "guest": + level = 10 + elif access_level == "reporter": + level = 20 + elif access_level == "developer": + level = 30 + elif access_level == "master": + level = 40 + elif access_level == "owner": + level = 50 + return self._gitlab.addgroupmember(group_id, user_id, level) + + def createOrUpdateUser(self, user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level): + group_id = '' + arguments = {"name": user_name, + "username": user_username, + "email": user_email} + + if group_name is not None: + if self.existsGroup(group_name): + group_id = self.getGroupId(group_name) + + if self.existsUser(user_username): + self.updateUser(group_id, user_sshkey_name, user_sshkey_file, access_level, arguments) + else: + if self._module.check_mode: + self._module.exit_json(changed=True) + self.createUser(group_id, user_password, user_sshkey_name, user_sshkey_file, access_level, arguments) + + def createUser(self, group_id, user_password, user_sshkey_name, user_sshkey_file, access_level, arguments): + user_changed = False + + # Create the user + user_username = arguments['username'] + user_name = arguments['name'] + user_email = arguments['email'] + if self._gitlab.createuser(password=user_password, **arguments): + user_id = self.getUserId(user_username) + if self._gitlab.addsshkeyuser(user_id=user_id, title=user_sshkey_name, key=user_sshkey_file): + user_changed = True + # Add the user to the group if group_id is not empty + if group_id != '': + if self.addToGroup(group_id, user_id, access_level): + user_changed = True + user_changed = True + + # Exit with change to true or false + if user_changed: + self._module.exit_json(changed=True, result="Created the user") + else: + self._module.exit_json(changed=False) + + def deleteUser(self, user_username): + user_id = self.getUserId(user_username) + + if self._gitlab.deleteuser(user_id): + self._module.exit_json(changed=True, result="Successfully deleted user %s" % user_username) + else: + self._module.exit_json(changed=False, result="User %s already deleted or something went wrong" % user_username) + + def existsGroup(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return True + return False + + def existsUser(self, username): + found_user = self._gitlab.getusers(search=username) + for user in found_user: + if user['id'] != '': + return True + return False + + def getGroupId(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return group['id'] + + def getUserId(self, username): + found_user = self._gitlab.getusers(search=username) + for user in found_user: + if user['id'] != '': + return user['id'] + + def updateUser(self, group_id, user_sshkey_name, user_sshkey_file, access_level, arguments): + user_changed = False + user_username = arguments['username'] + user_id = self.getUserId(user_username) + user_data = self._gitlab.getuser(user_id=user_id) + + # Lets check if we need to update the user + for arg_key, arg_value in arguments.items(): + if user_data[arg_key] != arg_value: + user_changed = True + + if user_changed: + if self._module.check_mode: + self._module.exit_json(changed=True) + self._gitlab.edituser(user_id=user_id, **arguments) + user_changed = True + if self._module.check_mode or self._gitlab.addsshkeyuser(user_id=user_id, title=user_sshkey_name, key=user_sshkey_file): + user_changed = True + if group_id != '': + if self._module.check_mode or self.addToGroup(group_id, user_id, access_level): + user_changed = True + if user_changed: + self._module.exit_json(changed=True, result="The user %s is updated" % user_username) + else: + self._module.exit_json(changed=False, result="The user %s is already up2date" % user_username) + + +def main(): + global user_id + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + login_user=dict(required=False, no_log=True), + login_password=dict(required=False, no_log=True), + login_token=dict(required=False, no_log=True), + name=dict(required=True), + username=dict(required=True), + password=dict(required=True), + email=dict(required=True), + sshkey_name=dict(required=False), + sshkey_file=dict(required=False), + group=dict(required=False), + access_level=dict(required=False, choices=["guest", "reporter", "developer", "master", "owner"]), + state=dict(default="present", choices=["present", "absent"]), + ), + supports_check_mode=True + ) + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install pyapi-gitlab") + + server_url = module.params['server_url'] + verify_ssl = module.params['validate_certs'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_token = module.params['login_token'] + user_name = module.params['name'] + user_username = module.params['username'] + user_password = module.params['password'] + user_email = module.params['email'] + user_sshkey_name = module.params['sshkey_name'] + user_sshkey_file = module.params['sshkey_file'] + group_name = module.params['group'] + access_level = module.params['access_level'] + state = module.params['state'] + + # We need both login_user and login_password or login_token, otherwise we fail. + if login_user is not None and login_password is not None: + use_credentials = True + elif login_token is not None: + use_credentials = False + else: + module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + + # Check if vars are none + if user_sshkey_file is not None and user_sshkey_name is not None: + use_sshkey = True + else: + use_sshkey = False + + if group_name is not None and access_level is not None: + add_to_group = True + group_name = group_name.lower() + else: + add_to_group = False + + user_username = user_username.lower() + + # Lets make an connection to the Gitlab server_url, with either login_user and login_password + # or with login_token + try: + if use_credentials: + git = gitlab.Gitlab(host=server_url) + git.login(user=login_user, password=login_password) + else: + git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) + except Exception, e: + module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) + + # Validate if group exists and take action based on "state" + user = GitLabUser(module, git) + + # Check if user exists, if not exists and state = absent, we exit nicely. + if not user.existsUser(user_username) and state == "absent": + module.exit_json(changed=False, result="User already deleted or does not exists") + else: + # User exists, + if state == "absent": + user.deleteUser(user_username) + else: + user.createOrUpdateUser(user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From 50179aca6969eacbb9ecae3585cd87dc049a5dfc Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Tue, 12 Apr 2016 19:19:25 +0200 Subject: [PATCH 18/33] New module for creating gitlab groups (#967) --- source_control/gitlab_group.py | 215 +++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 source_control/gitlab_group.py diff --git a/source_control/gitlab_group.py b/source_control/gitlab_group.py new file mode 100644 index 00000000000..83bc77857f0 --- /dev/null +++ b/source_control/gitlab_group.py @@ -0,0 +1,215 @@ +#!/usr/bin/python +# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: gitlab_group +short_description: Creates/updates/deletes Gitlab Groups +description: + - When the group does not exists in Gitlab, it will be created. + - When the group does exists and state=absent, the group will be deleted. +version_added: "2.1" +author: "Werner Dijkerman (@dj-wasabi)" +requirements: + - pyapi-gitlab python module +options: + server_url: + description: + - Url of Gitlab server, with protocol (http or https). + required: true + validate_certs: + description: + - When using https if SSL certificate needs to be verified. + required: false + default: true + aliases: + - verify_ssl + login_user: + description: + - Gitlab user name. + required: false + default: null + login_password: + description: + - Gitlab password for login_user + required: false + default: null + login_token: + description: + - Gitlab token for logging in. + required: false + default: null + name: + description: + - Name of the group you want to create. + required: true + path: + description: + - The path of the group you want to create, this will be server_url/group_path + - If not supplied, the group_name will be used. + required: false + default: null + state: + description: + - create or delete group. + - Possible values are present and absent. + required: false + default: "present" + choices: ["present", "absent"] +''' + +EXAMPLES = ''' +- name: "Delete Gitlab Group" + local_action: gitlab_group + server_url="http://gitlab.dj-wasabi.local" + validate_certs=false + login_token="WnUzDsxjy8230-Dy_k" + name=my_first_group + state=absent + +- name: "Create Gitlab Group" + local_action: gitlab_group + server_url="https://gitlab.dj-wasabi.local" + validate_certs=true + login_user=dj-wasabi + login_password="MySecretPassword" + name=my_first_group + path=my_first_group + state=present +''' + +RETURN = '''# ''' + +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except: + HAS_GITLAB_PACKAGE = False + + +class GitLabGroup(object): + def __init__(self, module, git): + self._module = module + self._gitlab = git + + def createGroup(self, group_name, group_path): + if self._module.check_mode: + self._module.exit_json(changed=True) + return self._gitlab.creategroup(group_name, group_path) + + def deleteGroup(self, group_name): + is_group_empty = True + group_id = self.idGroup(group_name) + + for project in self._gitlab.getall(self._gitlab.getprojects): + owner = project['namespace']['name'] + if owner == group_name: + is_group_empty = False + + if is_group_empty: + if self._module.check_mode: + self._module.exit_json(changed=True) + return self._gitlab.deletegroup(group_id) + else: + self._module.fail_json(msg="There are still projects in this group. These needs to be moved or deleted before this group can be removed.") + + def existsGroup(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return True + return False + + def idGroup(self, group_name): + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return group['id'] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + login_user=dict(required=False, no_log=True), + login_password=dict(required=False, no_log=True), + login_token=dict(required=False, no_log=True), + name=dict(required=True), + path=dict(required=False), + state=dict(default="present", choices=["present", "absent"]), + ), + supports_check_mode=True + ) + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg="Missing requried gitlab module (check docs or install with: pip install pyapi-gitlab") + + server_url = module.params['server_url'] + verify_ssl = module.params['validate_certs'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_token = module.params['login_token'] + group_name = module.params['name'] + group_path = module.params['path'] + state = module.params['state'] + + # We need both login_user and login_password or login_token, otherwise we fail. + if login_user is not None and login_password is not None: + use_credentials = True + elif login_token is not None: + use_credentials = False + else: + module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + + # Set group_path to group_name if it is empty. + if group_path is None: + group_path = group_name.replace(" ", "_") + + # Lets make an connection to the Gitlab server_url, with either login_user and login_password + # or with login_token + try: + if use_credentials: + git = gitlab.Gitlab(host=server_url) + git.login(user=login_user, password=login_password) + else: + git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) + except Exception, e: + module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) + + # Validate if group exists and take action based on "state" + group = GitLabGroup(module, git) + group_name = group_name.lower() + group_exists = group.existsGroup(group_name) + + if group_exists and state == "absent": + group.deleteGroup(group_name) + module.exit_json(changed=True, result="Successfully deleted group %s" % group_name) + else: + if state == "absent": + module.exit_json(changed=False, result="Group deleted or does not exists") + else: + if group_exists: + module.exit_json(changed=False) + else: + if group.createGroup(group_name, group_path): + module.exit_json(changed=True, result="Successfully created or updated the group %s" % group_name) + + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From ab2f4c4002ff244b99397efa550eb00275df94e7 Mon Sep 17 00:00:00 2001 From: Werner Dijkerman Date: Tue, 12 Apr 2016 19:20:45 +0200 Subject: [PATCH 19/33] New module for creating gitlab projects (#968) --- source_control/gitlab_project.py | 397 +++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 source_control/gitlab_project.py diff --git a/source_control/gitlab_project.py b/source_control/gitlab_project.py new file mode 100644 index 00000000000..602b9e832d7 --- /dev/null +++ b/source_control/gitlab_project.py @@ -0,0 +1,397 @@ +#!/usr/bin/python +# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: gitlab_project +short_description: Creates/updates/deletes Gitlab Projects +description: + - When the project does not exists in Gitlab, it will be created. + - When the project does exists and state=absent, the project will be deleted. + - When changes are made to the project, the project will be updated. +version_added: "2.1" +author: "Werner Dijkerman (@dj-wasabi)" +requirements: + - pyapi-gitlab python module +options: + server_url: + description: + - Url of Gitlab server, with protocol (http or https). + required: true + validate_certs: + description: + - When using https if SSL certificate needs to be verified. + required: false + default: true + aliases: + - verify_ssl + login_user: + description: + - Gitlab user name. + required: false + default: null + login_password: + description: + - Gitlab password for login_user + required: false + default: null + login_token: + description: + - Gitlab token for logging in. + required: false + default: null + group: + description: + - The name of the group of which this projects belongs to. + - When not provided, project will belong to user which is configured in 'login_user' or 'login_token' + - When provided with username, project will be created for this user. 'login_user' or 'login_token' needs admin rights. + required: false + default: null + name: + description: + - The name of the project + required: true + path: + description: + - The path of the project you want to create, this will be server_url//path + - If not supplied, name will be used. + required: false + default: null + description: + description: + - An description for the project. + required: false + default: null + issues_enabled: + description: + - Whether you want to create issues or not. + - Possible values are true and false. + required: false + default: true + merge_requests_enabled: + description: + - If merge requests can be made or not. + - Possible values are true and false. + required: false + default: true + wiki_enabled: + description: + - If an wiki for this project should be available or not. + - Possible values are true and false. + required: false + default: true + snippets_enabled: + description: + - If creating snippets should be available or not. + - Possible values are true and false. + required: false + default: true + public: + description: + - If the project is public available or not. + - Setting this to true is same as setting visibility_level to 20. + - Possible values are true and false. + required: false + default: false + visibility_level: + description: + - Private. visibility_level is 0. Project access must be granted explicitly for each user. + - Internal. visibility_level is 10. The project can be cloned by any logged in user. + - Public. visibility_level is 20. The project can be cloned without any authentication. + - Possible values are 0, 10 and 20. + required: false + default: 0 + import_url: + description: + - Git repository which will me imported into gitlab. + - Gitlab server needs read access to this git repository. + required: false + default: false + state: + description: + - create or delete project. + - Possible values are present and absent. + required: false + default: "present" + choices: ["present", "absent"] +''' + +EXAMPLES = ''' +- name: "Delete Gitlab Project" + local_action: gitlab_project + server_url="http://gitlab.dj-wasabi.local" + validate_certs=false + login_token="WnUzDsxjy8230-Dy_k" + name=my_first_project + state=absent + +- name: "Create Gitlab Project in group Ansible" + local_action: gitlab_project + server_url="https://gitlab.dj-wasabi.local" + validate_certs=true + login_user=dj-wasabi + login_password="MySecretPassword" + name=my_first_project + group=ansible + issues_enabled=false + wiki_enabled=true + snippets_enabled=true + import_url="http://git.example.com/example/lab.git" + state=present +''' + +RETURN = '''# ''' + +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except: + HAS_GITLAB_PACKAGE = False + + +class GitLabProject(object): + def __init__(self, module, git): + self._module = module + self._gitlab = git + + def createOrUpdateProject(self, project_exists, group_name, import_url, arguments): + is_user = False + group_id = self.getGroupId(group_name) + if not group_id: + group_id = self.getUserId(group_name) + is_user = True + + if project_exists: + # Edit project + return self.updateProject(group_name, arguments) + else: + # Create project + if self._module.check_mode: + self._module.exit_json(changed=True) + return self.createProject(is_user, group_id, import_url, arguments) + + def createProject(self, is_user, user_id, import_url, arguments): + if is_user: + return self._gitlab.createprojectuser(user_id=user_id, import_url=import_url, **arguments) + else: + group_id = user_id + return self._gitlab.createproject(namespace_id=group_id, import_url=import_url, **arguments) + + def deleteProject(self, group_name, project_name): + if self.existsGroup(group_name): + project_owner = group_name + else: + project_owner = self._gitlab.currentuser()['username'] + + search_results = self._gitlab.searchproject(search=project_name) + for result in search_results: + owner = result['namespace']['name'] + if owner == project_owner: + return self._gitlab.deleteproject(result['id']) + + def existsProject(self, group_name, project_name): + if self.existsGroup(group_name): + project_owner = group_name + else: + project_owner = self._gitlab.currentuser()['username'] + + search_results = self._gitlab.searchproject(search=project_name) + for result in search_results: + owner = result['namespace']['name'] + if owner == project_owner: + return True + return False + + def existsGroup(self, group_name): + if group_name is not None: + # Find the group, if group not exists we try for user + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return True + + user_name = group_name + user_data = self._gitlab.getusers(search=user_name) + for data in user_data: + if 'id' in user_data: + return True + return False + + def getGroupId(self, group_name): + if group_name is not None: + # Find the group, if group not exists we try for user + for group in self._gitlab.getall(self._gitlab.getgroups): + if group['name'] == group_name: + return group['id'] + + def getProjectId(self, group_name, project_name): + if self.existsGroup(group_name): + project_owner = group_name + else: + project_owner = self._gitlab.currentuser()['username'] + + search_results = self._gitlab.searchproject(search=project_name) + for result in search_results: + owner = result['namespace']['name'] + if owner == project_owner: + return result['id'] + + def getUserId(self, user_name): + user_data = self._gitlab.getusers(search=user_name) + + for data in user_data: + if 'id' in data: + return data['id'] + return self._gitlab.currentuser()['id'] + + def to_bool(self, value): + if value: + return 1 + else: + return 0 + + def updateProject(self, group_name, arguments): + project_changed = False + project_name = arguments['name'] + project_id = self.getProjectId(group_name, project_name) + project_data = self._gitlab.getproject(project_id=project_id) + + for arg_key, arg_value in arguments.items(): + project_data_value = project_data[arg_key] + + if isinstance(project_data_value, bool) or project_data_value is None: + to_bool = self.to_bool(project_data_value) + if to_bool != arg_value: + project_changed = True + continue + else: + if project_data_value != arg_value: + project_changed = True + + if project_changed: + if self._module.check_mode: + self._module.exit_json(changed=True) + return self._gitlab.editproject(project_id=project_id, **arguments) + else: + return False + + +def main(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True), + validate_certs=dict(required=False, default=True, type=bool, aliases=['verify_ssl']), + login_user=dict(required=False, no_log=True), + login_password=dict(required=False, no_log=True), + login_token=dict(required=False, no_log=True), + group=dict(required=False), + name=dict(required=True), + path=dict(required=False), + description=dict(required=False), + issues_enabled=dict(default=True, type=bool), + merge_requests_enabled=dict(default=True, type=bool), + wiki_enabled=dict(default=True, type=bool), + snippets_enabled=dict(default=True, type=bool), + public=dict(default=False, type=bool), + visibility_level=dict(default="0", choices=["0", "10", "20"]), + import_url=dict(required=False), + state=dict(default="present", choices=["present", 'absent']), + ), + supports_check_mode=True + ) + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install pyapi-gitlab") + + server_url = module.params['server_url'] + verify_ssl = module.params['validate_certs'] + login_user = module.params['login_user'] + login_password = module.params['login_password'] + login_token = module.params['login_token'] + group_name = module.params['group'] + project_name = module.params['name'] + project_path = module.params['path'] + description = module.params['description'] + issues_enabled = module.params['issues_enabled'] + merge_requests_enabled = module.params['merge_requests_enabled'] + wiki_enabled = module.params['wiki_enabled'] + snippets_enabled = module.params['snippets_enabled'] + public = module.params['public'] + visibility_level = module.params['visibility_level'] + import_url = module.params['import_url'] + state = module.params['state'] + + # We need both login_user and login_password or login_token, otherwise we fail. + if login_user is not None and login_password is not None: + use_credentials = True + elif login_token is not None: + use_credentials = False + else: + module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + + # Set project_path to project_name if it is empty. + if project_path is None: + project_path = project_name.replace(" ", "_") + + # Gitlab API makes no difference between upper and lower cases, so we lower them. + project_name = project_name.lower() + project_path = project_path.lower() + if group_name is not None: + group_name = group_name.lower() + + # Lets make an connection to the Gitlab server_url, with either login_user and login_password + # or with login_token + try: + if use_credentials: + git = gitlab.Gitlab(host=server_url) + git.login(user=login_user, password=login_password) + else: + git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) + except Exception, e: + module.fail_json(msg="Failed to connect to Gitlab server: %s " % e) + + # Validate if project exists and take action based on "state" + project = GitLabProject(module, git) + project_exists = project.existsProject(group_name, project_name) + + # Creating the project dict + arguments = {"name": project_name, + "path": project_path, + "description": description, + "issues_enabled": project.to_bool(issues_enabled), + "merge_requests_enabled": project.to_bool(merge_requests_enabled), + "wiki_enabled": project.to_bool(wiki_enabled), + "snippets_enabled": project.to_bool(snippets_enabled), + "public": project.to_bool(public), + "visibility_level": int(visibility_level)} + + if project_exists and state == "absent": + project.deleteProject(group_name, project_name) + module.exit_json(changed=True, result="Successfully deleted project %s" % project_name) + else: + if state == "absent": + module.exit_json(changed=False, result="Project deleted or does not exists") + else: + if project.createOrUpdateProject(project_exists, group_name, import_url, arguments): + module.exit_json(changed=True, result="Successfully created or updated the project %s" % project_name) + else: + module.exit_json(changed=False) + +from ansible.module_utils.basic import * + +if __name__ == '__main__': + main() From c65bc5f43d5fb480825a34060006a78929a254c6 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Tue, 5 Apr 2016 17:14:23 +0000 Subject: [PATCH 20/33] Add os_project_facts module This module gathers facts about OpenStack projects --- cloud/openstack/os_project_facts.py | 163 ++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 cloud/openstack/os_project_facts.py diff --git a/cloud/openstack/os_project_facts.py b/cloud/openstack/os_project_facts.py new file mode 100644 index 00000000000..87d3a1e9d76 --- /dev/null +++ b/cloud/openstack/os_project_facts.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# Copyright (c) 2016 Hewlett-Packard Enterprise Corporation +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +DOCUMENTATION = ''' +--- +module: os_project_facts +short_description: Retrieve facts about one or more OpenStack projects +extends_documentation_fragment: openstack +version_added: "2.1" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +description: + - Retrieve facts about a one or more OpenStack projects +requirements: + - "python >= 2.6" + - "shade" +options: + name: + description: + - Name or ID of the project + required: true + domain: + description: + - Name or ID of the domain containing the project if the cloud supports domains + required: false + default: None + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + default: None +''' + +EXAMPLES = ''' +# Gather facts about previously created projects +- os_project_facts: + cloud: awesomecloud +- debug: var=openstack_projects + +# Gather facts about a previously created project by name +- os_project_facts: + cloud: awesomecloud + name: demoproject +- debug: var=openstack_projects + +# Gather facts about a previously created project in a specific domain +- os_project_facts + cloud: awesomecloud + name: demoproject + domain: admindomain +- debug: var=openstack_projects + +# Gather facts about a previously created project in a specific domain + with filter +- os_project_facts + cloud: awesomecloud + name: demoproject + domain: admindomain + filters: + enabled: False +- debug: var=openstack_projects +''' + + +RETURN = ''' +openstack_projects: + description: has all the OpenStack facts about projects + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: string + name: + description: Name given to the project. + returned: success + type: string + description: + description: Description of the project + returned: success + type: string + enabled: + description: Flag to indicate if the project is enabled + returned: success + type: bool + domain_id: + description: Domain ID containing the project (keystone v3 clouds only) + returned: success + type: bool +''' + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + domain=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None), + ) + + module = AnsibleModule(argument_spec) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + + try: + name = module.params['name'] + domain = module.params['domain'] + filters = module.params['filters'] + + opcloud = shade.operator_cloud(**module.params) + + if domain: + try: + # We assume admin is passing domain id + dom = opcloud.get_domain(domain)['id'] + domain = dom + except: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + dom = opcloud.search_domains(filters={'name': domain}) + if dom: + domain = dom[0]['id'] + else: + module.fail_json(msg='Domain name or ID does not exist') + + if not filters: + filters = {} + + filters['domain_id'] = domain + + projects = opcloud.search_projects(name, filters) + module.exit_json(changed=False, ansible_facts=dict( + openstack_projects=projects)) + + except shade.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * + +if __name__ == '__main__': + main() From 10def11d39f787a810834610251201d68bdd765e Mon Sep 17 00:00:00 2001 From: Jens Carl Date: Tue, 12 Apr 2016 15:10:41 -0700 Subject: [PATCH 21/33] Fix code example (#2018) --- cloud/amazon/GUIDELINES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/GUIDELINES.md b/cloud/amazon/GUIDELINES.md index 0c831946be6..017ff9090db 100644 --- a/cloud/amazon/GUIDELINES.md +++ b/cloud/amazon/GUIDELINES.md @@ -79,7 +79,7 @@ except ImportError: def main(): - if not HAS_BOTO: + if not HAS_BOTO3: module.fail_json(msg='boto required for this module') ``` From 85c1440edea9d2cefdd85c686770df6ea523c060 Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Wed, 13 Apr 2016 11:02:42 +0200 Subject: [PATCH 22/33] Tweak and extend the pkgin module - make path to pkgin a global and stop passing it around; it's not going to change while ansible is running - add support for several new options: * upgrade * full_upgrade * force * clean - allow for update_cache to be run in the same task as upgrading/installing packages instead of needing a separate task for that --- packaging/os/pkgin.py | 160 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 28 deletions(-) diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index 5277f218242..cd48385eefe 100755 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -3,6 +3,7 @@ # Copyright (c) 2013 Shaun Zinck # Copyright (c) 2015 Lawrence Leonard Gilbert +# Copyright (c) 2016 Jasper Lievisse Adriaanse # # Written by Shaun Zinck # Based on pacman module written by Afterburn @@ -33,6 +34,7 @@ version_added: "1.0" author: - "Larry Gilbert (L2G)" - "Shaun Zinck (@szinck)" + - "Jasper Lievisse Adriaanse (@jasperla)" notes: - "Known bug with pkgin < 0.8.0: if a package is removed and another package depends on it, the other package will be silently removed as @@ -57,6 +59,34 @@ options: default: no choices: [ "yes", "no" ] version_added: "2.1" + upgrade: + description: + - Upgrade main packages to their newer versions + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" + full_upgrade: + description: + - Upgrade all packages to their newer versions + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" + clean: + description: + - Clean packages cache + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" + force: + description: + - Force package reinstall + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.1" ''' EXAMPLES = ''' @@ -74,12 +104,24 @@ EXAMPLES = ''' # Update repositories as a separate step - pkgin: update_cache=yes + +# Upgrade main packages (equivalent to C(pkgin upgrade)) +- pkgin: upgrade=yes + +# Upgrade all packages (equivalent to C(pkgin full-upgrade)) +- pkgin: full_upgrade=yes + +# Force-upgrade all packages (equivalent to C(pkgin -F full-upgrade)) +- pkgin: full_upgrade=yes force=yes + +# clean packages cache (equivalent to C(pkgin clean)) +- pkgin: clean=yes ''' import re -def query_package(module, pkgin_path, name): +def query_package(module, name): """Search for the package by name. Possible return values: @@ -89,7 +131,7 @@ def query_package(module, pkgin_path, name): """ # test whether '-p' (parsable) flag is supported. - rc, out, err = module.run_command("%s -p -v" % pkgin_path) + rc, out, err = module.run_command("%s -p -v" % PKGIN_PATH) if rc == 0: pflag = '-p' @@ -100,7 +142,7 @@ def query_package(module, pkgin_path, name): # Use "pkgin search" to find the package. The regular expression will # only match on the complete name. - rc, out, err = module.run_command("%s %s search \"^%s$\"" % (pkgin_path, pflag, name)) + rc, out, err = module.run_command("%s %s search \"^%s$\"" % (PKGIN_PATH, pflag, name)) # rc will not be 0 unless the search was a success if rc == 0: @@ -162,37 +204,43 @@ def format_action_message(module, action, count): return message + "s" -def format_pkgin_command(module, pkgin_path, command, package=None): +def format_pkgin_command(module, command, package=None): # Not all commands take a package argument, so cover this up by passing # an empty string. Some commands (e.g. 'update') will ignore extra # arguments, however this behaviour cannot be relied on for others. if package is None: package = "" - vars = { "pkgin": pkgin_path, + if module.params["force"]: + force = "-F" + else: + force = "" + + vars = { "pkgin": PKGIN_PATH, "command": command, - "package": package } + "package": package, + "force": force} if module.check_mode: return "%(pkgin)s -n %(command)s %(package)s" % vars else: - return "%(pkgin)s -y %(command)s %(package)s" % vars + return "%(pkgin)s -y %(force)s %(command)s %(package)s" % vars -def remove_packages(module, pkgin_path, packages): +def remove_packages(module, packages): remove_c = 0 # Using a for loop incase of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - if not query_package(module, pkgin_path, package): + if not query_package(module, package): continue rc, out, err = module.run_command( - format_pkgin_command(module, pkgin_path, "remove", package)) + format_pkgin_command(module, "remove", package)) - if not module.check_mode and query_package(module, pkgin_path, package): + if not module.check_mode and query_package(module, package): module.fail_json(msg="failed to remove %s: %s" % (package, out)) remove_c += 1 @@ -203,18 +251,18 @@ def remove_packages(module, pkgin_path, packages): module.exit_json(changed=False, msg="package(s) already absent") -def install_packages(module, pkgin_path, packages): +def install_packages(module, packages): install_c = 0 for package in packages: - if query_package(module, pkgin_path, package): + if query_package(module, package): continue rc, out, err = module.run_command( - format_pkgin_command(module, pkgin_path, "install", package)) + format_pkgin_command(module, "install", package)) - if not module.check_mode and not query_package(module, pkgin_path, package): + if not module.check_mode and not query_package(module, package): module.fail_json(msg="failed to install %s: %s" % (package, out)) install_c += 1 @@ -224,41 +272,97 @@ def install_packages(module, pkgin_path, packages): module.exit_json(changed=False, msg="package(s) already present") -def update_package_db(module, pkgin_path): +def update_package_db(module): rc, out, err = module.run_command( - format_pkgin_command(module, pkgin_path, "update")) + format_pkgin_command(module, "update")) if rc == 0: - return True + if re.search('database for.*is up-to-date\n$', out): + return False, "datebase is up-to-date" + else: + return True, "updated repository database" else: module.fail_json(msg="could not update package db") +def do_upgrade_packages(module, full=False): + if full: + cmd = "full-upgrade" + else: + cmd = "upgrade" + + rc, out, err = module.run_command( + format_pkgin_command(module, cmd)) + + if rc == 0: + if re.search('^nothing to do.\n$', out): + module.exit_json(changed=False, msg="nothing left to upgrade") + else: + module.fail_json(msg="could not %s packages" % cmd) + +def upgrade_packages(module): + do_upgrade_packages(module) + +def full_upgrade_packages(module): + do_upgrade_packages(module, True) + +def clean_cache(module): + rc, out, err = module.run_command( + format_pkgin_command(module, "clean")) + + if rc == 0: + # There's no indication if 'clean' actually removed anything, + # so assume it did. + module.exit_json(changed=True, msg="cleaned caches") + else: + module.fail_json(msg="could not clean package cache") def main(): module = AnsibleModule( argument_spec = dict( state = dict(default="present", choices=["present","absent"]), name = dict(aliases=["pkg"], type='list'), - update_cache = dict(default='no', type='bool')), - required_one_of = [['name', 'update_cache']], + update_cache = dict(default='no', type='bool'), + upgrade = dict(default='no', type='bool'), + full_upgrade = dict(default='no', type='bool'), + clean = dict(default='no', type='bool'), + force = dict(default='no', type='bool')), + required_one_of = [['name', 'update_cache', 'upgrade', 'full_upgrade', 'clean']], supports_check_mode = True) - pkgin_path = module.get_bin_path('pkgin', True, ['/opt/local/bin']) + global PKGIN_PATH + PKGIN_PATH = module.get_bin_path('pkgin', True, ['/opt/local/bin']) + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') p = module.params + if p["update_cache"]: + c, msg = update_package_db(module) + if not (p['name'] or p["upgrade"] or p["full_upgrade"]): + module.exit_json(changed=c, msg=msg) + + if p["upgrade"]: + upgrade_packages(module) + if not p['name']: + module.exit_json(changed=True, msg='upgraded packages') + + if p["full_upgrade"]: + full_upgrade_packages(module) + if not p['name']: + module.exit_json(changed=True, msg='upgraded all packages') + + if p["clean"]: + clean_cache(module) + if not p['name']: + module.exit_json(changed=True, msg='cleaned caches') + pkgs = p["name"] - if p["update_cache"]: - update_package_db(module, pkgin_path) - if not p['name']: - module.exit_json(changed=True, msg='updated repository database') - if p["state"] == "present": - install_packages(module, pkgin_path, pkgs) + install_packages(module, pkgs) elif p["state"] == "absent": - remove_packages(module, pkgin_path, pkgs) + remove_packages(module, pkgs) # import module snippets from ansible.module_utils.basic import * From 2b8debbc2bda8bb55f627e8b8047812aa59409af Mon Sep 17 00:00:00 2001 From: Jasper Lievisse Adriaanse Date: Wed, 13 Apr 2016 16:03:26 +0200 Subject: [PATCH 23/33] Sprinkle some LANG/LC_* where command output is parsed (#2019) --- packaging/os/homebrew.py | 3 +++ packaging/os/homebrew_cask.py | 3 +++ system/svc.py | 2 ++ 3 files changed, 8 insertions(+) mode change 100644 => 100755 packaging/os/homebrew.py mode change 100644 => 100755 packaging/os/homebrew_cask.py mode change 100644 => 100755 system/svc.py diff --git a/packaging/os/homebrew.py b/packaging/os/homebrew.py old mode 100644 new mode 100755 index 94d0ef865c4..077fd46dcc6 --- a/packaging/os/homebrew.py +++ b/packaging/os/homebrew.py @@ -813,6 +813,9 @@ def main(): ), supports_check_mode=True, ) + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + p = module.params if p['name']: diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py old mode 100644 new mode 100755 index e1b721a97b4..aa7d7ed84b8 --- a/packaging/os/homebrew_cask.py +++ b/packaging/os/homebrew_cask.py @@ -481,6 +481,9 @@ def main(): ), supports_check_mode=True, ) + + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + p = module.params if p['name']: diff --git a/system/svc.py b/system/svc.py old mode 100644 new mode 100755 index 0d3a83f6305..6cc8c1d21ef --- a/system/svc.py +++ b/system/svc.py @@ -249,6 +249,8 @@ def main(): supports_check_mode=True, ) + module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') + state = module.params['state'] enabled = module.params['enabled'] downed = module.params['downed'] From 4b3ab52374cc6e17cc88e290f016e0a4d0057f6b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 13 Apr 2016 20:32:47 +0200 Subject: [PATCH 24/33] Fix arguments for pushover module Since user_key and app_token are used for authentication, I suspect both of them should be kept secret. According to the API manual, https://pushover.net/api priority go from -2 to 2, so the argument should be constrained. --- notification/pushover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notification/pushover.py b/notification/pushover.py index 29afcaa6356..2cd973b1bcc 100644 --- a/notification/pushover.py +++ b/notification/pushover.py @@ -95,9 +95,9 @@ def main(): module = AnsibleModule( argument_spec=dict( msg=dict(required=True), - app_token=dict(required=True), - user_key=dict(required=True), - pri=dict(required=False, default=0), + app_token=dict(required=True, no_log=True), + user_key=dict(required=True, no_log=True), + pri=dict(required=False, default='0', choices=['-2','-1','0','1','2']), ), ) From 8c6a3e732edc2f70be63702c46d8d295553f5e33 Mon Sep 17 00:00:00 2001 From: "Christopher M. Fuhrman" Date: Thu, 14 Apr 2016 03:51:29 -0700 Subject: [PATCH 25/33] pkgin: Fix bad regexp which did not catch packages such as p5-SVN-Notify The previous version of my regexp did not take into account packages such as 'p5-Perl-Tidy' or 'p5-Test-Output', so use a greedy match up to the last occurrance of '-' for matching the package. This regex has been extensively tested using all packages as provided by pkgsrc-2016Q1[1]. Footnotes: [1] http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/?only_with_tag=pkgsrc-2016Q1 --- packaging/os/pkgin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/pkgin.py b/packaging/os/pkgin.py index cd48385eefe..055891ebe08 100755 --- a/packaging/os/pkgin.py +++ b/packaging/os/pkgin.py @@ -164,7 +164,7 @@ def query_package(module, name): # Search for package, stripping version # (results in sth like 'gcc47-libs' or 'emacs24-nox11') - pkg_search_obj = re.search(r'^([a-zA-Z]+[0-9]*[\-]*\w*)-[0-9]', pkgname_with_version, re.M) + pkg_search_obj = re.search(r'^(.*?)\-[0-9][0-9.]*(nb[0-9]+)*', pkgname_with_version, re.M) # Do not proceed unless we have a match if not pkg_search_obj: From e8dbb4e4f290486dbcb114e35ec10205649d7a17 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 14 Apr 2016 21:15:07 +0200 Subject: [PATCH 26/33] Mark conf_file as a path, for various user expansion --- packaging/os/dnf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py index 2bd279785f6..8df9401fa16 100644 --- a/packaging/os/dnf.py +++ b/packaging/os/dnf.py @@ -323,7 +323,7 @@ def main(): enablerepo=dict(type='list', default=[]), disablerepo=dict(type='list', default=[]), list=dict(), - conf_file=dict(default=None), + conf_file=dict(default=None, type='path'), disable_gpg_check=dict(default=False, type='bool'), ), required_one_of=[['name', 'list']], From bd0deed367b94c7aedf7d006d5a6f4d4a18ad62f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 14 Apr 2016 23:37:01 +0200 Subject: [PATCH 27/33] Use type=path for pem_file, since that's a file (#1934) --- cloud/google/gce_img.py | 2 +- cloud/google/gce_tag.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/google/gce_img.py b/cloud/google/gce_img.py index b64f12febd0..bf3d9da5356 100644 --- a/cloud/google/gce_img.py +++ b/cloud/google/gce_img.py @@ -180,7 +180,7 @@ def main(): state=dict(default='present', choices=['present', 'absent']), zone=dict(default='us-central1-a'), service_account_email=dict(), - pem_file=dict(), + pem_file=dict(type='path'), project_id=dict(), timeout=dict(type='int', default=180) ) diff --git a/cloud/google/gce_tag.py b/cloud/google/gce_tag.py index 4f60f58f760..cb1f2a2c3ed 100644 --- a/cloud/google/gce_tag.py +++ b/cloud/google/gce_tag.py @@ -188,7 +188,7 @@ def main(): state=dict(default='present', choices=['present', 'absent']), zone=dict(default='us-central1-a'), service_account_email=dict(), - pem_file=dict(), + pem_file=dict(type='path'), project_id=dict(), ) ) From 84ca3ba7eecbcb9c1ebcfbcbb6ab98f97d1c097e Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 14 Apr 2016 23:44:28 +0200 Subject: [PATCH 28/33] Do not use a default value for -n parameter, fix #1400 (#1417) --- messaging/rabbitmq_user.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/messaging/rabbitmq_user.py b/messaging/rabbitmq_user.py index ba77e47c998..85921ce45c7 100644 --- a/messaging/rabbitmq_user.py +++ b/messaging/rabbitmq_user.py @@ -144,7 +144,9 @@ class RabbitMqUser(object): def _exec(self, args, run_in_check_mode=False): if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): - cmd = [self._rabbitmqctl, '-q', '-n', self.node] + cmd = [self._rabbitmqctl, '-q'] + if self.node is not None: + cmd.append(['-n', self.node]) rc, out, err = self.module.run_command(cmd + args, check_rc=True) return out.splitlines() return list() @@ -235,7 +237,7 @@ def main(): read_priv=dict(default='^$'), force=dict(default='no', type='bool'), state=dict(default='present', choices=['present', 'absent']), - node=dict(default='rabbit') + node=dict(default=None) ) module = AnsibleModule( argument_spec=arg_spec, From 3afe117730c7c4e046254fdd77c25fd01761fd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jos=C3=A9=20Pando?= Date: Thu, 14 Apr 2016 17:58:44 -0400 Subject: [PATCH 29/33] Add SQS queue policy attachment functionality (#1716) * Add SQS queue policy attachment functionality SQS queue has no attribute 'Policy' until one is attached, so this special case must be handled uniquely SQS queue Policy can now be passed in as json --- cloud/amazon/sqs_queue.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/sqs_queue.py b/cloud/amazon/sqs_queue.py index de0ca7ebff1..a16db036b01 100644 --- a/cloud/amazon/sqs_queue.py +++ b/cloud/amazon/sqs_queue.py @@ -22,7 +22,9 @@ description: - Create or delete AWS SQS queues. - Update attributes on existing queues. version_added: "2.0" -author: Alan Loi (@loia) +author: + - Alan Loi (@loia) + - Fernando Jose Pando (@nand0p) requirements: - "boto >= 2.33.0" options: @@ -61,6 +63,12 @@ options: - The receive message wait time in seconds. required: false default: null + policy: + description: + - The json dict policy to attach to queue + required: false + default: null + version_added: "2.1" extends_documentation_fragment: - aws - ec2 @@ -76,6 +84,7 @@ EXAMPLES = ''' maximum_message_size: 1024 delivery_delay: 30 receive_message_wait_time: 20 + policy: "{{ json_dict }}" # Delete SQS queue - sqs_queue: @@ -102,6 +111,7 @@ def create_or_update_sqs_queue(connection, module): maximum_message_size=module.params.get('maximum_message_size'), delivery_delay=module.params.get('delivery_delay'), receive_message_wait_time=module.params.get('receive_message_wait_time'), + policy=module.params.get('policy'), ) result = dict( @@ -136,7 +146,8 @@ def update_sqs_queue(queue, message_retention_period=None, maximum_message_size=None, delivery_delay=None, - receive_message_wait_time=None): + receive_message_wait_time=None, + policy=None): changed = False changed = set_queue_attribute(queue, 'VisibilityTimeout', default_visibility_timeout, @@ -149,6 +160,8 @@ def update_sqs_queue(queue, check_mode=check_mode) or changed changed = set_queue_attribute(queue, 'ReceiveMessageWaitTimeSeconds', receive_message_wait_time, check_mode=check_mode) or changed + changed = set_queue_attribute(queue, 'Policy', policy, + check_mode=check_mode) or changed return changed @@ -156,7 +169,17 @@ def set_queue_attribute(queue, attribute, value, check_mode=False): if not value: return False - existing_value = queue.get_attributes(attributes=attribute)[attribute] + try: + existing_value = queue.get_attributes(attributes=attribute)[attribute] + except: + existing_value = '' + + # convert dict attributes to JSON strings (sort keys for comparing) + if attribute is 'Policy': + value = json.dumps(value, sort_keys=True) + if existing_value: + existing_value = json.dumps(json.loads(existing_value), sort_keys=True) + if str(value) != existing_value: if not check_mode: queue.set_attribute(attribute, value) @@ -200,6 +223,7 @@ def main(): maximum_message_size=dict(type='int'), delivery_delay=dict(type='int'), receive_message_wait_time=dict(type='int'), + policy=dict(type='dict', required=False), )) module = AnsibleModule( From 03af9ab49193181a6ab186af3a9fffe153e69aff Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 15 Apr 2016 15:45:37 +0200 Subject: [PATCH 30/33] Set api_key as no_log, since that's likely something that should be kept private (#2038) --- notification/pushbullet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification/pushbullet.py b/notification/pushbullet.py index dfd89af577d..0d5ab7c4d48 100644 --- a/notification/pushbullet.py +++ b/notification/pushbullet.py @@ -108,7 +108,7 @@ else: def main(): module = AnsibleModule( argument_spec = dict( - api_key = dict(type='str', required=True), + api_key = dict(type='str', required=True, no_log=True), channel = dict(type='str', default=None), device = dict(type='str', default=None), push_type = dict(type='str', default="note", choices=['note', 'link']), From ff74fc00727290cab6ac8fca075a49b82f419420 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 15 Apr 2016 16:18:05 +0200 Subject: [PATCH 31/33] Remove the +x from crypttab and cronvar (#2039) While this change nothing, it is better to enforce consistency --- system/cronvar.py | 0 system/crypttab.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 system/cronvar.py mode change 100755 => 100644 system/crypttab.py diff --git a/system/cronvar.py b/system/cronvar.py old mode 100755 new mode 100644 diff --git a/system/crypttab.py b/system/crypttab.py old mode 100755 new mode 100644 From 8e7051ad9da4f9e98a18db70e79639a921845919 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 15 Apr 2016 16:27:47 +0200 Subject: [PATCH 32/33] Do not leak password by error for ovirt module (#1991) --- cloud/misc/ovirt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 760d4ffc62c..02596b441ea 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -333,7 +333,7 @@ def main(): user = dict(required=True), url = dict(required=True), instance_name = dict(required=True, aliases=['vmname']), - password = dict(required=True), + password = dict(required=True, no_log=True), image = dict(), resource_type = dict(choices=['new', 'template']), zone = dict(), From a61742e070f399af223e7faff585612f4f30ba99 Mon Sep 17 00:00:00 2001 From: Karim Boumedhel Date: Fri, 15 Apr 2016 20:23:37 +0200 Subject: [PATCH 33/33] Add cloudinit support to ovirt.py module --- cloud/misc/ovirt.py | 95 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/cloud/misc/ovirt.py b/cloud/misc/ovirt.py index 02596b441ea..86c769bdb24 100644 --- a/cloud/misc/ovirt.py +++ b/cloud/misc/ovirt.py @@ -144,6 +144,48 @@ options: default: null required: false aliases: [] + instance_dns: + description: + - define the instance's Primary DNS server + required: false + aliases: [ dns ] + version_added: "2.1" + instance_domain: + description: + - define the instance's Domain + required: false + aliases: [ domain ] + version_added: "2.1" + instance_hostname: + description: + - define the instance's Hostname + required: false + aliases: [ hostname ] + version_added: "2.1" + instance_ip: + description: + - define the instance's IP + required: false + aliases: [ ip ] + version_added: "2.1" + instance_netmask: + description: + - define the instance's Netmask + required: false + aliases: [ netmask ] + version_added: "2.1" + instance_rootpw: + description: + - define the instance's Root password + required: false + aliases: [ rootpw ] + version_added: "2.1" + instance_key: + description: + - define the instance's Authorized key + required: false + aliases: [ key ] + version_added: "2.1" state: description: - create, terminate or remove instances @@ -205,6 +247,19 @@ ovirt: password: secret url: https://ovirt.example.com +# starting an instance with cloud init information +ovirt: + instance_name: testansible + state: started + user: admin@internal + password: secret + url: https://ovirt.example.com + hostname: testansible + domain: ansible.local + ip: 192.168.1.100 + netmask: 255.255.255.0 + gateway: 192.168.1.1 + rootpw: bigsecret ''' @@ -273,9 +328,23 @@ def create_vm_template(conn, vmname, image, zone): # start instance -def vm_start(conn, vmname): +def vm_start(conn, vmname, hostname=None, ip=None, netmask=None, gateway=None, + domain=None, dns=None, rootpw=None, key=None): vm = conn.vms.get(name=vmname) - vm.start() + use_cloud_init = False + nics = None + if hostname or ip or netmask or gateway or domain or dns or rootpw or key: + use_cloud_init = True + if ip and netmask and gateway: + ipinfo = params.IP(address=ip, netmask=netmask, gateway=gateway) + nic = params.GuestNicConfiguration(name='eth0', boot_protocol='STATIC', ip=ipinfo, on_boot=True) + nics = params.Nics() + nics = params.GuestNicsConfiguration(nic_configuration=[nic]) + initialization=params.Initialization(regenerate_ssh_keys=True, host_name=hostname, domain=domain, user_name='root', + root_password=rootpw, nic_configurations=nics, dns_servers=dns, + authorized_ssh_keys=key) + action = params.Action(use_cloud_init=use_cloud_init, vm=params.VM(initialization=initialization)) + vm.start(action=action) # Stop instance def vm_stop(conn, vmname): @@ -302,7 +371,6 @@ def vm_remove(conn, vmname): # Get the VMs status def vm_status(conn, vmname): status = conn.vms.get(name=vmname).status.state - print "vm status is : %s" % status return status @@ -311,10 +379,8 @@ def get_vm(conn, vmname): vm = conn.vms.get(name=vmname) if vm == None: name = "empty" - print "vmname: %s" % name else: name = vm.get_name() - print "vmname: %s" % name return name # ------------------------------------------------------------------- # @@ -347,6 +413,14 @@ def main(): disk_int = dict(default='virtio', choices=['virtio', 'ide']), instance_os = dict(aliases=['vmos']), instance_cores = dict(default=1, aliases=['vmcores']), + instance_hostname = dict(aliases=['hostname']), + instance_ip = dict(aliases=['ip']), + instance_netmask = dict(aliases=['netmask']), + instance_gateway = dict(aliases=['gateway']), + instance_domain = dict(aliases=['domain']), + instance_dns = dict(aliases=['dns']), + instance_rootpw = dict(aliases=['rootpw']), + instance_key = dict(aliases=['key']), sdomain = dict(), region = dict(), ) @@ -375,6 +449,14 @@ def main(): vmcores = module.params['instance_cores'] # number of cores sdomain = module.params['sdomain'] # storage domain to store disk on region = module.params['region'] # oVirt Datacenter + hostname = module.params['instance_hostname'] + ip = module.params['instance_ip'] + netmask = module.params['instance_netmask'] + gateway = module.params['instance_gateway'] + domain = module.params['instance_domain'] + dns = module.params['instance_dns'] + rootpw = module.params['instance_rootpw'] + key = module.params['instance_key'] #initialize connection try: c = conn(url+"/api", user, password) @@ -405,7 +487,8 @@ def main(): if vm_status(c, vmname) == 'up': module.exit_json(changed=False, msg="VM %s is already running" % vmname) else: - vm_start(c, vmname) + #vm_start(c, vmname) + vm_start(c, vmname, hostname, ip, netmask, gateway, domain, dns, rootpw, key) module.exit_json(changed=True, msg="VM %s started" % vmname) if state == 'shutdown':