1#!/usr/bin/python 2# Copyright: Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import absolute_import, division, print_function 6__metaclass__ = type 7 8 9ANSIBLE_METADATA = {'metadata_version': '1.1', 10 'status': ['preview'], 11 'supported_by': 'community'} 12 13 14DOCUMENTATION = ''' 15--- 16module: rax_clb_nodes 17short_description: add, modify and remove nodes from a Rackspace Cloud Load Balancer 18description: 19 - Adds, modifies and removes nodes from a Rackspace Cloud Load Balancer 20version_added: "1.4" 21options: 22 address: 23 required: false 24 description: 25 - IP address or domain name of the node 26 condition: 27 required: false 28 choices: 29 - enabled 30 - disabled 31 - draining 32 description: 33 - Condition for the node, which determines its role within the load 34 balancer 35 load_balancer_id: 36 required: true 37 description: 38 - Load balancer id 39 node_id: 40 required: false 41 description: 42 - Node id 43 port: 44 required: false 45 description: 46 - Port number of the load balanced service on the node 47 state: 48 required: false 49 default: "present" 50 choices: 51 - present 52 - absent 53 description: 54 - Indicate desired state of the node 55 type: 56 required: false 57 choices: 58 - primary 59 - secondary 60 description: 61 - Type of node 62 wait: 63 required: false 64 default: "no" 65 type: bool 66 description: 67 - Wait for the load balancer to become active before returning 68 wait_timeout: 69 required: false 70 default: 30 71 description: 72 - How long to wait before giving up and returning an error 73 weight: 74 required: false 75 description: 76 - Weight of node 77author: "Lukasz Kawczynski (@neuroid)" 78extends_documentation_fragment: 79 - rackspace 80 - rackspace.openstack 81''' 82 83EXAMPLES = ''' 84# Add a new node to the load balancer 85- local_action: 86 module: rax_clb_nodes 87 load_balancer_id: 71 88 address: 10.2.2.3 89 port: 80 90 condition: enabled 91 type: primary 92 wait: yes 93 credentials: /path/to/credentials 94 95# Drain connections from a node 96- local_action: 97 module: rax_clb_nodes 98 load_balancer_id: 71 99 node_id: 410 100 condition: draining 101 wait: yes 102 credentials: /path/to/credentials 103 104# Remove a node from the load balancer 105- local_action: 106 module: rax_clb_nodes 107 load_balancer_id: 71 108 node_id: 410 109 state: absent 110 wait: yes 111 credentials: /path/to/credentials 112''' 113 114import os 115 116try: 117 import pyrax 118 HAS_PYRAX = True 119except ImportError: 120 HAS_PYRAX = False 121 122from ansible.module_utils.basic import AnsibleModule 123from ansible.module_utils.rax import rax_argument_spec, rax_clb_node_to_dict, rax_required_together, setup_rax_module 124 125 126def _activate_virtualenv(path): 127 activate_this = os.path.join(path, 'bin', 'activate_this.py') 128 with open(activate_this) as f: 129 code = compile(f.read(), activate_this, 'exec') 130 exec(code) 131 132 133def _get_node(lb, node_id=None, address=None, port=None): 134 """Return a matching node""" 135 for node in getattr(lb, 'nodes', []): 136 match_list = [] 137 if node_id is not None: 138 match_list.append(getattr(node, 'id', None) == node_id) 139 if address is not None: 140 match_list.append(getattr(node, 'address', None) == address) 141 if port is not None: 142 match_list.append(getattr(node, 'port', None) == port) 143 144 if match_list and all(match_list): 145 return node 146 147 return None 148 149 150def main(): 151 argument_spec = rax_argument_spec() 152 argument_spec.update( 153 dict( 154 address=dict(), 155 condition=dict(choices=['enabled', 'disabled', 'draining']), 156 load_balancer_id=dict(required=True, type='int'), 157 node_id=dict(type='int'), 158 port=dict(type='int'), 159 state=dict(default='present', choices=['present', 'absent']), 160 type=dict(choices=['primary', 'secondary']), 161 virtualenv=dict(type='path'), 162 wait=dict(default=False, type='bool'), 163 wait_timeout=dict(default=30, type='int'), 164 weight=dict(type='int'), 165 ) 166 ) 167 168 module = AnsibleModule( 169 argument_spec=argument_spec, 170 required_together=rax_required_together(), 171 ) 172 173 if not HAS_PYRAX: 174 module.fail_json(msg='pyrax is required for this module') 175 176 address = module.params['address'] 177 condition = (module.params['condition'] and 178 module.params['condition'].upper()) 179 load_balancer_id = module.params['load_balancer_id'] 180 node_id = module.params['node_id'] 181 port = module.params['port'] 182 state = module.params['state'] 183 typ = module.params['type'] and module.params['type'].upper() 184 virtualenv = module.params['virtualenv'] 185 wait = module.params['wait'] 186 wait_timeout = module.params['wait_timeout'] or 1 187 weight = module.params['weight'] 188 189 if virtualenv: 190 try: 191 _activate_virtualenv(virtualenv) 192 except IOError as e: 193 module.fail_json(msg='Failed to activate virtualenv %s (%s)' % ( 194 virtualenv, e)) 195 196 setup_rax_module(module, pyrax) 197 198 if not pyrax.cloud_loadbalancers: 199 module.fail_json(msg='Failed to instantiate client. This ' 200 'typically indicates an invalid region or an ' 201 'incorrectly capitalized region name.') 202 203 try: 204 lb = pyrax.cloud_loadbalancers.get(load_balancer_id) 205 except pyrax.exc.PyraxException as e: 206 module.fail_json(msg='%s' % e.message) 207 208 node = _get_node(lb, node_id, address, port) 209 210 result = rax_clb_node_to_dict(node) 211 212 if state == 'absent': 213 if not node: # Removing a non-existent node 214 module.exit_json(changed=False, state=state) 215 try: 216 lb.delete_node(node) 217 result = {} 218 except pyrax.exc.NotFound: 219 module.exit_json(changed=False, state=state) 220 except pyrax.exc.PyraxException as e: 221 module.fail_json(msg='%s' % e.message) 222 else: # present 223 if not node: 224 if node_id: # Updating a non-existent node 225 msg = 'Node %d not found' % node_id 226 if lb.nodes: 227 msg += (' (available nodes: %s)' % 228 ', '.join([str(x.id) for x in lb.nodes])) 229 module.fail_json(msg=msg) 230 else: # Creating a new node 231 try: 232 node = pyrax.cloudloadbalancers.Node( 233 address=address, port=port, condition=condition, 234 weight=weight, type=typ) 235 resp, body = lb.add_nodes([node]) 236 result.update(body['nodes'][0]) 237 except pyrax.exc.PyraxException as e: 238 module.fail_json(msg='%s' % e.message) 239 else: # Updating an existing node 240 mutable = { 241 'condition': condition, 242 'type': typ, 243 'weight': weight, 244 } 245 246 for name, value in mutable.items(): 247 if value is None or value == getattr(node, name): 248 mutable.pop(name) 249 250 if not mutable: 251 module.exit_json(changed=False, state=state, node=result) 252 253 try: 254 # The diff has to be set explicitly to update node's weight and 255 # type; this should probably be fixed in pyrax 256 lb.update_node(node, diff=mutable) 257 result.update(mutable) 258 except pyrax.exc.PyraxException as e: 259 module.fail_json(msg='%s' % e.message) 260 261 if wait: 262 pyrax.utils.wait_until(lb, "status", "ACTIVE", interval=1, 263 attempts=wait_timeout) 264 if lb.status != 'ACTIVE': 265 module.fail_json( 266 msg='Load balancer not active after %ds (current status: %s)' % 267 (wait_timeout, lb.status.lower())) 268 269 kwargs = {'node': result} if result else {} 270 module.exit_json(changed=True, state=state, **kwargs) 271 272 273if __name__ == '__main__': 274 main() 275