blob: 9998692b54f1b54e4637cd624ae265d3a3e5caf3 [file] [log] [blame]
David K. Bainbridge6e23ac82016-12-07 12:55:41 -08001#!/usr/bin/python
2
3DOCUMENTATION = '''
4---
5module: maas_cluster_interface
6short_description: Manage MAAS Clusters Interfaces
7options:
8 maas:
9 description:
10 - URL of MAAS server
11 default: http://localhost/MAAS/api/1.0/
12 key:
13 description:
14 - MAAS API key
15 required: yes
16 cluster_name:
17 description:
18 - name of the cluster for the interface
19 required: yes
20 name:
21 description:
22 - name of the cluster interface
23 required: yes
24 management:
25 description:
26 - indicates how or if MAAS manages this interface
27 choices: ['unmanaged', 'dhcp', 'dhcpdns']
28 default: unmanaged
29 interface:
30 description:
31 - the physical NIC for the interface
32 required: no
33 ip:
34 description:
35 - IP address assigned for this interface
36 required: no
37 subnet_mask:
38 description:
39 - network subnet mask for this interface
40 required: no
41 broadcast_ip:
42 description:
43 - broadcast IP for this interfaece's network
44 required: no
45 router_ip:
46 description:
47 - gateway router IP for this interface's network
48 required: no
49 ip_range_low:
50 description:
51 - the low range for dynamic IP address assignement
52 required: no
53 ip_range_high:
54 description:
55 - the high range for dynamic IP address assignment
56 required: no
57 static_ip_range_low:
58 description:
59 - the low range for static IP address assignment
60 required: no
61 static_ip_range_high:
62 description:
63 - the high range for static IP address assignment
64 required: no
65 state:
66 description:
67 - possible states for this cluster interface
68 choices: ['present', 'absent', 'query']
69 default: present
70
71requirements: [ipaddress, requests_oauthlib, maasclient]
72author: David Bainbridge
73'''
74
75EXAMPLES = '''
76examples:
77 maas_cluster_interface:
78 maas: http://my.maas.server.com/MAAS/api/1.0/
79 key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
80 name: MyClusterInterface
81 interface: eth0
82 cluster_name: MyCluster
83 ip: 172.16.10.2
84 subnet_mask: 255.255.255.0
85 broadcast_ip: 172.16.10.255
86 router_ip: 172.16.10.1
87 ip_range_low: 172.16.10.3
88 ip_range_high: 172.16.10.127
89 static_ip_range_low: 172.16.10.128
90 static_ip_range_high: 172.16.10.253
91 management: dhcpdns
92 status: enabled
93 state: present
94
95 maas_cluster_interface:
96 maas: http://my.maas.server.com/MAAS/api/1.0/
97 key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
98 name: MyDeadClusterInterface
99 state: absent
100'''
101
102import sys
103import json
104import ipaddress
105import requests
106from maasclient.auth import MaasAuth
107from maasclient import MaasClient
108
109# For some reason the maasclient doesn't provide a put method. So
110# we will add it here
111def put(client, url, params=None):
112 return requests.put(url=client.auth.api_url + url,
113 auth=client._oauth(), data=params)
114
115# Attempt to interpret the given value as a JSON object, if that fails
116# just return it as a string
117def string_or_object(val):
118 try:
119 return json.loads(val)
120 except:
121 return val
122
123# Return a copy of the given dictionary with any `null` valued entries
124# removed
125def remove_null(d_in):
126 d = d_in.copy()
127 to_remove = []
128 for k in d.keys():
129 if d[k] == None:
130 to_remove.append(k)
131 for k in to_remove:
132 del d[k]
133 return d
134
135# Deterine if two dictionaries are different
136def different(have, want, debug):
137 have_keys = have.keys()
138 for key in want.keys():
139 if (key in have_keys and want[key] != have[key]) or key not in have_keys:
140 diff = {"diff": key, "want": want[key]}
141 if key in have_keys:
142 diff['have'] = have[key]
143 else:
144 diff['have'] = False
145 debug.append(diff)
146 return True
147 return False
148
149# Get an cluster from MAAS using its name, if not found return None
150def get_cluster(maas, name):
151 res = maas.get('/nodegroups/', dict(op='list'))
152 if res.ok:
153 for ng in json.loads(res.text):
154 if ng['cluster_name'] == name:
155 return ng
156 return None
157
158# Get an cluster interface from MAAS using its name, if not found return None
159def get_cluster_interface(maas, cluster, name):
160 res = maas.get('/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], name))
161 if res.ok:
162 return json.loads(res.text)
163 return None
164
165# Create an cluster interface based on the value given
166def create_cluster_interface(maas, cluster, cluster_interface):
167 merged = cluster_interface.copy()
168 merged['op'] = 'new'
169 res = maas.post('/nodegroups/%s/interfaces/' % cluster['uuid'], merged)
170 if res.ok:
171 return { 'error': False, 'status': get_cluster_interface(maas, cluster, merged['name']) }
172 return { 'error': True, 'status': string_or_object(res.text) }
173
174# Delete an cluster interface based on the name
175def delete_cluster_interface(maas, cluster, name):
176 res = maas.delete('/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], name))
177 if res.ok:
178 return { 'error': False }
179 return { 'error': True, 'status': string_or_object(res.text) }
180
181def update_cluster_interface(maas, cluster, have, want):
182 merged = have.copy()
183 merged.update(want)
184 res = put(maas, '/nodegroups/%s/interfaces/%s/' % (cluster['uuid'], merged['name']), merged)
185 if res.ok:
186 return { 'error': False, 'status': get_cluster_interface(maas, cluster, merged['name']) }
187 return { 'error': True, 'status': string_or_object(res.text) }
188
189def main():
190 module = AnsibleModule(
191 argument_spec = dict(
192 maas=dict(default='http://localhost/MAAS/api/1.0/'),
193 key=dict(required=True),
194 base=dict(required=False),
195 cluster_name=dict(required=True),
196 name=dict(required=True),
197 interface=dict(required=False),
198 ip=dict(required=False),
199 subnet_mask=dict(required=False),
200 management=dict(default='unmanaged', choices=['unmanaged', 'dhcp', 'dhcpdns']),
201 ip_range_low=dict(required=False),
202 ip_range_high=dict(required=False),
203 static_ip_range_low=dict(required=False),
204 static_ip_range_high=dict(required=False),
205 broadcast_ip=dict(required=False),
206 router_ip=dict(required=False),
207 state=dict(default='present', choices=['present', 'absent', 'query'])
208 ),
209 supports_check_mode = False
210 )
211
212 maas = module.params['maas']
213 key = module.params['key']
214 state = module.params['state']
215
216 management_map = {
217 'unmanaged': 0,
218 'dhcp': 1,
219 'dhcpdns': 2
220 }
221
222 # Construct a sparsely populate desired state
223 desired = remove_null({
224 'name': module.params['name'],
225 'interface': module.params['interface'],
226 'ip': module.params['ip'],
227 'subnet_mask': module.params['subnet_mask'],
228 'management': management_map[module.params['management']],
229 'ip_range_low': module.params['ip_range_low'],
230 'ip_range_high': module.params['ip_range_high'],
231 'static_ip_range_low': module.params['static_ip_range_low'],
232 'static_ip_range_high': module.params['static_ip_range_high'],
233 'broadcast_ip': module.params['broadcast_ip'],
234 'router_ip': module.params['router_ip'],
235 })
236
237 debug = []
238
239 # Authenticate into MAAS
240 auth = MaasAuth(maas, key)
241 maas = MaasClient(auth)
242
243 # Attempt to locate the cluster on which we will be working, error out if it can't be found
244 cluster = get_cluster(maas, module.params['cluster_name'])
245 if not cluster:
246 module.fail_json(msg='Unable to find specified cluster "%s", cannot continue' % module.params['cluster_name'])
247 return
248
249 debug.append({"desired": desired})
250
251 # Attempt to get the cluster interface from MAAS
252 cluster_interface = get_cluster_interface(maas, cluster, desired['name'])
253
254 debug.append({"found": cluster_interface})
255
256 # Actions if the cluster interface does not currently exist
257 if not cluster_interface:
258 if state == 'query':
259 # If this is a query, returne it is not found
260 module.exit_json(changed=False, found=False)
261 elif state == 'present':
262 # If this should be present, then attempt to create it
263 res = create_cluster_interface(maas, cluster, desired)
264 if res['error']:
265 module.fail_json(msg=res['status'])
266 else:
267 module.exit_json(changed=True, cluster_interface=res['status'], debug=debug)
268 else:
269 # If this should be absent, then we are done and in the desired state
270 module.exit_json(changed=False)
271
272 # Done with cluster interfaces does not exists actions
273 return
274
275 # Actions if the cluster interface does exist
276 if state == 'query':
277 # If this is a query, return the cluster interface
278 module.exit_json(changed=False, found=True, cluster_interface=cluster_interface)
279 elif state == 'present':
280 # If we want this to exists check to see if this is different and
281 # needs updated
282 if different(cluster_interface, desired, debug):
283 res = update_cluster_interface(maas, cluster, cluster_interface, desired)
284 if res['error']:
285 module.fail_json(msg=res['status'])
286 else:
287 module.exit_json(changed=True, cluster_interface=res['status'], debug=debug)
288 else:
289 # No differences, to nothing to change
290 module.exit_json(changed=False, cluster_interface=cluster_interface)
291 else:
292 # If we don't want this cluster interface, then delete it
293 res = delete_cluster_interface(maas, cluster, cluster_interface['name'])
294 if res['error']:
295 module.fail_json(msg=res['status'])
296 else:
297 module.exit_json(changed=True, cluster_interface=cluster_interface)
298
299# this is magic, see lib/ansible/module_common.py
300#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
301if __name__ == '__main__':
302 main()