blob: e3354cdf55727c9480e50c8de67e2d394bc6b660 [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_subnet
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 name:
31 description:
32 - name of the subnet
33 required: yes
34 space:
35 description:
36 - network space of the subnet
37 dns_servers:
38 description:
39 - dns servers for the subnet
40 gateway_ip:
41 description:
42 - gateway IP for the subnet
43 cidr:
44 description:
45 - cidr for the subnet
46 state:
47 description:
48 - possible states for this subnet
49 choices: ['present', 'absent', 'query']
50 default: present
51
52requirements: [ipaddress, requests_oauthlib, maasclient]
53author: David Bainbridge
54'''
55
56EXAMPLES = '''
57examples:
58 maas_subnet:
59 maas: http://my.maas.server.com/MAAS/api/1.0/
60 key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
61 name: MySubnet
62 state: present
63
64 maas_subnet:
65 maas: http://my.maas.server.com/MAAS/api/1.0/
66 key: 'xBvr9dx5k7S52myufC:fqBXV7hJgXegNZDw9c:K8hsmL47XjAppfQy2pDVW7G49p6PELgp'
67 name: MyDeadSubnet
68 state: absent
69'''
70
71import sys
72import json
73import ipaddress
74import requests
75import string
76from maasclient.auth import MaasAuth
77from maasclient import MaasClient
78
79debug = []
80
81# For some reason the maasclient doesn't provide a put method. So
82# we will add it here
83def put(client, url, params=None):
84 return requests.put(url=client.auth.api_url + url,
85 auth=client._oauth(), data=params)
86
87# Attempt to interpret the given value as a JSON object, if that fails
88# just return it as a string
89def string_or_object(val):
90 try:
91 return json.loads(val)
92 except:
93 return val
94
95# Return a copy of the given dictionary with any `null` valued entries
96# removed
97def remove_null(d_in):
98 d = d_in.copy()
99 to_remove = []
100 for k in d.keys():
101 if d[k] == None:
102 to_remove.append(k)
103 for k in to_remove:
104 del d[k]
105 return d
106
107# Removes keys from a dictionary either using an include or
108# exclude filter This change happens on given dictionary is
109# modified.
110def filter(filter_type, d, keys):
111 if filter_type == 'include':
112 for k in d.keys():
113 if k not in keys:
114 d.pop(k, None)
115 else:
116 for k in d.keys():
117 if k in keys:
118 d.pop(k, None)
119
120# Converts a subnet structure with names for the vlan and space to their
121# ID equivalents that can be used in a REST call to MAAS
122def convert(maas, subnet):
123 copy = subnet.copy()
124 copy['space'] = get_space(maas, subnet['space'])['id']
125 fabric_name, vlan_name = string.split(subnet['vlan'], ':', 1)
126 fabric = get_fabric(maas, fabric_name)
127 copy['vlan'] = get_vlan(maas, fabric, vlan_name)['id']
128 return copy
129
130# replaces the expanded VLAN object with a unique identifier of
131# `fabric`:`name`
132def simplify(subnet):
133 copy = subnet.copy()
134 if 'dns_servers' in copy.keys() and type(copy['dns_servers']) == list:
135 copy['dns_servers'] = ",".join(copy['dns_servers'])
136 if subnet['vlan'] and type(subnet['vlan']) == dict:
137 copy['vlan'] = "%s:%s" % (subnet['vlan']['fabric'], subnet['vlan']['name'])
138 return copy
139
140# Deterine if two dictionaries are different
141def different(have, want):
142 have_keys = have.keys()
143 for key in want.keys():
144 if (key in have_keys and want[key] != have[key]) or key not in have_keys:
145 debug.append({"have": have, "want": want, "key": key})
146 return True
147 return False
148
149# Get a space object form MAAS based on its name
150def get_space(maas, name):
151 res = maas.get('/spaces/')
152 if res.ok:
153 for space in json.loads(res.text):
154 if space['name'] == name:
155 return space
156 return None
157
158# Get a fabric object from MAAS based on its name
159def get_fabric(maas, name):
160 res = maas.get('/fabrics/')
161 if res.ok:
162 for fabric in json.loads(res.text):
163 if fabric['name'] == name:
164 return fabric
165 return None
166
167# Get a VLAN object form MAAS based on its name
168def get_vlan(maas, fabric, name ):
169 res = maas.get('/fabrics/%d/vlans/' % fabric['id'])
170 if res.ok:
171 for vlan in json.loads(res.text):
172 if vlan['name'] == name:
173 return vlan
174 return None
175
176# Get an subnet from MAAS using its name, if not found return None
177def get_subnet(maas, name):
178 res = maas.get('/subnets/')
179 if res.ok:
180 for subnet in json.loads(res.text):
181 if subnet['name'] == name:
182 return simplify(subnet)
183 return None
184
185# Create an subnet based on the value given
186def create_subnet(maas, subnet):
187 merged = subnet.copy()
188 # merged['op'] = 'new'
189 res = maas.post('/subnets/', convert(maas, merged))
190 if res.ok:
191 return { 'error': False, 'status': get_subnet(maas, merged['name']) }
192 return { 'error': True, 'status': string_or_object(res.text) }
193
194# Delete an subnet based on the name
195def delete_subnet(maas, name):
196 res = maas.delete('/subnets/%s/' % name)
197 if res.ok:
198 return { 'error': False }
199 return { 'error': True, 'status': string_or_object(res.text) }
200
201def update_subnet(maas, have, want):
202 merged = have.copy()
203 merged.update(want)
204 res = put(maas, '/subnets/%s/' % merged['id'], convert(maas, merged))
205 if res.ok:
206 return { 'error': False, 'status': get_subnet(maas, merged['name']) }
207 return { 'error': True, 'status': string_or_object(res.text) }
208
209def main():
210 module = AnsibleModule(
211 argument_spec = dict(
212 maas=dict(default='http://localhost/MAAS/api/1.0/'),
213 key=dict(required=True),
214 name=dict(required=True),
215 space=dict(required=False),
216 dns_servers=dict(required=False),
217 gateway_ip=dict(required=False),
218 cidr=dict(required=False),
219 state=dict(default='present', choices=['present', 'absent', 'query'])
220 ),
221 supports_check_mode = False
222 )
223
224 maas = module.params['maas']
225 key = module.params['key']
226 state = module.params['state']
227
228 # Construct a sparsely populate desired state
229 desired = remove_null({
230 'name': module.params['name'],
231 'space': module.params['space'],
232 'dns_servers': module.params['dns_servers'],
233 'gateway_ip': module.params['gateway_ip'],
234 'cidr': module.params['cidr'],
235 })
236
237 # Authenticate into MAAS
238 auth = MaasAuth(maas, key)
239 maas = MaasClient(auth)
240
241 # Attempt to get the subnet from MAAS
242 subnet = get_subnet(maas, desired['name'])
243
244 # Actions if the subnet does not currently exist
245 if not subnet:
246 if state == 'query':
247 # If this is a query, returne it is not found
248 module.exit_json(changed=False, found=False)
249 elif state == 'present':
250 # If this should be present, then attempt to create it
251 res = create_subnet(maas, desired)
252 if res['error']:
253 module.fail_json(msg=res['status'])
254 else:
255 module.exit_json(changed=True, subnet=res['status'])
256 else:
257 # If this should be absent, then we are done and in the desired state
258 module.exit_json(changed=False)
259
260 # Done with subnets does not exists actions
261 return
262
263 # Actions if the subnet does exist
264 if state == 'query':
265 # If this is a query, return the subnet
266 module.exit_json(changed=False, found=True, subnet=subnet)
267 elif state == 'present':
268 # If we want this to exists check to see if this is different and
269 # needs updated
270 if different(subnet, desired):
271 res = update_subnet(maas, subnet, desired)
272 if res['error']:
273 module.fail_json(msg=res['status'])
274 else:
275 module.exit_json(changed=True, subnet=res['status'], debug=debug)
276 else:
277 # No differences, to nothing to change
278 module.exit_json(changed=False, subnet=subnet)
279 else:
280 # If we don't want this subnet, then delete it
281 res = delete_subnet(maas, subnet['name'])
282 if res['error']:
283 module.fail_json(msg=res['status'])
284 else:
285 module.exit_json(changed=True, subnet=subnet)
286
287# this is magic, see lib/ansible/module_common.py
288#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
289if __name__ == '__main__':
290 main()