blob: e1cbf0ff277bba38b46ec2d71fd758efc6a414e5 [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:
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +010058 api_token = file(APIKEY_FILE).read().splitlines()[-1].strip().split(':')
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010059 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()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +010079 if isinstance(self._create_url, tuple):
80 return self._maas.post(*self._create_url, **data).read()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010081 return self._maas.post(self._create_url, None, **data).read()
82
83 def process(self):
84 config = __salt__['config.get']('maas')
85 for part in self._config_path.split('.'):
86 config = config.get(part, {})
87 extra = {}
88 for name, url_call in self._extra_data_urls.iteritems():
89 key = 'id'
90 if isinstance(url_call, tuple):
91 url_call, key = url_call[:]
92 extra[name] = {v['name']: v[key] for v in
93 json.loads(self._maas.get(url_call).read())}
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +010094 if self._all_elements_url:
95 elements = self._maas.get(self._all_elements_url).read()
96 all_elements = {v[self._element_key]: v for v in json.loads(elements)}
97 else:
98 all_elements = {}
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010099 ret = {
100 'success': [],
101 'errors': {},
102 'updated': [],
103 }
104 for name, config_data in config.iteritems():
105 try:
106 data = self.fill_data(name, config_data, **extra)
107 if name in all_elements:
108 self._update = True
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100109 data = self.update(data, all_elements[name])
110 self.send(data)
111 ret['updated'].append(name)
112 else:
113 self.send(data)
114 ret['success'].append(name)
115 except urllib2.HTTPError as e:
116 etxt = e.read()
117 LOG.exception('Failed for object %s reason %s', name, etxt)
118 ret['errors'][name] = str(etxt)
119 except Exception as e:
120 LOG.exception('Failed for object %s reason %s', name, e)
121 ret['errors'][name] = str(e)
122 if ret['errors']:
123 raise Exception(ret)
124 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100125
126
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100127class Fabric(MaasObject):
128 def __init__(self):
129 super(Fabric, self).__init__()
130 self._all_elements_url = u'api/2.0/fabrics/'
131 self._create_url = u'api/2.0/fabrics/'
132 self._update_url = u'api/2.0/fabrics/{0}/'
133 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100134
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100135 def fill_data(self, name, fabric):
136 data = {
137 'name': name,
138 'description': fabric.get('description', ''),
139 }
140 if 'class_type' in fabric:
141 data['class_type'] = fabric.get('class_type'),
142 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100143
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100144 def update(self, new, old):
145 new['id'] = str(old['id'])
146 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100147
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100148class Subnet(MaasObject):
149 def __init__(self):
150 super(Subnet, self).__init__()
151 self._all_elements_url = u'api/2.0/subnets/'
152 self._create_url = u'api/2.0/subnets/'
153 self._update_url = u'api/2.0/subnets/{0}/'
154 self._config_path = 'region.subnets'
155 self._extra_data_urls = {'fabrics':u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100156
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100157 def fill_data(self, name, subnet, fabrics):
158 data = {
159 'name': name,
160 'fabric': str(fabrics[subnet.get('fabric', '')]),
161 'cidr': subnet.get('cidr'),
162 'gateway_ip': subnet['gateway_ip'],
163 }
164 self._iprange = subnet['iprange']
165 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100166
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100167 def update(self, new, old):
168 new['id'] = str(old['id'])
169 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100170
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100171 def send(self, data):
172 response = super(Subnet, self).send(data)
173 res_json = json.loads(response)
174 self._process_iprange(res_json['id'])
175 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100176
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100177 def _process_iprange(self, subnet_id):
178 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
179 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
180 update = False
181 old_data = None
182 for iprange in ipranges:
183 if iprange['subnet']['id'] == subnet_id:
184 update = True
185 old_data = iprange
186 break
187 data = {
188 'start_ip': self._iprange.get('start'),
189 'end_ip': self._iprange.get('end'),
190 'subnet': str(subnet_id),
191 'type': self._iprange.get('type', 'dynamic')
192 }
193 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
194 LOG.info('iprange %s', _format_data(data))
195 if update:
196 LOG.warn('UPDATING %s %s', data, old_data)
197 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']), **data)
smolaonc3385f82016-03-11 19:01:24 +0100198 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100199 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100200
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100201class DHCPSnippet(MaasObject):
202 def __init__(self):
203 super(DHCPSnippet, self).__init__()
204 self._all_elements_url = u'api/2.0/dhcp-snippets/'
205 self._create_url = u'api/2.0/dhcp-snippets/'
206 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
207 self._config_path = 'region.dhcp_snippets'
208 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100209
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100210 def fill_data(self, name, snippet, subnets):
211 data = {
212 'name': name,
213 'value': snippet['value'],
214 'description': snippet['description'],
215 'enabled': str(snippet['enabled'] and 1 or 0),
216 'subnet': str(subnets[snippet['subnet']]),
217 }
218 return data
smolaonc3385f82016-03-11 19:01:24 +0100219
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100220 def update(self, new, old):
221 new['id'] = str(old['id'])
222 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100223
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100224class PacketRepository(MaasObject):
225 def __init__(self):
226 super(PacketRepository, self).__init__()
227 self._all_elements_url = u'api/2.0/package-repositories/'
228 self._create_url = u'api/2.0/package-repositories/'
229 self._update_url = u'api/2.0/package-repositories/{0}/'
230 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100231
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100232 def fill_data(self, name, package_repository):
233 data = {
234 'name': name,
235 'url': package_repository['url'],
236 'distributions': package_repository['distributions'],
237 'components': package_repository['components'],
238 'arches': package_repository['arches'],
239 'key': package_repository['key'],
240 'enabled': str(package_repository['enabled'] and 1 or 0),
241 }
242 if 'disabled_pockets' in package_repository:
243 data['disabled_pockets'] = package_repository['disable_pockets'],
244 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100245
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100246 def update(self, new, old):
247 new['id'] = str(old['id'])
248 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100249
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100250class Device(MaasObject):
251 def __init__(self):
252 super(Device, self).__init__()
253 self._all_elements_url = u'api/2.0/devices/'
254 self._create_url = u'api/2.0/devices/'
255 self._update_url = u'api/2.0/devices/{0}/'
256 self._config_path = 'region.devices'
257 self._element_key = 'hostname'
258 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100259
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100260 def fill_data(self, name, device_data):
261 data = {
262 'mac_addresses': device_data['mac'],
263 'hostname': name,
264 }
265 self._interface = device_data['interface']
266 return data
267
268 def update(self, new, old):
269 new_macs = set(new['mac_addresses'])
270 old_macs = set(v['mac_address'] for v in old[interface_set])
271 if new_macs - old_macs:
272 self._update = False
273 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
274 else:
275 new[self._update_key] = str(old[self._update_key])
276 return new
277
278 def send(self, data):
279 response = super(Device, self).send(data)
280 resp_json = json.loads(response)
281 system_id = resp_json['system_id']
282 iface_id = resp_json['interface_set'][0]['id']
283 self._link_interface(maas, system_id, iface_id)
284 return response
285
286 def _link_interface(self, system_id, interface_id):
287 data = {
288 'mode': self._interface.get('mode', 'STATIC'),
289 'subnet': self._interface.get('subnet'),
290 'ip_address': self._interface.get('ip_address'),
291 }
292 if 'default_gateway' in self._interface:
293 data['default_gateway'] = self._interface.get('default_gateway')
294 if self._update:
295 data['force'] = '1'
296 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
297 _format_data(data))
298 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
299 .format(system_id, interface_id), 'link_subnet',
300 **data)
301
302
303class Machine(MaasObject):
304 def __init__(self):
305 super(Machine, self).__init__()
306 self._all_elements_url = u'api/2.0/machines/'
307 self._create_url = u'api/2.0/machines/'
308 self._update_url = u'api/2.0/machines/{0}/'
309 self._config_path = 'region.machines'
310 self._element_key = 'hostname'
311 self._update_key = 'system_id'
312
313 def fill_data(self, name, machine_data):
Krzysztof Szukiełojć690d1a22017-03-17 14:21:57 +0100314 self._interface = machine_data['interface']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100315 power_data = machine_data['power_parameters']
316 data = {
317 'hostname': name,
318 'architecture': machine_data.get('architecture', 'amd64/generic'),
Krzysztof Szukiełojć690d1a22017-03-17 14:21:57 +0100319 'mac_addresses': self._interface['mac'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100320 'power_type': machine_data.get('power_type', 'ipmi'),
321 'power_parameters_power_address': power_data['power_address'],
322 }
323 if 'power_user' in power_data:
324 data['power_parameters_power_user'] = power_data['power_user']
325 if 'power_password' in power_data:
326 data['power_parameters_power_pass'] = \
327 power_data['power_password']
328 return data
329
330 def update(self, new, old):
331 new_macs = set(new['mac_addresses'])
332 old_macs = set(v['mac_address'] for v in old[interface_set])
333 if new_macs - old_macs:
334 self._update = False
335 self._maas.delete(u'api/2.0/machiens/{0}/'.format(old['system_id']))
336 else:
337 new[self._update_key] = str(old[self._update_key])
338 return new
339
Krzysztof Szukiełojć690d1a22017-03-17 14:21:57 +0100340 def _link_interface(self, system_id, interface_id):
341 if 'ip' not in self._interface:
342 return
343 data = {
344 'mode': 'STATIC',
345 'subnet': self._interface.get('subnet'),
346 'ip_address': self._interface.get('ip'),
347 }
348 if 'default_gateway' in self._interface:
349 data['default_gateway'] = self._interface.get('gateway')
350 if self._update:
351 data['force'] = '1'
352 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
353 _format_data(data))
354 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
355 .format(system_id, interface_id), 'link_subnet',
356 **data)
357
358 def send(self, data):
359 response = super(Device, self).send(data)
360 resp_json = json.loads(response)
361 system_id = resp_json['system_id']
362 iface_id = resp_json['interface_set'][0]['id']
363 self._link_interface(maas, system_id, iface_id)
364 return response
365
366
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100367class BootResource(MaasObject):
368 def __init__(self):
369 super(BootResource, self).__init__()
370 self._all_elements_url = u'api/2.0/boot-resources/'
371 self._create_url = u'api/2.0/boot-resources/'
372 self._update_url = u'api/2.0/boot-resources/{0}/'
373 self._config_path = 'region.boot_resources'
374
375 def fill_data(self, name, boot_data):
376 sha256 = hashlib.sha256()
377 sha256.update(file(boot_data['content']).read())
378 data = {
379 'name': name,
380 'title': boot_data['title'],
381 'architecture': boot_data['architecture'],
382 'filetype': boot_data['filetype'],
383 'size': str(os.path.getsize(boot_data['content'])),
384 'sha256': sha256.hexdigest(),
385 'content': io.open(boot_data['content']),
386 }
387 return data
388
389 def update(self, new, old):
390 self._update = False
391 return new
392
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100393class CommissioningScripts(MaasObject):
394 def __init__(self):
395 super(CommissioningScripts, self).__init__()
396 self._all_elements_url = u'api/2.0/commissioning-scripts/'
397 self._create_url = u'api/2.0/commissioning-scripts/'
398 self._update_url = u'api/2.0/commissioning-scripts/{0}/'
399 self._config_path = 'region.commissioning_scripts'
400 self._update_key = 'name'
401
402 def fill_data(self, name, file_path):
403 data = {
404 'name': name,
405 'content': io.open(file_path),
406 }
407 return data
408
409 def update(self, new, old):
410 return new
411
412class MaasConfig(MaasObject):
413 def __init__(self):
414 super(MaasConfig, self).__init__()
415 self._all_elements_url = None
416 self._create_url = (u'api/2.0/maas/', u'set_config')
417 self._config_path = 'region.maas_config'
418
419 def fill_data(self, name, value):
420 data = {
421 'name': name,
422 'value': value,
423 }
424 return data
425
426 def update(self, new, old):
427 self._update = False
428 return new
429
430
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100431def process_fabrics():
432 return Fabric().process()
433
434def process_subnets():
435 return Subnet().process()
436
437def process_dhcp_snippets():
438 return DHCPSnippet().process()
439
440def process_package_repositories():
441 return PacketRepository().process()
442
443def process_devices():
444 return Device().process()
445
446def process_machines():
447 return Machine().process()
448
449def process_boot_resources():
450 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100451
452def process_maas_config():
453 return MaasConfig().process()
454
455def process_commissioning_scripts():
456 return CommissioningScripts().process()