blob: 3c9b55e30a26472f8b2116500e57cec299d660cd [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ćb3216752017-04-05 15:50:41 +020018import collections
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010019import os.path
20import subprocess
21import urllib2
22import hashlib
Ales Komarek663b85c2016-03-11 14:26:42 +010023
smolaon27359ae2016-03-11 17:15:34 +010024import json
25
Ales Komarek663b85c2016-03-11 14:26:42 +010026LOG = logging.getLogger(__name__)
27
28# Import third party libs
29HAS_MASS = False
30try:
Damian Szelugad0ac0ac2017-03-29 15:15:33 +020031 from maas_client import MAASClient, MAASDispatcher, MAASOAuth
Ales Komarek663b85c2016-03-11 14:26:42 +010032 HAS_MASS = True
33except ImportError:
Damian Szelugaa4004212017-05-17 15:43:14 +020034 LOG.debug('Missing python-oauth module. Skipping')
35 pass
Ales Komarek663b85c2016-03-11 14:26:42 +010036
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020037
Ales Komarek663b85c2016-03-11 14:26:42 +010038def __virtual__():
39 '''
40 Only load this module if maas-client
41 is installed on this minion.
42 '''
43 if HAS_MASS:
44 return 'maas'
45 return False
46
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020047
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010048APIKEY_FILE = '/var/lib/maas/.maas_credentials'
49
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020050
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010051def _format_data(data):
52 class Lazy:
53 def __str__(self):
54 return ' '.join(['{0}={1}'.format(k, v)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020055 for k, v in data.iteritems()])
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +020056 return Lazy()
Ales Komarek663b85c2016-03-11 14:26:42 +010057
58
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010059def _create_maas_client():
60 global APIKEY_FILE
61 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020062 api_token = file(APIKEY_FILE).read().splitlines()[-1].strip()\
63 .split(':')
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010064 except:
65 LOG.exception('token')
66 auth = MAASOAuth(*api_token)
67 api_url = 'http://localhost:5240/MAAS'
Ales Komarek663b85c2016-03-11 14:26:42 +010068 dispatcher = MAASDispatcher()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010069 return MAASClient(auth, dispatcher, api_url)
Ales Komarek663b85c2016-03-11 14:26:42 +010070
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020071
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010072class MaasObject(object):
73 def __init__(self):
74 self._maas = _create_maas_client()
75 self._extra_data_urls = {}
76 self._extra_data = {}
77 self._update = False
78 self._element_key = 'name'
79 self._update_key = 'id'
80
81 def send(self, data):
82 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
83 if self._update:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020084 return self._maas.put(
85 self._update_url.format(data[self._update_key]), **data).read()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +010086 if isinstance(self._create_url, tuple):
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +020087 return self._maas.post(self._create_url[0].format(**data),
88 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć76d9a5c2017-04-14 12:00:42 +020089 return self._maas.post(self._create_url.format(**data),
90 None, **data).read()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010091
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +020092 def process(self, objects_name=None):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020093 ret = {
94 'success': [],
95 'errors': {},
96 'updated': [],
97 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +020098 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020099 config = __salt__['config.get']('maas')
100 for part in self._config_path.split('.'):
101 config = config.get(part, {})
102 extra = {}
103 for name, url_call in self._extra_data_urls.iteritems():
104 key = 'id'
105 key_name = 'name'
106 if isinstance(url_call, tuple):
107 if len(url_call) == 2:
108 url_call, key = url_call[:]
109 else:
110 url_call, key, key_name = url_call[:]
111 json_res = json.loads(self._maas.get(url_call).read())
112 if key:
113 extra[name] = {v[key_name]: v[key] for v in json_res}
114 else:
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +0200115 extra[name] = {v[key_name]: v for v in json_res}
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200116 if self._all_elements_url:
117 all_elements = {}
118 elements = self._maas.get(self._all_elements_url).read()
119 res_json = json.loads(elements)
120 for element in res_json:
121 if isinstance(element, (str, unicode)):
122 all_elements[element] = {}
123 else:
124 all_elements[element[self._element_key]] = element
125 else:
126 all_elements = {}
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200127
128 def process_single(name, config_data):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200129 self._update = False
130 try:
131 data = self.fill_data(name, config_data, **extra)
132 if data is None:
133 ret['updated'].append(name)
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200134 return
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200135 if name in all_elements:
136 self._update = True
137 data = self.update(data, all_elements[name])
138 self.send(data)
139 ret['updated'].append(name)
140 else:
141 self.send(data)
142 ret['success'].append(name)
143 except urllib2.HTTPError as e:
144 etxt = e.read()
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200145 LOG.error('Failed for object %s reason %s', name, etxt)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200146 ret['errors'][name] = str(etxt)
147 except Exception as e:
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200148 LOG.error('Failed for object %s reason %s', name, e)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200149 ret['errors'][name] = str(e)
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200150 if objects_name is not None:
151 if ',' in objects_name:
152 objects_name = objects_name.split(',')
153 else:
154 objects_name = [objects_name]
155 for object_name in objects_name:
156 process_single(object_name, config[object_name])
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200157 else:
158 for name, config_data in config.iteritems():
159 process_single(name, config_data)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200160 except Exception as e:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200161 LOG.exception('Error Global')
162 raise
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100163 if ret['errors']:
164 raise Exception(ret)
165 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100166
167
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100168class Fabric(MaasObject):
169 def __init__(self):
170 super(Fabric, self).__init__()
171 self._all_elements_url = u'api/2.0/fabrics/'
172 self._create_url = u'api/2.0/fabrics/'
173 self._update_url = u'api/2.0/fabrics/{0}/'
174 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100175
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100176 def fill_data(self, name, fabric):
177 data = {
178 'name': name,
179 'description': fabric.get('description', ''),
180 }
181 if 'class_type' in fabric:
182 data['class_type'] = fabric.get('class_type'),
183 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100184
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100185 def update(self, new, old):
186 new['id'] = str(old['id'])
187 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100188
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200189
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100190class Subnet(MaasObject):
191 def __init__(self):
192 super(Subnet, self).__init__()
193 self._all_elements_url = u'api/2.0/subnets/'
194 self._create_url = u'api/2.0/subnets/'
195 self._update_url = u'api/2.0/subnets/{0}/'
196 self._config_path = 'region.subnets'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200197 self._extra_data_urls = {'fabrics': u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100198
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100199 def fill_data(self, name, subnet, fabrics):
200 data = {
201 'name': name,
202 'fabric': str(fabrics[subnet.get('fabric', '')]),
203 'cidr': subnet.get('cidr'),
204 'gateway_ip': subnet['gateway_ip'],
205 }
206 self._iprange = subnet['iprange']
207 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100208
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100209 def update(self, new, old):
210 new['id'] = str(old['id'])
211 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100212
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100213 def send(self, data):
214 response = super(Subnet, self).send(data)
215 res_json = json.loads(response)
216 self._process_iprange(res_json['id'])
217 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100218
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100219 def _process_iprange(self, subnet_id):
220 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
221 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
222 update = False
223 old_data = None
224 for iprange in ipranges:
225 if iprange['subnet']['id'] == subnet_id:
226 update = True
227 old_data = iprange
228 break
229 data = {
230 'start_ip': self._iprange.get('start'),
231 'end_ip': self._iprange.get('end'),
232 'subnet': str(subnet_id),
233 'type': self._iprange.get('type', 'dynamic')
234 }
235 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
236 LOG.info('iprange %s', _format_data(data))
237 if update:
238 LOG.warn('UPDATING %s %s', data, old_data)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200239 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']),
240 **data)
smolaonc3385f82016-03-11 19:01:24 +0100241 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100242 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100243
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200244
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100245class DHCPSnippet(MaasObject):
246 def __init__(self):
247 super(DHCPSnippet, self).__init__()
248 self._all_elements_url = u'api/2.0/dhcp-snippets/'
249 self._create_url = u'api/2.0/dhcp-snippets/'
250 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
251 self._config_path = 'region.dhcp_snippets'
252 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100253
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100254 def fill_data(self, name, snippet, subnets):
255 data = {
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200256 'name': name,
257 'value': snippet['value'],
258 'description': snippet['description'],
259 'enabled': str(snippet['enabled'] and 1 or 0),
260 'subnet': str(subnets[snippet['subnet']]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100261 }
262 return data
smolaonc3385f82016-03-11 19:01:24 +0100263
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100264 def update(self, new, old):
265 new['id'] = str(old['id'])
266 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100267
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200268
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100269class PacketRepository(MaasObject):
270 def __init__(self):
271 super(PacketRepository, self).__init__()
272 self._all_elements_url = u'api/2.0/package-repositories/'
273 self._create_url = u'api/2.0/package-repositories/'
274 self._update_url = u'api/2.0/package-repositories/{0}/'
275 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100276
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100277 def fill_data(self, name, package_repository):
278 data = {
279 'name': name,
280 'url': package_repository['url'],
281 'distributions': package_repository['distributions'],
282 'components': package_repository['components'],
283 'arches': package_repository['arches'],
284 'key': package_repository['key'],
285 'enabled': str(package_repository['enabled'] and 1 or 0),
286 }
287 if 'disabled_pockets' in package_repository:
288 data['disabled_pockets'] = package_repository['disable_pockets'],
289 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100290
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100291 def update(self, new, old):
292 new['id'] = str(old['id'])
293 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100294
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200295
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100296class Device(MaasObject):
297 def __init__(self):
298 super(Device, self).__init__()
299 self._all_elements_url = u'api/2.0/devices/'
300 self._create_url = u'api/2.0/devices/'
301 self._update_url = u'api/2.0/devices/{0}/'
302 self._config_path = 'region.devices'
303 self._element_key = 'hostname'
304 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100305
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100306 def fill_data(self, name, device_data):
307 data = {
308 'mac_addresses': device_data['mac'],
309 'hostname': name,
310 }
311 self._interface = device_data['interface']
312 return data
313
314 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100315 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
316 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100317 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100318 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100319 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
320 else:
321 new[self._update_key] = str(old[self._update_key])
322 return new
323
324 def send(self, data):
325 response = super(Device, self).send(data)
326 resp_json = json.loads(response)
327 system_id = resp_json['system_id']
328 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100329 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100330 return response
331
332 def _link_interface(self, system_id, interface_id):
333 data = {
334 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100335 'subnet': self._interface['subnet'],
336 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100337 }
338 if 'default_gateway' in self._interface:
339 data['default_gateway'] = self._interface.get('default_gateway')
340 if self._update:
341 data['force'] = '1'
342 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200343 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100344 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
345 .format(system_id, interface_id), 'link_subnet',
346 **data)
347
348
349class Machine(MaasObject):
350 def __init__(self):
351 super(Machine, self).__init__()
352 self._all_elements_url = u'api/2.0/machines/'
353 self._create_url = u'api/2.0/machines/'
354 self._update_url = u'api/2.0/machines/{0}/'
355 self._config_path = 'region.machines'
356 self._element_key = 'hostname'
357 self._update_key = 'system_id'
358
359 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100360 power_data = machine_data['power_parameters']
361 data = {
362 'hostname': name,
363 'architecture': machine_data.get('architecture', 'amd64/generic'),
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200364 'mac_addresses': machine_data['interface']['mac'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100365 'power_type': machine_data.get('power_type', 'ipmi'),
366 'power_parameters_power_address': power_data['power_address'],
367 }
368 if 'power_user' in power_data:
369 data['power_parameters_power_user'] = power_data['power_user']
370 if 'power_password' in power_data:
371 data['power_parameters_power_pass'] = \
372 power_data['power_password']
373 return data
374
375 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100376 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
377 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100378 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100379 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200380 self._maas.delete(u'api/2.0/machines/{0}/'
381 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100382 else:
383 new[self._update_key] = str(old[self._update_key])
384 return new
385
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200386
387class AssignMachinesIP(MaasObject):
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200388 READY = 4
389
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200390 def __init__(self):
391 super(AssignMachinesIP, self).__init__()
392 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200393 self._create_url = \
394 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
395 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200396 self._config_path = 'region.machines'
397 self._element_key = 'hostname'
398 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200399 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
400 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200401
402 def fill_data(self, name, data, machines):
403 interface = data['interface']
404 machine = machines[name]
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200405 if machine['status'] != self.READY:
406 raise Exception('Not in ready state')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200407 if 'ip' not in interface:
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100408 return
409 data = {
410 'mode': 'STATIC',
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200411 'subnet': str(interface.get('subnet')),
412 'ip_address': str(interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100413 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200414 if 'default_gateway' in interface:
415 data['default_gateway'] = interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200416 data['force'] = '1'
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200417 data['system_id'] = str(machine['system_id'])
418 data['interface_id'] = str(machine['interface_set'][0]['id'])
419 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100420
421
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200422class DeployMachines(MaasObject):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200423 READY = 4
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200424
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200425 def __init__(self):
426 super(DeployMachines, self).__init__()
427 self._all_elements_url = None
428 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
429 self._config_path = 'region.machines'
430 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200431 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
432 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200433
434 def fill_data(self, name, machine_data, machines):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200435 machine = machines[name]
436 if machine['status'] != self.READY:
437 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200438 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200439 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200440 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200441 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200442 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200443 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200444 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200445 return data
446
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200447 def send(self, data):
448 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200449 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200450 return self._maas.post(self._create_url[0].format(**data),
451 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200452
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100453class BootResource(MaasObject):
454 def __init__(self):
455 super(BootResource, self).__init__()
456 self._all_elements_url = u'api/2.0/boot-resources/'
457 self._create_url = u'api/2.0/boot-resources/'
458 self._update_url = u'api/2.0/boot-resources/{0}/'
459 self._config_path = 'region.boot_resources'
460
461 def fill_data(self, name, boot_data):
462 sha256 = hashlib.sha256()
463 sha256.update(file(boot_data['content']).read())
464 data = {
465 'name': name,
466 'title': boot_data['title'],
467 'architecture': boot_data['architecture'],
468 'filetype': boot_data['filetype'],
469 'size': str(os.path.getsize(boot_data['content'])),
470 'sha256': sha256.hexdigest(),
471 'content': io.open(boot_data['content']),
472 }
473 return data
474
475 def update(self, new, old):
476 self._update = False
477 return new
478
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200479
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100480class CommissioningScripts(MaasObject):
481 def __init__(self):
482 super(CommissioningScripts, self).__init__()
483 self._all_elements_url = u'api/2.0/commissioning-scripts/'
484 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100485 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100486 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100487 self._update_key = 'name'
488
489 def fill_data(self, name, file_path):
490 data = {
491 'name': name,
492 'content': io.open(file_path),
493 }
494 return data
495
496 def update(self, new, old):
497 return new
498
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200499
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100500class MaasConfig(MaasObject):
501 def __init__(self):
502 super(MaasConfig, self).__init__()
503 self._all_elements_url = None
504 self._create_url = (u'api/2.0/maas/', u'set_config')
505 self._config_path = 'region.maas_config'
506
507 def fill_data(self, name, value):
508 data = {
509 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100510 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100511 }
512 return data
513
514 def update(self, new, old):
515 self._update = False
516 return new
517
518
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200519class SSHPrefs(MaasObject):
520 def __init__(self):
521 super(SSHPrefs, self).__init__()
522 self._all_elements_url = None
523 self._create_url = u'api/2.0/account/prefs/sshkeys/'
524 self._config_path = 'region.sshprefs'
525 self._element_key = 'hostname'
526 self._update_key = 'system_id'
527
528 def fill_data(self, value):
529 data = {
530 'key': value,
531 }
532 return data
533
534 def process(self):
535 config = __salt__['config.get']('maas')
536 for part in self._config_path.split('.'):
537 config = config.get(part, {})
538 extra = {}
539 for name, url_call in self._extra_data_urls.iteritems():
540 key = 'id'
541 if isinstance(url_call, tuple):
542 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200543 json_res = json.loads(self._maas.get(url_call).read())
544 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200545 if self._all_elements_url:
546 all_elements = {}
547 elements = self._maas.get(self._all_elements_url).read()
548 res_json = json.loads(elements)
549 for element in res_json:
550 if isinstance(element, (str, unicode)):
551 all_elements[element] = {}
552 else:
553 all_elements[element[self._element_key]] = element
554 else:
555 all_elements = {}
556 ret = {
557 'success': [],
558 'errors': {},
559 'updated': [],
560 }
561 for config_data in config:
562 name = config_data[:10]
563 try:
564 data = self.fill_data(config_data, **extra)
565 self.send(data)
566 ret['success'].append(name)
567 except urllib2.HTTPError as e:
568 etxt = e.read()
569 LOG.exception('Failed for object %s reason %s', name, etxt)
570 ret['errors'][name] = str(etxt)
571 except Exception as e:
572 LOG.exception('Failed for object %s reason %s', name, e)
573 ret['errors'][name] = str(e)
574 if ret['errors']:
575 raise Exception(ret)
576 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200577
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200578
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200579class Domain(MaasObject):
580 def __init__(self):
581 super(Domain, self).__init__()
582 self._all_elements_url = u'/api/2.0/domains/'
583 self._create_url = u'/api/2.0/domains/'
584 self._config_path = 'region.domain'
585 self._update_url = u'/api/2.0/domains/{0}/'
586
587 def fill_data(self, value):
588 data = {
589 'name': value,
590 }
591 self._update = True
592 return data
593
594 def update(self, new, old):
595 new['id'] = str(old['id'])
596 new['authoritative'] = str(old['authoritative'])
597 return new
598
599 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200600 ret = {
601 'success': [],
602 'errors': {},
603 'updated': [],
604 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200605 config = __salt__['config.get']('maas')
606 for part in self._config_path.split('.'):
607 config = config.get(part, {})
608 extra = {}
609 for name, url_call in self._extra_data_urls.iteritems():
610 key = 'id'
611 if isinstance(url_call, tuple):
612 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200613 json_res = json.loads(self._maas.get(url_call).read())
614 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200615 if self._all_elements_url:
616 all_elements = {}
617 elements = self._maas.get(self._all_elements_url).read()
618 res_json = json.loads(elements)
619 for element in res_json:
620 if isinstance(element, (str, unicode)):
621 all_elements[element] = {}
622 else:
623 all_elements[element[self._element_key]] = element
624 else:
625 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200626 try:
627 data = self.fill_data(config, **extra)
628 data = self.update(data, all_elements.values()[0])
629 self.send(data)
630 ret['success'].append('domain')
631 except urllib2.HTTPError as e:
632 etxt = e.read()
633 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
634 ret['errors']['domain'] = str(etxt)
635 except Exception as e:
636 LOG.exception('Failed for object %s reason %s', 'domain', e)
637 ret['errors']['domain'] = str(e)
638 if ret['errors']:
639 raise Exception(ret)
640 return ret
641
642
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200643class MachinesStatus(MaasObject):
644 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200645 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200646 cls._maas = _create_maas_client()
647 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200648 json_result = json.loads(result.read())
649 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200650 status_name_dict = dict([
651 (0, 'New'), (1, 'Commissioning'), (2, 'Failed commissioning'),
652 (3, 'Missing'), (4, 'Ready'), (5, 'Reserved'), (10, 'Allocated'),
653 (9, 'Deploying'), (6, 'Deployed'), (7, 'Retired'), (8, 'Broken'),
654 (11, 'Failed deployment'), (12, 'Releasing'),
655 (13, 'Releasing failed'), (14, 'Disk erasing'),
656 (15, 'Failed disk erasing')])
657 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200658 if objects_name:
659 if ',' in objects_name:
660 objects_name = set(objects_name.split(','))
661 else:
662 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200663 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200664 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200665 continue
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200666 status = status_name_dict[machine['status']]
667 summary[status] += 1
Krzysztof Szukiełojć02e10d32017-04-12 12:23:51 +0200668 res.append('hostname:{},system_id:{},status:{}'
669 .format(machine['hostname'], machine['system_id'],
670 status))
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200671 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200672
673
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100674def process_fabrics():
675 return Fabric().process()
676
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200677
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100678def process_subnets():
679 return Subnet().process()
680
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200681
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100682def process_dhcp_snippets():
683 return DHCPSnippet().process()
684
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200685
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100686def process_package_repositories():
687 return PacketRepository().process()
688
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200689
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +0200690def process_devices(*args):
691 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100692
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200693
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200694def process_machines(*args):
695 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100696
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200697
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200698def process_assign_machines_ip(*args):
699 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200700
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200701
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200702def machines_status(*args):
703 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200704
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200705
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200706def deploy_machines(*args):
707 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200708
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200709
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100710def process_boot_resources():
711 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100712
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200713
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100714def process_maas_config():
715 return MaasConfig().process()
716
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200717
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100718def process_commissioning_scripts():
719 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200720
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200721
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200722def process_domain():
723 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200724
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200725
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200726def process_sshprefs():
727 return SSHPrefs().process()