blob: 0677fcda9dfba5101759df42d970f509a4612500 [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
109 LOG.error('%s DATA %s', all_elements[name], data)
110 data = self.update(data, all_elements[name])
111 self.send(data)
112 ret['updated'].append(name)
113 else:
114 self.send(data)
115 ret['success'].append(name)
116 except urllib2.HTTPError as e:
117 etxt = e.read()
118 LOG.exception('Failed for object %s reason %s', name, etxt)
119 ret['errors'][name] = str(etxt)
120 except Exception as e:
121 LOG.exception('Failed for object %s reason %s', name, e)
122 ret['errors'][name] = str(e)
123 if ret['errors']:
124 raise Exception(ret)
125 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100126
127
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100128class Fabric(MaasObject):
129 def __init__(self):
130 super(Fabric, self).__init__()
131 self._all_elements_url = u'api/2.0/fabrics/'
132 self._create_url = u'api/2.0/fabrics/'
133 self._update_url = u'api/2.0/fabrics/{0}/'
134 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100135
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100136 def fill_data(self, name, fabric):
137 data = {
138 'name': name,
139 'description': fabric.get('description', ''),
140 }
141 if 'class_type' in fabric:
142 data['class_type'] = fabric.get('class_type'),
143 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100144
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100145 def update(self, new, old):
146 new['id'] = str(old['id'])
147 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100148
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100149class Subnet(MaasObject):
150 def __init__(self):
151 super(Subnet, self).__init__()
152 self._all_elements_url = u'api/2.0/subnets/'
153 self._create_url = u'api/2.0/subnets/'
154 self._update_url = u'api/2.0/subnets/{0}/'
155 self._config_path = 'region.subnets'
156 self._extra_data_urls = {'fabrics':u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100157
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100158 def fill_data(self, name, subnet, fabrics):
159 data = {
160 'name': name,
161 'fabric': str(fabrics[subnet.get('fabric', '')]),
162 'cidr': subnet.get('cidr'),
163 'gateway_ip': subnet['gateway_ip'],
164 }
165 self._iprange = subnet['iprange']
166 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100167
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100168 def update(self, new, old):
169 new['id'] = str(old['id'])
170 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100171
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100172 def send(self, data):
173 response = super(Subnet, self).send(data)
174 res_json = json.loads(response)
175 self._process_iprange(res_json['id'])
176 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100177
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100178 def _process_iprange(self, subnet_id):
179 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
180 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
181 update = False
182 old_data = None
183 for iprange in ipranges:
184 if iprange['subnet']['id'] == subnet_id:
185 update = True
186 old_data = iprange
187 break
188 data = {
189 'start_ip': self._iprange.get('start'),
190 'end_ip': self._iprange.get('end'),
191 'subnet': str(subnet_id),
192 'type': self._iprange.get('type', 'dynamic')
193 }
194 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
195 LOG.info('iprange %s', _format_data(data))
196 if update:
197 LOG.warn('UPDATING %s %s', data, old_data)
198 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']), **data)
smolaonc3385f82016-03-11 19:01:24 +0100199 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100200 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100201
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100202class DHCPSnippet(MaasObject):
203 def __init__(self):
204 super(DHCPSnippet, self).__init__()
205 self._all_elements_url = u'api/2.0/dhcp-snippets/'
206 self._create_url = u'api/2.0/dhcp-snippets/'
207 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
208 self._config_path = 'region.dhcp_snippets'
209 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100210
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100211 def fill_data(self, name, snippet, subnets):
212 data = {
213 'name': name,
214 'value': snippet['value'],
215 'description': snippet['description'],
216 'enabled': str(snippet['enabled'] and 1 or 0),
217 'subnet': str(subnets[snippet['subnet']]),
218 }
219 return data
smolaonc3385f82016-03-11 19:01:24 +0100220
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100221 def update(self, new, old):
222 new['id'] = str(old['id'])
223 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100224
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100225class PacketRepository(MaasObject):
226 def __init__(self):
227 super(PacketRepository, self).__init__()
228 self._all_elements_url = u'api/2.0/package-repositories/'
229 self._create_url = u'api/2.0/package-repositories/'
230 self._update_url = u'api/2.0/package-repositories/{0}/'
231 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100232
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100233 def fill_data(self, name, package_repository):
234 data = {
235 'name': name,
236 'url': package_repository['url'],
237 'distributions': package_repository['distributions'],
238 'components': package_repository['components'],
239 'arches': package_repository['arches'],
240 'key': package_repository['key'],
241 'enabled': str(package_repository['enabled'] and 1 or 0),
242 }
243 if 'disabled_pockets' in package_repository:
244 data['disabled_pockets'] = package_repository['disable_pockets'],
245 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100246
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100247 def update(self, new, old):
248 new['id'] = str(old['id'])
249 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100250
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100251class Device(MaasObject):
252 def __init__(self):
253 super(Device, self).__init__()
254 self._all_elements_url = u'api/2.0/devices/'
255 self._create_url = u'api/2.0/devices/'
256 self._update_url = u'api/2.0/devices/{0}/'
257 self._config_path = 'region.devices'
258 self._element_key = 'hostname'
259 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100260
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100261 def fill_data(self, name, device_data):
262 data = {
263 'mac_addresses': device_data['mac'],
264 'hostname': name,
265 }
266 self._interface = device_data['interface']
267 return data
268
269 def update(self, new, old):
270 new_macs = set(new['mac_addresses'])
271 old_macs = set(v['mac_address'] for v in old[interface_set])
272 if new_macs - old_macs:
273 self._update = False
274 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
275 else:
276 new[self._update_key] = str(old[self._update_key])
277 return new
278
279 def send(self, data):
280 response = super(Device, self).send(data)
281 resp_json = json.loads(response)
282 system_id = resp_json['system_id']
283 iface_id = resp_json['interface_set'][0]['id']
284 self._link_interface(maas, system_id, iface_id)
285 return response
286
287 def _link_interface(self, system_id, interface_id):
288 data = {
289 'mode': self._interface.get('mode', 'STATIC'),
290 'subnet': self._interface.get('subnet'),
291 'ip_address': self._interface.get('ip_address'),
292 }
293 if 'default_gateway' in self._interface:
294 data['default_gateway'] = self._interface.get('default_gateway')
295 if self._update:
296 data['force'] = '1'
297 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
298 _format_data(data))
299 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
300 .format(system_id, interface_id), 'link_subnet',
301 **data)
302
303
304class Machine(MaasObject):
305 def __init__(self):
306 super(Machine, self).__init__()
307 self._all_elements_url = u'api/2.0/machines/'
308 self._create_url = u'api/2.0/machines/'
309 self._update_url = u'api/2.0/machines/{0}/'
310 self._config_path = 'region.machines'
311 self._element_key = 'hostname'
312 self._update_key = 'system_id'
313
314 def fill_data(self, name, machine_data):
315 main_interface = next(machine_data['interfaces'][0].iteritems())
316 interfaces = machine_data['interfaces'][1:]
317 power_data = machine_data['power_parameters']
318 data = {
319 'hostname': name,
320 'architecture': machine_data.get('architecture', 'amd64/generic'),
321 'mac_addresses': main_interface[1],
322 'power_type': machine_data.get('power_type', 'ipmi'),
323 'power_parameters_power_address': power_data['power_address'],
324 }
325 if 'power_user' in power_data:
326 data['power_parameters_power_user'] = power_data['power_user']
327 if 'power_password' in power_data:
328 data['power_parameters_power_pass'] = \
329 power_data['power_password']
330 return data
331
332 def update(self, new, old):
333 new_macs = set(new['mac_addresses'])
334 old_macs = set(v['mac_address'] for v in old[interface_set])
335 if new_macs - old_macs:
336 self._update = False
337 self._maas.delete(u'api/2.0/machiens/{0}/'.format(old['system_id']))
338 else:
339 new[self._update_key] = str(old[self._update_key])
340 return new
341
342class BootResource(MaasObject):
343 def __init__(self):
344 super(BootResource, self).__init__()
345 self._all_elements_url = u'api/2.0/boot-resources/'
346 self._create_url = u'api/2.0/boot-resources/'
347 self._update_url = u'api/2.0/boot-resources/{0}/'
348 self._config_path = 'region.boot_resources'
349
350 def fill_data(self, name, boot_data):
351 sha256 = hashlib.sha256()
352 sha256.update(file(boot_data['content']).read())
353 data = {
354 'name': name,
355 'title': boot_data['title'],
356 'architecture': boot_data['architecture'],
357 'filetype': boot_data['filetype'],
358 'size': str(os.path.getsize(boot_data['content'])),
359 'sha256': sha256.hexdigest(),
360 'content': io.open(boot_data['content']),
361 }
362 return data
363
364 def update(self, new, old):
365 self._update = False
366 return new
367
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100368class CommissioningScripts(MaasObject):
369 def __init__(self):
370 super(CommissioningScripts, self).__init__()
371 self._all_elements_url = u'api/2.0/commissioning-scripts/'
372 self._create_url = u'api/2.0/commissioning-scripts/'
373 self._update_url = u'api/2.0/commissioning-scripts/{0}/'
374 self._config_path = 'region.commissioning_scripts'
375 self._update_key = 'name'
376
377 def fill_data(self, name, file_path):
378 data = {
379 'name': name,
380 'content': io.open(file_path),
381 }
382 return data
383
384 def update(self, new, old):
385 return new
386
387class MaasConfig(MaasObject):
388 def __init__(self):
389 super(MaasConfig, self).__init__()
390 self._all_elements_url = None
391 self._create_url = (u'api/2.0/maas/', u'set_config')
392 self._config_path = 'region.maas_config'
393
394 def fill_data(self, name, value):
395 data = {
396 'name': name,
397 'value': value,
398 }
399 return data
400
401 def update(self, new, old):
402 self._update = False
403 return new
404
405
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100406def process_fabrics():
407 return Fabric().process()
408
409def process_subnets():
410 return Subnet().process()
411
412def process_dhcp_snippets():
413 return DHCPSnippet().process()
414
415def process_package_repositories():
416 return PacketRepository().process()
417
418def process_devices():
419 return Device().process()
420
421def process_machines():
422 return Machine().process()
423
424def process_boot_resources():
425 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100426
427def process_maas_config():
428 return MaasConfig().process()
429
430def process_commissioning_scripts():
431 return CommissioningScripts().process()