Repository: ansible/ansible-modules-extras

Issue:
Add cloudstack loadbalancer modules
This is a continuation of #982 after I had to delete my fork



Discussion:
Thanks @DazWorrall. Pinging @resmo since he's the guy to approve this one, as he was in the previous PR.

Adding new_module boilerplate for housekeeping purposes also.

# 

Thanks for submitting this new module to Ansible Extras! This module is now in community review, a process that is open to all Ansible users. In order for this module to be approved, it must gain the following votes:

“works_for_me”: If you have tested the module thoroughly, including testing of all of the module’s options, and if the module works for you, please add “works_for_me” in the comments.

“passes_guidelines”: If you have gone through the module guidelines and the module meets all of the requirements, please add “passes_guidelines” in the comments. Guidelines are available here: http://docs.ansible.com/developing_modules.html#module-checklist

“needs_revision”: If the module fails to work for you, or if it doesn’t meet guidelines, please add “needs_revision” in the comments with details about what needs to be fixed.

When a module has both “works_for_me” and “passes_guidelines” tags, we will promote the module for inclusion in Ansible Extras.  At this point, you will be expected to maintain the module by fixing bugs and evaluating pull requests in a timely manner. 

Thanks again for submitting your Ansible module!

Just leaving a note here that I believe @resmo has been on a well-deserved vacation (or at least, what looks like a vacation, judging by the beach photos on twitter...) -- thanks @DazWorrall for your patience :)

@robynbergeron touché. But back at work now, would be great if it could make it into ansible v2 before release... working on it @DazWorrall 

I'm around this week, let me know if you want any more changes making. Fwiw, we're using these modules in production at the moment.

@resmo -- thanks. Let us know when you get a chance to eyeball it and we'll keep it moving :)

@DazWorrall made a PR to your repo with my final changes.

LGTM

works_for_me (see integration tests)
passes_guidelines

can you flatten the commits?


Resolving patch:
diff --git a/cloud/cloudstack/cs_loadbalancer_rule.py b/cloud/cloudstack/cs_loadbalancer_rule.py
new file mode 100644
--- /dev/null
+++ b/cloud/cloudstack/cs_loadbalancer_rule.py
@@ -0,0 +1,394 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# (c) 2015, Darren Worrall <darren@iweb.co.uk>
+# (c) 2015, René Moser <mail@renemoser.net>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+DOCUMENTATION = '''
+---
+module: cs_loadbalancer_rule
+short_description: Manages load balancer rules on Apache CloudStack based clouds.
+description:
+    - Add, update and remove load balancer rules.
+version_added: '2.0'
+author:
+    - "Darren Worrall (@dazworrall)"
+    - "René Moser (@resmo)"
+options:
+  name:
+    description:
+      - The name of the load balancer rule.
+    required: true
+  description:
+    description:
+      - The description of the load balancer rule.
+    required: false
+    default: null
+  algorithm:
+    description:
+      - Load balancer algorithm
+      - Required when using C(state=present).
+    required: false
+    choices: [ 'source', 'roundrobin', 'leastconn' ]
+    default: 'source'
+  protocol:
+    description:
+      - Protocol for the load balancer rule.
+    required: false
+    default: null
+  private_port:
+    description:
+      - The private port of the private ip address/virtual machine where the network traffic will be load balanced to.
+      - Required when using C(state=present).
+      - Can not be changed once the rule exists due API limitation.
+    required: false
+    default: null
+  public_port:
+    description:
+      - The public port from where the network traffic will be load balanced from.
+      - Required when using C(state=present).
+      - Can not be changed once the rule exists due API limitation.
+    required: true
+    default: null
+  ip_address:
+    description:
+      - Public IP address from where the network traffic will be load balanced from.
+    required: true
+    aliases: [ 'public_ip' ]
+  open_firewall:
+    description:
+      - Whether the firewall rule for public port should be created, while creating the new rule.
+      - Use M(cs_firewall) for managing firewall rules.
+    required: false
+    default: false
+  cidr:
+    description:
+    required: false
+    default: null
+  protocol:
+    description:
+      - The protocol to be used on the load balancer
+      - CIDR (full notation) to be used for firewall rule if required.
+    required: false
+    default: null
+  project:
+    description:
+      - Name of the project the load balancer IP address is related to.
+    required: false
+    default: null
+  state:
+    description:
+      - State of the rule.
+    required: true
+    default: 'present'
+    choices: [ 'present', 'absent' ]
+  domain:
+    description:
+      - Domain the rule is related to.
+    required: false
+    default: null
+  account:
+    description:
+      - Account the rule is related to.
+    required: false
+    default: null
+  zone:
+    description:
+      - Name of the zone in which the rule shoud be created.
+      - If not set, default zone is used.
+    required: false
+    default: null
+extends_documentation_fragment: cloudstack
+'''
+
+EXAMPLES = '''
+# Create a load balancer rule
+- local_action:
+    module: cs_loadbalancer_rule
+    name: balance_http
+    public_ip: 1.2.3.4
+    algorithm: leastconn
+    public_port: 80
+    private_port: 8080
+
+# update algorithm of an existing load balancer rule
+- local_action:
+    module: cs_loadbalancer_rule
+    name: balance_http
+    public_ip: 1.2.3.4
+    algorithm: roundrobin
+    public_port: 80
+    private_port: 8080
+
+# Delete a load balancer rule
+- local_action:
+    module: cs_loadbalancer_rule
+    name: balance_http
+    public_ip: 1.2.3.4
+    state: absent
+'''
+
+RETURN = '''
+---
+id:
+  description: UUID of the rule.
+  returned: success
+  type: string
+  sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
+zone:
+  description: Name of zone the rule is related to.
+  returned: success
+  type: string
+  sample: ch-gva-2
+project:
+  description: Name of project the rule is related to.
+  returned: success
+  type: string
+  sample: Production
+account:
+  description: Account the rule is related to.
+  returned: success
+  type: string
+  sample: example account
+domain:
+  description: Domain the rule is related to.
+  returned: success
+  type: string
+  sample: example domain
+algorithm:
+  description: Load balancer algorithm used.
+  returned: success
+  type: string
+  sample: "source"
+cidr:
+  description: CIDR to forward traffic from.
+  returned: success
+  type: string
+  sample: ""
+name:
+  description: Name of the rule.
+  returned: success
+  type: string
+  sample: "http-lb"
+description:
+  description: Description of the rule.
+  returned: success
+  type: string
+  sample: "http load balancer rule"
+protocol:
+  description: Protocol of the rule.
+  returned: success
+  type: string
+  sample: "tcp"
+public_port:
+  description: Public port.
+  returned: success
+  type: string
+  sample: 80
+private_port:
+  description: Private IP address.
+  returned: success
+  type: string
+  sample: 80
+public_ip:
+  description: Public IP address.
+  returned: success
+  type: string
+  sample: "1.2.3.4"
+tags:
+  description: List of resource tags associated with the rule.
+  returned: success
+  type: dict
+  sample: '[ { "key": "foo", "value": "bar" } ]'
+state:
+  description: State of the rule.
+  returned: success
+  type: string
+  sample: "Add"
+'''
+
+try:
+    from cs import CloudStack, CloudStackException, read_config
+    has_lib_cs = True
+except ImportError:
+    has_lib_cs = False
+
+# import cloudstack common
+from ansible.module_utils.cloudstack import *
+
+class AnsibleCloudStackLBRule(AnsibleCloudStack):
+
+    def __init__(self, module):
+        super(AnsibleCloudStackLBRule, self).__init__(module)
+        self.returns = {
+            'publicip': 'public_ip',
+            'algorithm': 'algorithm',
+            'cidrlist': 'cidr',
+            'protocol': 'protocol',
+        }
+        # these values will be casted to int
+        self.returns_to_int = {
+            'publicport': 'public_port',
+            'privateport': 'private_port',
+        }
+
+
+    def get_rule(self, **kwargs):
+        rules = self.cs.listLoadBalancerRules(**kwargs)
+        if rules:
+            return rules['loadbalancerrule'][0]
+
+
+    def _get_common_args(self):
+        return {
+            'account': self.get_account(key='name'),
+            'domainid': self.get_domain(key='id'),
+            'projectid': self.get_project(key='id'),
+            'zoneid': self.get_zone(key='id'),
+            'publicipid': self.get_ip_address(key='id'),
+            'name': self.module.params.get('name'),
+        }
+
+
+    def present_lb_rule(self):
+        missing_params = []
+        for required_params in [
+            'algorithm',
+            'private_port',
+            'public_port',
+        ]:
+            if not self.module.params.get(required_params):
+                missing_params.append(required_params)
+        if missing_params:
+            self.module.fail_json(msg="missing required arguments: %s" % ','.join(missing_params))
+
+        args = self._get_common_args()
+        rule = self.get_rule(**args)
+        if rule:
+            rule = self._update_lb_rule(rule)
+        else:
+            rule = self._create_lb_rule(rule)
+
+        if rule:
+            rule = self.ensure_tags(resource=rule, resource_type='LoadBalancer')
+        return rule
+
+
+    def _create_lb_rule(self, rule):
+        self.result['changed'] = True
+        if not self.module.check_mode:
+            args = self._get_common_args()
+            args['algorithm']   = self.module.params.get('algorithm')
+            args['privateport'] = self.module.params.get('private_port')
+            args['publicport']  = self.module.params.get('public_port')
+            args['cidrlist']    = self.module.params.get('cidr')
+            args['description'] = self.module.params.get('description')
+            args['protocol']    = self.module.params.get('protocol')
+            res = self.cs.createLoadBalancerRule(**args)
+            if 'errortext' in res:
+                self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
+
+            poll_async = self.module.params.get('poll_async')
+            if poll_async:
+                rule = self.poll_job(res, 'loadbalancer')
+        return rule
+
+
+    def _update_lb_rule(self, rule):
+        args                = {}
+        args['id']          = rule['id']
+        args['algorithm']   = self.module.params.get('algorithm')
+        args['description'] = self.module.params.get('description')
+        if self.has_changed(args, rule):
+            self.result['changed'] = True
+            if not self.module.check_mode:
+                res = self.cs.updateLoadBalancerRule(**args)
+                if 'errortext' in res:
+                    self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
+
+                poll_async = self.module.params.get('poll_async')
+                if poll_async:
+                    rule = self.poll_job(res, 'loadbalancer')
+        return rule
+
+
+    def absent_lb_rule(self):
+        args = self._get_common_args()
+        rule = self.get_rule(**args)
+        if rule:
+            self.result['changed'] = True
+        if rule and not self.module.check_mode:
+            res = self.cs.deleteLoadBalancerRule(id=rule['id'])
+            if 'errortext' in res:
+                self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
+            poll_async = self.module.params.get('poll_async')
+            if poll_async:
+                res = self._poll_job(res, 'loadbalancer')
+        return rule
+
+
+def main():
+    argument_spec = cs_argument_spec()
+    argument_spec.update(dict(
+        name = dict(required=True),
+        description = dict(default=None),
+        algorithm = dict(choices=['source', 'roundrobin', 'leastconn'], default='source'),
+        private_port = dict(type='int', default=None),
+        public_port = dict(type='int', default=None),
+        protocol = dict(default=None),
+        state = dict(choices=['present', 'absent'], default='present'),
+        ip_address = dict(required=True, aliases=['public_ip']),
+        cidr = dict(default=None),
+        project = dict(default=None),
+        open_firewall = dict(choices=BOOLEANS, default=False),
+        tags = dict(type='list', aliases=['tag'], default=None),
+        zone = dict(default=None),
+        domain = dict(default=None),
+        account = dict(default=None),
+        poll_async = dict(choices=BOOLEANS, default=True),
+    ))
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        required_together=cs_required_together(),
+        supports_check_mode=True
+    )
+
+    if not has_lib_cs:
+        module.fail_json(msg="python library cs required: pip install cs")
+
+    try:
+        acs_lb_rule = AnsibleCloudStackLBRule(module)
+
+        state = module.params.get('state')
+        if state in ['absent']:
+            rule = acs_lb_rule.absent_lb_rule()
+        else:
+            rule = acs_lb_rule.present_lb_rule()
+
+        result = acs_lb_rule.get_result(rule)
+
+    except CloudStackException, e:
+        module.fail_json(msg='CloudStackException: %s' % str(e))
+
+    module.exit_json(**result)
+
+# import module snippets
+from ansible.module_utils.basic import *
+if __name__ == '__main__':
+    main()
diff --git a/cloud/cloudstack/cs_loadbalancer_rule_member.py b/cloud/cloudstack/cs_loadbalancer_rule_member.py
new file mode 100644
--- /dev/null
+++ b/cloud/cloudstack/cs_loadbalancer_rule_member.py
@@ -0,0 +1,369 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# (c) 2015, Darren Worrall <darren@iweb.co.uk>
+# (c) 2015, René Moser <mail@renemoser.net>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+DOCUMENTATION = '''
+---
+module: cs_loadbalancer_rule_member
+short_description: Manages load balancer rule members on Apache CloudStack based clouds.
+description:
+    - Add and remove load balancer rule members.
+version_added: '2.0'
+author:
+    - "Darren Worrall (@dazworrall)"
+    - "René Moser (@resmo)"
+options:
+  name:
+    description:
+      - The name of the load balancer rule.
+    required: true
+  ip_address:
+    description:
+      - Public IP address from where the network traffic will be load balanced from.
+      - Only needed to find the rule if C(name) is not unique.
+    required: false
+    default: null
+    aliases: [ 'public_ip' ]
+  vms:
+    description:
+      - List of VMs to assign to or remove from the rule.
+    required: true
+    type: list
+    aliases: [ 'vm' ]
+  state:
+    description:
+      - Should the VMs be present or absent from the rule.
+    required: true
+    default: 'present'
+    choices: [ 'present', 'absent' ]
+  project:
+    description:
+      - Name of the project the firewall rule is related to.
+    required: false
+    default: null
+  domain:
+    description:
+      - Domain the rule is related to.
+    required: false
+    default: null
+  account:
+    description:
+      - Account the rule is related to.
+    required: false
+    default: null
+  zone:
+    description:
+      - Name of the zone in which the rule should be located.
+      - If not set, default zone is used.
+    required: false
+    default: null
+extends_documentation_fragment: cloudstack
+'''
+
+EXAMPLES = '''
+# Add VMs to an exising load balancer
+- local_action:
+    module: cs_loadbalancer_rule_member
+    name: balance_http
+    vms:
+      - web01
+      - web02
+
+# Remove a VM from an existing load balancer
+- local_action:
+    module: cs_loadbalancer_rule_member
+    name: balance_http
+    vms:
+      - web01
+      - web02
+    state: absent
+
+# Rolling upgrade of hosts
+- hosts: webservers
+  serial: 1
+  pre_tasks:
+    - name: Remove from load balancer
+      local_action:
+      module: cs_loadbalancer_rule_member
+      name: balance_http
+      vm: "{{ ansible_hostname }}"
+      state: absent
+  tasks:
+    # Perform update
+  post_tasks:
+    - name: Add to load balancer
+      local_action:
+      module: cs_loadbalancer_rule_member
+      name: balance_http
+      vm: "{{ ansible_hostname }}"
+      state: present
+'''
+
+RETURN = '''
+---
+id:
+  description: UUID of the rule.
+  returned: success
+  type: string
+  sample: a6f7a5fc-43f8-11e5-a151-feff819cdc9f
+zone:
+  description: Name of zone the rule is related to.
+  returned: success
+  type: string
+  sample: ch-gva-2
+project:
+  description: Name of project the rule is related to.
+  returned: success
+  type: string
+  sample: Production
+account:
+  description: Account the rule is related to.
+  returned: success
+  type: string
+  sample: example account
+domain:
+  description: Domain the rule is related to.
+  returned: success
+  type: string
+  sample: example domain
+algorithm:
+  description: Load balancer algorithm used.
+  returned: success
+  type: string
+  sample: "source"
+cidr:
+  description: CIDR to forward traffic from.
+  returned: success
+  type: string
+  sample: ""
+name:
+  description: Name of the rule.
+  returned: success
+  type: string
+  sample: "http-lb"
+description:
+  description: Description of the rule.
+  returned: success
+  type: string
+  sample: "http load balancer rule"
+protocol:
+  description: Protocol of the rule.
+  returned: success
+  type: string
+  sample: "tcp"
+public_port:
+  description: Public port.
+  returned: success
+  type: string
+  sample: 80
+private_port:
+  description: Private IP address.
+  returned: success
+  type: string
+  sample: 80
+public_ip:
+  description: Public IP address.
+  returned: success
+  type: string
+  sample: "1.2.3.4"
+vms:
+  description: Rule members.
+  returned: success
+  type: list
+  sample: '[ "web01", "web02" ]'
+tags:
+  description: List of resource tags associated with the rule.
+  returned: success
+  type: dict
+  sample: '[ { "key": "foo", "value": "bar" } ]'
+state:
+  description: State of the rule.
+  returned: success
+  type: string
+  sample: "Add"
+'''
+
+try:
+    from cs import CloudStack, CloudStackException, read_config
+    has_lib_cs = True
+except ImportError:
+    has_lib_cs = False
+
+# import cloudstack common
+from ansible.module_utils.cloudstack import *
+
+class AnsibleCloudStackLBRuleMember(AnsibleCloudStack):
+
+    def __init__(self, module):
+        super(AnsibleCloudStackLBRuleMember, self).__init__(module)
+        self.returns = {
+            'publicip': 'public_ip',
+            'algorithm': 'algorithm',
+            'cidrlist': 'cidr',
+            'protocol': 'protocol',
+        }
+        # these values will be casted to int
+        self.returns_to_int = {
+            'publicport': 'public_port',
+            'privateport': 'private_port',
+        }
+
+
+    def get_rule(self):
+        args               = self._get_common_args()
+        args['name']       = self.module.params.get('name')
+        args['zoneid']     = self.get_zone(key='id')
+        if self.module.params.get('ip_address'):
+            args['publicipid'] = self.get_ip_address(key='id')
+        rules = self.cs.listLoadBalancerRules(**args)
+        if rules:
+            if len(rules['loadbalancerrule']) > 1:
+                self.module.fail_json(msg="More than one rule having name %s. Please pass 'ip_address' as well." % args['name'])
+            return rules['loadbalancerrule'][0]
+        return None
+
+
+    def _get_common_args(self):
+        return {
+            'account': self.get_account(key='name'),
+            'domainid': self.get_domain(key='id'),
+            'projectid': self.get_project(key='id'),
+        }
+
+
+    def _get_members_of_rule(self, rule):
+        res = self.cs.listLoadBalancerRuleInstances(id=rule['id'])
+        if 'errortext' in res:
+            self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
+        return res.get('loadbalancerruleinstance', [])
+
+
+    def _ensure_members(self, operation):
+        if operation not in ['add', 'remove']:
+            self.module.fail_json(msg="Bad operation: %s" % operation)
+
+        rule = self.get_rule()
+        if not rule:
+            self.module.fail_json(msg="Unknown rule: %s" % self.module.params.get('name'))
+
+        existing = {}
+        for vm in self._get_members_of_rule(rule=rule):
+            existing[vm['name']] = vm['id']
+
+        wanted_names = self.module.params.get('vms')
+
+        if operation =='add':
+            cs_func = self.cs.assignToLoadBalancerRule
+            to_change = set(wanted_names) - set(existing.keys())
+        else:
+            cs_func = self.cs.removeFromLoadBalancerRule
+            to_change = set(wanted_names) & set(existing.keys())
+
+        if not to_change:
+            return rule
+
+        args = self._get_common_args()
+        vms = self.cs.listVirtualMachines(**args)
+        to_change_ids = []
+        for name in to_change:
+            for vm in vms.get('virtualmachine', []):
+                if vm['name'] == name:
+                    to_change_ids.append(vm['id'])
+                    break
+            else:
+                self.module.fail_json(msg="Unknown VM: %s" % name)
+
+        if to_change_ids:
+            self.result['changed'] = True
+
+        if to_change_ids and not self.module.check_mode:
+            res = cs_func(
+                id = rule['id'],
+                virtualmachineids = to_change_ids,
+            )
+            if 'errortext' in res:
+                self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
+            poll_async = self.module.params.get('poll_async')
+            if poll_async:
+                self.poll_job(res)
+                rule = self.get_rule()
+        return rule
+
+
+    def add_members(self):
+        return self._ensure_members('add')
+
+
+    def remove_members(self):
+        return self._ensure_members('remove')
+
+
+    def get_result(self, rule):
+        super(AnsibleCloudStackLBRuleMember, self).get_result(rule)
+        if rule:
+            self.result['vms'] = []
+            for vm in self._get_members_of_rule(rule=rule):
+                self.result['vms'].append(vm['name'])
+        return self.result
+
+
+def main():
+    argument_spec = cs_argument_spec()
+    argument_spec.update(dict(
+        name = dict(required=True),
+        ip_address = dict(default=None, aliases=['public_ip']),
+        vms = dict(required=True, aliases=['vm'], type='list'),
+        state = dict(choices=['present', 'absent'], default='present'),
+        zone = dict(default=None),
+        domain = dict(default=None),
+        project = dict(default=None),
+        account = dict(default=None),
+        poll_async = dict(choices=BOOLEANS, default=True),
+    ))
+
+    module = AnsibleModule(
+        argument_spec=argument_spec,
+        required_together=cs_required_together(),
+        supports_check_mode=True
+    )
+
+    if not has_lib_cs:
+        module.fail_json(msg="python library cs required: pip install cs")
+
+    try:
+        acs_lb_rule_member = AnsibleCloudStackLBRuleMember(module)
+
+        state = module.params.get('state')
+        if state in ['absent']:
+            rule = acs_lb_rule_member.remove_members()
+        else:
+            rule = acs_lb_rule_member.add_members()
+
+        result = acs_lb_rule_member.get_result(rule)
+
+    except CloudStackException, e:
+        module.fail_json(msg='CloudStackException: %s' % str(e))
+
+    module.exit_json(**result)
+
+# import module snippets
+from ansible.module_utils.basic import *
+if __name__ == '__main__':
+    main()


Test patch: