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