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