blob: b84b99fe0dbf9d60aa3c532c3297cee2c4ac67fa [file] [log] [blame]
Ales Komarek663b85c2016-03-11 14:26:42 +01001# -*- coding: utf-8 -*-
2'''
3Module for handling maas calls.
4
5:optdepends: pyapi-maas Python adapter
6:configuration: This module is not usable until the following are specified
7 either in a pillar or in the minion's config file::
8
9 maas.url: 'https://maas.domain.com/'
10 maas.token: fdsfdsdsdsfa:fsdfae3fassd:fdsfdsfsafasdfsa
11
12'''
13
14from __future__ import absolute_import
15
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010016import io
Ales Komarek663b85c2016-03-11 14:26:42 +010017import logging
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010018import os.path
19import subprocess
20import urllib2
21import hashlib
Ales Komarek663b85c2016-03-11 14:26:42 +010022
smolaon27359ae2016-03-11 17:15:34 +010023import json
24
Ales Komarek663b85c2016-03-11 14:26:42 +010025LOG = logging.getLogger(__name__)
26
27# Import third party libs
28HAS_MASS = False
29try:
30 from apiclient.maas_client import MAASClient, MAASDispatcher, MAASOAuth
31 HAS_MASS = True
32except ImportError:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010033 LOG.exception('why??')
Ales Komarek663b85c2016-03-11 14:26:42 +010034
35def __virtual__():
36 '''
37 Only load this module if maas-client
38 is installed on this minion.
39 '''
40 if HAS_MASS:
41 return 'maas'
42 return False
43
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010044APIKEY_FILE = '/var/lib/maas/.maas_credentials'
45
46def _format_data(data):
47 class Lazy:
48 def __str__(self):
49 return ' '.join(['{0}={1}'.format(k, v)
50 for k, v in data.iteritems()])
51
52 return Lazy()
Ales Komarek663b85c2016-03-11 14:26:42 +010053
54
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010055def _create_maas_client():
56 global APIKEY_FILE
57 try:
58 api_token = file(APIKEY_FILE).read().strip().split(':')
59 except:
60 LOG.exception('token')
61 auth = MAASOAuth(*api_token)
62 api_url = 'http://localhost:5240/MAAS'
Ales Komarek663b85c2016-03-11 14:26:42 +010063 dispatcher = MAASDispatcher()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010064 return MAASClient(auth, dispatcher, api_url)
Ales Komarek663b85c2016-03-11 14:26:42 +010065
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010066class MaasObject(object):
67 def __init__(self):
68 self._maas = _create_maas_client()
69 self._extra_data_urls = {}
70 self._extra_data = {}
71 self._update = False
72 self._element_key = 'name'
73 self._update_key = 'id'
74
75 def send(self, data):
76 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
77 if self._update:
78 return self._maas.put(self._update_url.format(data[self._update_key]), **data).read()
79 return self._maas.post(self._create_url, None, **data).read()
80
81 def process(self):
82 config = __salt__['config.get']('maas')
83 for part in self._config_path.split('.'):
84 config = config.get(part, {})
85 extra = {}
86 for name, url_call in self._extra_data_urls.iteritems():
87 key = 'id'
88 if isinstance(url_call, tuple):
89 url_call, key = url_call[:]
90 extra[name] = {v['name']: v[key] for v in
91 json.loads(self._maas.get(url_call).read())}
92 elements = self._maas.get(self._all_elements_url).read()
93 all_elements = {v[self._element_key]: v for v in json.loads(elements)}
94 ret = {
95 'success': [],
96 'errors': {},
97 'updated': [],
98 }
99 for name, config_data in config.iteritems():
100 try:
101 data = self.fill_data(name, config_data, **extra)
102 if name in all_elements:
103 self._update = True
104 LOG.error('%s DATA %s', all_elements[name], data)
105 data = self.update(data, all_elements[name])
106 self.send(data)
107 ret['updated'].append(name)
108 else:
109 self.send(data)
110 ret['success'].append(name)
111 except urllib2.HTTPError as e:
112 etxt = e.read()
113 LOG.exception('Failed for object %s reason %s', name, etxt)
114 ret['errors'][name] = str(etxt)
115 except Exception as e:
116 LOG.exception('Failed for object %s reason %s', name, e)
117 ret['errors'][name] = str(e)
118 if ret['errors']:
119 raise Exception(ret)
120 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100121
122
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100123class Fabric(MaasObject):
124 def __init__(self):
125 super(Fabric, self).__init__()
126 self._all_elements_url = u'api/2.0/fabrics/'
127 self._create_url = u'api/2.0/fabrics/'
128 self._update_url = u'api/2.0/fabrics/{0}/'
129 self._config_path = 'region.fabrics'
130# self._update_keys = ['name', 'description', 'class_type', 'id']
Ales Komarek663b85c2016-03-11 14:26:42 +0100131
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100132 def fill_data(self, name, fabric):
133 data = {
134 'name': name,
135 'description': fabric.get('description', ''),
136 }
137 if 'class_type' in fabric:
138 data['class_type'] = fabric.get('class_type'),
139 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100140
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100141 def update(self, new, old):
142 new['id'] = str(old['id'])
143 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100144
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100145class Subnet(MaasObject):
146 def __init__(self):
147 super(Subnet, self).__init__()
148 self._all_elements_url = u'api/2.0/subnets/'
149 self._create_url = u'api/2.0/subnets/'
150 self._update_url = u'api/2.0/subnets/{0}/'
151 self._config_path = 'region.subnets'
152 self._extra_data_urls = {'fabrics':u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100153
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100154 def fill_data(self, name, subnet, fabrics):
155 data = {
156 'name': name,
157 'fabric': str(fabrics[subnet.get('fabric', '')]),
158 'cidr': subnet.get('cidr'),
159 'gateway_ip': subnet['gateway_ip'],
160 }
161 self._iprange = subnet['iprange']
162 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100163
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100164 def update(self, new, old):
165 new['id'] = str(old['id'])
166 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100167
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100168 def send(self, data):
169 response = super(Subnet, self).send(data)
170 res_json = json.loads(response)
171 self._process_iprange(res_json['id'])
172 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100173
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100174 def _process_iprange(self, subnet_id):
175 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
176 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
177 update = False
178 old_data = None
179 for iprange in ipranges:
180 if iprange['subnet']['id'] == subnet_id:
181 update = True
182 old_data = iprange
183 break
184 data = {
185 'start_ip': self._iprange.get('start'),
186 'end_ip': self._iprange.get('end'),
187 'subnet': str(subnet_id),
188 'type': self._iprange.get('type', 'dynamic')
189 }
190 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
191 LOG.info('iprange %s', _format_data(data))
192 if update:
193 LOG.warn('UPDATING %s %s', data, old_data)
194 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']), **data)
smolaonc3385f82016-03-11 19:01:24 +0100195 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100196 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100197
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100198class DHCPSnippet(MaasObject):
199 def __init__(self):
200 super(DHCPSnippet, self).__init__()
201 self._all_elements_url = u'api/2.0/dhcp-snippets/'
202 self._create_url = u'api/2.0/dhcp-snippets/'
203 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
204 self._config_path = 'region.dhcp_snippets'
205 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100206
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100207 def fill_data(self, name, snippet, subnets):
208 data = {
209 'name': name,
210 'value': snippet['value'],
211 'description': snippet['description'],
212 'enabled': str(snippet['enabled'] and 1 or 0),
213 'subnet': str(subnets[snippet['subnet']]),
214 }
215 return data
smolaonc3385f82016-03-11 19:01:24 +0100216
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100217 def update(self, new, old):
218 new['id'] = str(old['id'])
219 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100220
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100221class PacketRepository(MaasObject):
222 def __init__(self):
223 super(PacketRepository, self).__init__()
224 self._all_elements_url = u'api/2.0/package-repositories/'
225 self._create_url = u'api/2.0/package-repositories/'
226 self._update_url = u'api/2.0/package-repositories/{0}/'
227 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100228
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100229 def fill_data(self, name, package_repository):
230 data = {
231 'name': name,
232 'url': package_repository['url'],
233 'distributions': package_repository['distributions'],
234 'components': package_repository['components'],
235 'arches': package_repository['arches'],
236 'key': package_repository['key'],
237 'enabled': str(package_repository['enabled'] and 1 or 0),
238 }
239 if 'disabled_pockets' in package_repository:
240 data['disabled_pockets'] = package_repository['disable_pockets'],
241 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100242
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100243 def update(self, new, old):
244 new['id'] = str(old['id'])
245 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100246
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100247class Device(MaasObject):
248 def __init__(self):
249 super(Device, self).__init__()
250 self._all_elements_url = u'api/2.0/devices/'
251 self._create_url = u'api/2.0/devices/'
252 self._update_url = u'api/2.0/devices/{0}/'
253 self._config_path = 'region.devices'
254 self._element_key = 'hostname'
255 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100256
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100257 def fill_data(self, name, device_data):
258 data = {
259 'mac_addresses': device_data['mac'],
260 'hostname': name,
261 }
262 self._interface = device_data['interface']
263 return data
264
265 def update(self, new, old):
266 new_macs = set(new['mac_addresses'])
267 old_macs = set(v['mac_address'] for v in old[interface_set])
268 if new_macs - old_macs:
269 self._update = False
270 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
271 else:
272 new[self._update_key] = str(old[self._update_key])
273 return new
274
275 def send(self, data):
276 response = super(Device, self).send(data)
277 resp_json = json.loads(response)
278 system_id = resp_json['system_id']
279 iface_id = resp_json['interface_set'][0]['id']
280 self._link_interface(maas, system_id, iface_id)
281 return response
282
283 def _link_interface(self, system_id, interface_id):
284 data = {
285 'mode': self._interface.get('mode', 'STATIC'),
286 'subnet': self._interface.get('subnet'),
287 'ip_address': self._interface.get('ip_address'),
288 }
289 if 'default_gateway' in self._interface:
290 data['default_gateway'] = self._interface.get('default_gateway')
291 if self._update:
292 data['force'] = '1'
293 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
294 _format_data(data))
295 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
296 .format(system_id, interface_id), 'link_subnet',
297 **data)
298
299
300class Machine(MaasObject):
301 def __init__(self):
302 super(Machine, self).__init__()
303 self._all_elements_url = u'api/2.0/machines/'
304 self._create_url = u'api/2.0/machines/'
305 self._update_url = u'api/2.0/machines/{0}/'
306 self._config_path = 'region.machines'
307 self._element_key = 'hostname'
308 self._update_key = 'system_id'
309
310 def fill_data(self, name, machine_data):
311 main_interface = next(machine_data['interfaces'][0].iteritems())
312 interfaces = machine_data['interfaces'][1:]
313 power_data = machine_data['power_parameters']
314 data = {
315 'hostname': name,
316 'architecture': machine_data.get('architecture', 'amd64/generic'),
317 'mac_addresses': main_interface[1],
318 'power_type': machine_data.get('power_type', 'ipmi'),
319 'power_parameters_power_address': power_data['power_address'],
320 }
321 if 'power_user' in power_data:
322 data['power_parameters_power_user'] = power_data['power_user']
323 if 'power_password' in power_data:
324 data['power_parameters_power_pass'] = \
325 power_data['power_password']
326 return data
327
328 def update(self, new, old):
329 new_macs = set(new['mac_addresses'])
330 old_macs = set(v['mac_address'] for v in old[interface_set])
331 if new_macs - old_macs:
332 self._update = False
333 self._maas.delete(u'api/2.0/machiens/{0}/'.format(old['system_id']))
334 else:
335 new[self._update_key] = str(old[self._update_key])
336 return new
337
338class BootResource(MaasObject):
339 def __init__(self):
340 super(BootResource, self).__init__()
341 self._all_elements_url = u'api/2.0/boot-resources/'
342 self._create_url = u'api/2.0/boot-resources/'
343 self._update_url = u'api/2.0/boot-resources/{0}/'
344 self._config_path = 'region.boot_resources'
345
346 def fill_data(self, name, boot_data):
347 sha256 = hashlib.sha256()
348 sha256.update(file(boot_data['content']).read())
349 data = {
350 'name': name,
351 'title': boot_data['title'],
352 'architecture': boot_data['architecture'],
353 'filetype': boot_data['filetype'],
354 'size': str(os.path.getsize(boot_data['content'])),
355 'sha256': sha256.hexdigest(),
356 'content': io.open(boot_data['content']),
357 }
358 return data
359
360 def update(self, new, old):
361 self._update = False
362 return new
363
364def process_fabrics():
365 return Fabric().process()
366
367def process_subnets():
368 return Subnet().process()
369
370def process_dhcp_snippets():
371 return DHCPSnippet().process()
372
373def process_package_repositories():
374 return PacketRepository().process()
375
376def process_devices():
377 return Device().process()
378
379def process_machines():
380 return Machine().process()
381
382def process_boot_resources():
383 return BootResource().process()