1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2017, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15
16DOCUMENTATION = '''
17---
18module: cloudscale_floating_ip
19short_description: Manages floating IPs on the cloudscale.ch IaaS service
20description:
21  - Create, assign and delete floating IPs on the cloudscale.ch IaaS service.
22notes:
23  - To create a new floating IP at least the C(ip_version) and C(server) options are required.
24  - Once a floating_ip is created all parameters except C(server) are read-only.
25  - It's not possible to request a floating IP without associating it with a server at the same time.
26  - This module requires the ipaddress python library. This library is included in Python since version 3.3. It is available as a
27    module on PyPI for earlier versions.
28version_added: "2.5"
29author: "Gaudenz Steinlin (@gaudenz)"
30options:
31  state:
32    description:
33      - State of the floating IP.
34    default: present
35    choices: [ present, absent ]
36    type: str
37  ip:
38    description:
39      - Floating IP address to change.
40      - Required to assign the IP to a different server or if I(state) is absent.
41    aliases: [ network ]
42    type: str
43  ip_version:
44    description:
45      - IP protocol version of the floating IP.
46    choices: [ 4, 6 ]
47    type: int
48  server:
49    description:
50      - UUID of the server assigned to this floating IP.
51      - Required unless I(state) is absent.
52    type: str
53  prefix_length:
54    description:
55      - Only valid if I(ip_version) is 6.
56      - Prefix length for the IPv6 network. Currently only a prefix of /56 can be requested. If no I(prefix_length) is present, a
57        single address is created.
58    choices: [ 56 ]
59    type: int
60  reverse_ptr:
61    description:
62      - Reverse PTR entry for this address.
63      - You cannot set a reverse PTR entry for IPv6 floating networks. Reverse PTR entries are only allowed for single addresses.
64    type: str
65extends_documentation_fragment: cloudscale
66'''
67
68EXAMPLES = '''
69# Request a new floating IP
70- name: Request a floating IP
71  cloudscale_floating_ip:
72    ip_version: 4
73    server: 47cec963-fcd2-482f-bdb6-24461b2d47b1
74    reverse_ptr: my-server.example.com
75    api_token: xxxxxx
76  register: floating_ip
77
78# Assign an existing floating IP to a different server
79- name: Move floating IP to backup server
80  cloudscale_floating_ip:
81    ip: 192.0.2.123
82    server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
83    api_token: xxxxxx
84
85# Request a new floating IPv6 network
86- name: Request a floating IP
87  cloudscale_floating_ip:
88    ip_version: 6
89    prefix_length: 56
90    server: 47cec963-fcd2-482f-bdb6-24461b2d47b1
91    api_token: xxxxxx
92  register: floating_ip
93
94# Assign an existing floating network to a different server
95- name: Move floating IP to backup server
96  cloudscale_floating_ip:
97    ip: '{{ floating_ip.network | ip }}'
98    server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
99    api_token: xxxxxx
100
101# Release a floating IP
102- name: Release floating IP
103  cloudscale_floating_ip:
104    ip: 192.0.2.123
105    state: absent
106    api_token: xxxxxx
107'''
108
109RETURN = '''
110href:
111  description: The API URL to get details about this floating IP.
112  returned: success when state == present
113  type: str
114  sample: https://api.cloudscale.ch/v1/floating-ips/2001:db8::cafe
115network:
116  description: The CIDR notation of the network that is routed to your server.
117  returned: success when state == present
118  type: str
119  sample: 2001:db8::cafe/128
120next_hop:
121  description: Your floating IP is routed to this IP address.
122  returned: success when state == present
123  type: str
124  sample: 2001:db8:dead:beef::42
125reverse_ptr:
126  description: The reverse pointer for this floating IP address.
127  returned: success when state == present
128  type: str
129  sample: 185-98-122-176.cust.cloudscale.ch
130server:
131  description: The floating IP is routed to this server.
132  returned: success when state == present
133  type: str
134  sample: 47cec963-fcd2-482f-bdb6-24461b2d47b1
135ip:
136  description: The floating IP address or network. This is always present and used to identify floating IPs after creation.
137  returned: success
138  type: str
139  sample: 185.98.122.176
140state:
141  description: The current status of the floating IP.
142  returned: success
143  type: str
144  sample: present
145'''
146
147import traceback
148
149IPADDRESS_IMP_ERR = None
150try:
151    from ipaddress import ip_network
152    HAS_IPADDRESS = True
153except ImportError:
154    IPADDRESS_IMP_ERR = traceback.format_exc()
155    HAS_IPADDRESS = False
156
157from ansible.module_utils.basic import AnsibleModule, env_fallback, missing_required_lib
158from ansible.module_utils.cloudscale import AnsibleCloudscaleBase, cloudscale_argument_spec
159
160
161class AnsibleCloudscaleFloatingIP(AnsibleCloudscaleBase):
162
163    def __init__(self, module):
164        super(AnsibleCloudscaleFloatingIP, self).__init__(module)
165
166        # Initialize info dict
167        # Set state to absent, will be updated by self.update_info()
168        self.info = {'state': 'absent'}
169
170        if self._module.params['ip']:
171            self.update_info()
172
173    @staticmethod
174    def _resp2info(resp):
175        # If the API response has some content, the floating IP must exist
176        resp['state'] = 'present'
177
178        # Add the IP address to the response, otherwise handling get's to complicated as this
179        # has to be converted from the network all the time.
180        resp['ip'] = str(ip_network(resp['network']).network_address)
181
182        # Replace the server with just the UUID, the href to the server is useless and just makes
183        # things more complicated
184        if resp['server'] is not None:
185            resp['server'] = resp['server']['uuid']
186
187        return resp
188
189    def update_info(self):
190        resp = self._get('floating-ips/' + self._module.params['ip'])
191        if resp:
192            self.info = self._resp2info(resp)
193        else:
194            self.info = {'ip': self._module.params['ip'],
195                         'state': 'absent'}
196
197    def request_floating_ip(self):
198        params = self._module.params
199
200        # check for required parameters to request a floating IP
201        missing_parameters = []
202        for p in ('ip_version', 'server'):
203            if p not in params or not params[p]:
204                missing_parameters.append(p)
205
206        if len(missing_parameters) > 0:
207            self._module.fail_json(msg='Missing required parameter(s) to request a floating IP: %s.' %
208                                   ' '.join(missing_parameters))
209
210        data = {'ip_version': params['ip_version'],
211                'server': params['server']}
212
213        if params['prefix_length']:
214            data['prefix_length'] = params['prefix_length']
215        if params['reverse_ptr']:
216            data['reverse_ptr'] = params['reverse_ptr']
217
218        self.info = self._resp2info(self._post('floating-ips', data))
219
220    def release_floating_ip(self):
221        self._delete('floating-ips/%s' % self._module.params['ip'])
222        self.info = {'ip': self.info['ip'], 'state': 'absent'}
223
224    def update_floating_ip(self):
225        params = self._module.params
226        if 'server' not in params or not params['server']:
227            self._module.fail_json(msg='Missing required parameter to update a floating IP: server.')
228        self.info = self._resp2info(self._post('floating-ips/%s' % params['ip'], {'server': params['server']}))
229
230
231def main():
232    argument_spec = cloudscale_argument_spec()
233    argument_spec.update(dict(
234        state=dict(default='present', choices=('present', 'absent'), type='str'),
235        ip=dict(aliases=('network', ), type='str'),
236        ip_version=dict(choices=(4, 6), type='int'),
237        server=dict(type='str'),
238        prefix_length=dict(choices=(56,), type='int'),
239        reverse_ptr=dict(type='str'),
240    ))
241
242    module = AnsibleModule(
243        argument_spec=argument_spec,
244        required_one_of=(('ip', 'ip_version'),),
245        supports_check_mode=True,
246    )
247
248    if not HAS_IPADDRESS:
249        module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMP_ERR)
250
251    target_state = module.params['state']
252    target_server = module.params['server']
253    floating_ip = AnsibleCloudscaleFloatingIP(module)
254    current_state = floating_ip.info['state']
255    current_server = floating_ip.info['server'] if 'server' in floating_ip.info else None
256
257    if module.check_mode:
258        module.exit_json(changed=not target_state == current_state or
259                         (current_state == 'present' and current_server != target_server),
260                         **floating_ip.info)
261
262    changed = False
263    if current_state == 'absent' and target_state == 'present':
264        floating_ip.request_floating_ip()
265        changed = True
266    elif current_state == 'present' and target_state == 'absent':
267        floating_ip.release_floating_ip()
268        changed = True
269    elif current_state == 'present' and current_server != target_server:
270        floating_ip.update_floating_ip()
271        changed = True
272
273    module.exit_json(changed=changed, **floating_ip.info)
274
275
276if __name__ == '__main__':
277    main()
278