blob: c5029a7f87611c8ea3ca2554c37e6e074e6596f0 [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ćb3216752017-04-05 15:50:41 +020016import collections
azvyagintsev7605a662017-11-03 19:05:04 +020017import copy
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010018import hashlib
azvyagintsev7605a662017-11-03 19:05:04 +020019import io
smolaon27359ae2016-03-11 17:15:34 +010020import json
azvyagintsev7605a662017-11-03 19:05:04 +020021import logging
22import os.path
23import time
24import urllib2
smolaon27359ae2016-03-11 17:15:34 +010025
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')
Ales Komarek663b85c2016-03-11 14:26:42 +010035
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020036
Ales Komarek663b85c2016-03-11 14:26:42 +010037def __virtual__():
38 '''
39 Only load this module if maas-client
40 is installed on this minion.
41 '''
42 if HAS_MASS:
43 return 'maas'
44 return False
45
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020046
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010047APIKEY_FILE = '/var/lib/maas/.maas_credentials'
48
azvyagintsev7605a662017-11-03 19:05:04 +020049STATUS_NAME_DICT = dict([
50 (0, 'New'), (1, 'Commissioning'), (2, 'Failed commissioning'),
51 (3, 'Missing'), (4, 'Ready'), (5, 'Reserved'), (10, 'Allocated'),
52 (9, 'Deploying'), (6, 'Deployed'), (7, 'Retired'), (8, 'Broken'),
53 (11, 'Failed deployment'), (12, 'Releasing'),
54 (13, 'Releasing failed'), (14, 'Disk erasing'),
55 (15, 'Failed disk erasing')])
56
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020057
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010058def _format_data(data):
59 class Lazy:
60 def __str__(self):
61 return ' '.join(['{0}={1}'.format(k, v)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020062 for k, v in data.iteritems()])
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +020063 return Lazy()
Ales Komarek663b85c2016-03-11 14:26:42 +010064
65
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010066def _create_maas_client():
67 global APIKEY_FILE
68 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020069 api_token = file(APIKEY_FILE).read().splitlines()[-1].strip()\
70 .split(':')
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010071 except:
72 LOG.exception('token')
73 auth = MAASOAuth(*api_token)
74 api_url = 'http://localhost:5240/MAAS'
Ales Komarek663b85c2016-03-11 14:26:42 +010075 dispatcher = MAASDispatcher()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010076 return MAASClient(auth, dispatcher, api_url)
Ales Komarek663b85c2016-03-11 14:26:42 +010077
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020078
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010079class MaasObject(object):
80 def __init__(self):
81 self._maas = _create_maas_client()
82 self._extra_data_urls = {}
83 self._extra_data = {}
84 self._update = False
85 self._element_key = 'name'
86 self._update_key = 'id'
87
88 def send(self, data):
89 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
90 if self._update:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020091 return self._maas.put(
92 self._update_url.format(data[self._update_key]), **data).read()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +010093 if isinstance(self._create_url, tuple):
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +020094 return self._maas.post(self._create_url[0].format(**data),
95 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć76d9a5c2017-04-14 12:00:42 +020096 return self._maas.post(self._create_url.format(**data),
97 None, **data).read()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010098
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +020099 def process(self, objects_name=None):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200100 ret = {
101 'success': [],
102 'errors': {},
103 'updated': [],
104 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200105 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200106 config = __salt__['config.get']('maas')
107 for part in self._config_path.split('.'):
108 config = config.get(part, {})
109 extra = {}
110 for name, url_call in self._extra_data_urls.iteritems():
111 key = 'id'
112 key_name = 'name'
113 if isinstance(url_call, tuple):
114 if len(url_call) == 2:
115 url_call, key = url_call[:]
116 else:
117 url_call, key, key_name = url_call[:]
118 json_res = json.loads(self._maas.get(url_call).read())
119 if key:
120 extra[name] = {v[key_name]: v[key] for v in json_res}
121 else:
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +0200122 extra[name] = {v[key_name]: v for v in json_res}
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200123 if self._all_elements_url:
124 all_elements = {}
125 elements = self._maas.get(self._all_elements_url).read()
126 res_json = json.loads(elements)
127 for element in res_json:
128 if isinstance(element, (str, unicode)):
129 all_elements[element] = {}
130 else:
131 all_elements[element[self._element_key]] = element
132 else:
133 all_elements = {}
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200134
135 def process_single(name, config_data):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200136 self._update = False
137 try:
138 data = self.fill_data(name, config_data, **extra)
139 if data is None:
140 ret['updated'].append(name)
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200141 return
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200142 if name in all_elements:
143 self._update = True
144 data = self.update(data, all_elements[name])
145 self.send(data)
146 ret['updated'].append(name)
147 else:
148 self.send(data)
149 ret['success'].append(name)
150 except urllib2.HTTPError as e:
151 etxt = e.read()
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200152 LOG.error('Failed for object %s reason %s', name, etxt)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200153 ret['errors'][name] = str(etxt)
154 except Exception as e:
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200155 LOG.error('Failed for object %s reason %s', name, e)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200156 ret['errors'][name] = str(e)
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200157 if objects_name is not None:
158 if ',' in objects_name:
159 objects_name = objects_name.split(',')
160 else:
161 objects_name = [objects_name]
162 for object_name in objects_name:
163 process_single(object_name, config[object_name])
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200164 else:
165 for name, config_data in config.iteritems():
166 process_single(name, config_data)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200167 except Exception as e:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200168 LOG.exception('Error Global')
169 raise
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100170 if ret['errors']:
171 raise Exception(ret)
172 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100173
174
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100175class Fabric(MaasObject):
176 def __init__(self):
177 super(Fabric, self).__init__()
178 self._all_elements_url = u'api/2.0/fabrics/'
179 self._create_url = u'api/2.0/fabrics/'
180 self._update_url = u'api/2.0/fabrics/{0}/'
181 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100182
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100183 def fill_data(self, name, fabric):
184 data = {
185 'name': name,
186 'description': fabric.get('description', ''),
187 }
188 if 'class_type' in fabric:
189 data['class_type'] = fabric.get('class_type'),
190 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100191
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100192 def update(self, new, old):
193 new['id'] = str(old['id'])
194 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100195
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200196
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100197class Subnet(MaasObject):
198 def __init__(self):
199 super(Subnet, self).__init__()
200 self._all_elements_url = u'api/2.0/subnets/'
201 self._create_url = u'api/2.0/subnets/'
202 self._update_url = u'api/2.0/subnets/{0}/'
203 self._config_path = 'region.subnets'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200204 self._extra_data_urls = {'fabrics': u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100205
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100206 def fill_data(self, name, subnet, fabrics):
207 data = {
208 'name': name,
209 'fabric': str(fabrics[subnet.get('fabric', '')]),
210 'cidr': subnet.get('cidr'),
211 'gateway_ip': subnet['gateway_ip'],
212 }
213 self._iprange = subnet['iprange']
214 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100215
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100216 def update(self, new, old):
217 new['id'] = str(old['id'])
218 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100219
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100220 def send(self, data):
221 response = super(Subnet, self).send(data)
222 res_json = json.loads(response)
223 self._process_iprange(res_json['id'])
224 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100225
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100226 def _process_iprange(self, subnet_id):
227 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
228 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
229 update = False
230 old_data = None
231 for iprange in ipranges:
232 if iprange['subnet']['id'] == subnet_id:
233 update = True
234 old_data = iprange
235 break
236 data = {
237 'start_ip': self._iprange.get('start'),
238 'end_ip': self._iprange.get('end'),
239 'subnet': str(subnet_id),
240 'type': self._iprange.get('type', 'dynamic')
241 }
242 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
243 LOG.info('iprange %s', _format_data(data))
244 if update:
245 LOG.warn('UPDATING %s %s', data, old_data)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200246 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']),
247 **data)
smolaonc3385f82016-03-11 19:01:24 +0100248 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100249 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100250
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200251
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100252class DHCPSnippet(MaasObject):
253 def __init__(self):
254 super(DHCPSnippet, self).__init__()
255 self._all_elements_url = u'api/2.0/dhcp-snippets/'
256 self._create_url = u'api/2.0/dhcp-snippets/'
257 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
258 self._config_path = 'region.dhcp_snippets'
259 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100260
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100261 def fill_data(self, name, snippet, subnets):
262 data = {
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200263 'name': name,
264 'value': snippet['value'],
265 'description': snippet['description'],
266 'enabled': str(snippet['enabled'] and 1 or 0),
267 'subnet': str(subnets[snippet['subnet']]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100268 }
269 return data
smolaonc3385f82016-03-11 19:01:24 +0100270
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100271 def update(self, new, old):
272 new['id'] = str(old['id'])
273 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100274
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200275
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100276class PacketRepository(MaasObject):
277 def __init__(self):
278 super(PacketRepository, self).__init__()
279 self._all_elements_url = u'api/2.0/package-repositories/'
280 self._create_url = u'api/2.0/package-repositories/'
281 self._update_url = u'api/2.0/package-repositories/{0}/'
282 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100283
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100284 def fill_data(self, name, package_repository):
285 data = {
286 'name': name,
287 'url': package_repository['url'],
288 'distributions': package_repository['distributions'],
289 'components': package_repository['components'],
290 'arches': package_repository['arches'],
291 'key': package_repository['key'],
292 'enabled': str(package_repository['enabled'] and 1 or 0),
293 }
294 if 'disabled_pockets' in package_repository:
295 data['disabled_pockets'] = package_repository['disable_pockets'],
296 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100297
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100298 def update(self, new, old):
299 new['id'] = str(old['id'])
300 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100301
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200302
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100303class Device(MaasObject):
304 def __init__(self):
305 super(Device, self).__init__()
306 self._all_elements_url = u'api/2.0/devices/'
307 self._create_url = u'api/2.0/devices/'
308 self._update_url = u'api/2.0/devices/{0}/'
309 self._config_path = 'region.devices'
310 self._element_key = 'hostname'
311 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100312
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100313 def fill_data(self, name, device_data):
314 data = {
315 'mac_addresses': device_data['mac'],
316 'hostname': name,
317 }
318 self._interface = device_data['interface']
319 return data
320
321 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100322 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
323 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100324 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100325 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100326 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
327 else:
328 new[self._update_key] = str(old[self._update_key])
329 return new
330
331 def send(self, data):
332 response = super(Device, self).send(data)
333 resp_json = json.loads(response)
334 system_id = resp_json['system_id']
335 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100336 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100337 return response
338
339 def _link_interface(self, system_id, interface_id):
340 data = {
341 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100342 'subnet': self._interface['subnet'],
343 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100344 }
345 if 'default_gateway' in self._interface:
346 data['default_gateway'] = self._interface.get('default_gateway')
347 if self._update:
348 data['force'] = '1'
349 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200350 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100351 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
352 .format(system_id, interface_id), 'link_subnet',
353 **data)
354
355
356class Machine(MaasObject):
357 def __init__(self):
358 super(Machine, self).__init__()
359 self._all_elements_url = u'api/2.0/machines/'
360 self._create_url = u'api/2.0/machines/'
361 self._update_url = u'api/2.0/machines/{0}/'
362 self._config_path = 'region.machines'
363 self._element_key = 'hostname'
364 self._update_key = 'system_id'
365
366 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100367 power_data = machine_data['power_parameters']
368 data = {
369 'hostname': name,
370 'architecture': machine_data.get('architecture', 'amd64/generic'),
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200371 'mac_addresses': machine_data['interface']['mac'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100372 'power_type': machine_data.get('power_type', 'ipmi'),
373 'power_parameters_power_address': power_data['power_address'],
374 }
Ondrej Smola455003c2017-06-01 22:53:39 +0200375 if 'power_driver' in power_data:
376 data['power_parameters_power_driver'] = power_data['power_driver']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100377 if 'power_user' in power_data:
378 data['power_parameters_power_user'] = power_data['power_user']
379 if 'power_password' in power_data:
380 data['power_parameters_power_pass'] = \
381 power_data['power_password']
382 return data
383
384 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100385 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
386 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100387 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100388 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200389 self._maas.delete(u'api/2.0/machines/{0}/'
390 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100391 else:
392 new[self._update_key] = str(old[self._update_key])
393 return new
394
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200395
396class AssignMachinesIP(MaasObject):
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200397 READY = 4
Andreyef156992017-07-03 14:54:03 -0500398 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200399
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200400 def __init__(self):
401 super(AssignMachinesIP, self).__init__()
402 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200403 self._create_url = \
404 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
405 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200406 self._config_path = 'region.machines'
407 self._element_key = 'hostname'
408 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200409 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
410 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200411
412 def fill_data(self, name, data, machines):
413 interface = data['interface']
414 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500415 if machine['status'] == self.DEPLOYED:
416 return
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200417 if machine['status'] != self.READY:
azvyagintsev7605a662017-11-03 19:05:04 +0200418 raise Exception('Machine:{} not in READY state'.format(name))
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200419 if 'ip' not in interface:
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100420 return
421 data = {
422 'mode': 'STATIC',
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200423 'subnet': str(interface.get('subnet')),
424 'ip_address': str(interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100425 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200426 if 'default_gateway' in interface:
427 data['default_gateway'] = interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200428 data['force'] = '1'
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200429 data['system_id'] = str(machine['system_id'])
430 data['interface_id'] = str(machine['interface_set'][0]['id'])
431 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100432
433
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200434class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200435 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200436 READY = 4
Andreyef156992017-07-03 14:54:03 -0500437 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200438
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200439 def __init__(self):
440 super(DeployMachines, self).__init__()
441 self._all_elements_url = None
442 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
443 self._config_path = 'region.machines'
444 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200445 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
446 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200447
448 def fill_data(self, name, machine_data, machines):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200449 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500450 if machine['status'] == self.DEPLOYED:
451 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200452 if machine['status'] != self.READY:
453 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200454 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200455 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200456 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200457 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200458 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200459 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200460 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200461 return data
462
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200463 def send(self, data):
464 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200465 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200466 return self._maas.post(self._create_url[0].format(**data),
467 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200468
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100469class BootResource(MaasObject):
470 def __init__(self):
471 super(BootResource, self).__init__()
472 self._all_elements_url = u'api/2.0/boot-resources/'
473 self._create_url = u'api/2.0/boot-resources/'
474 self._update_url = u'api/2.0/boot-resources/{0}/'
475 self._config_path = 'region.boot_resources'
476
477 def fill_data(self, name, boot_data):
478 sha256 = hashlib.sha256()
479 sha256.update(file(boot_data['content']).read())
480 data = {
481 'name': name,
482 'title': boot_data['title'],
483 'architecture': boot_data['architecture'],
484 'filetype': boot_data['filetype'],
485 'size': str(os.path.getsize(boot_data['content'])),
486 'sha256': sha256.hexdigest(),
487 'content': io.open(boot_data['content']),
488 }
489 return data
490
491 def update(self, new, old):
492 self._update = False
493 return new
494
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200495
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100496class CommissioningScripts(MaasObject):
497 def __init__(self):
498 super(CommissioningScripts, self).__init__()
499 self._all_elements_url = u'api/2.0/commissioning-scripts/'
500 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100501 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100502 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100503 self._update_key = 'name'
504
505 def fill_data(self, name, file_path):
506 data = {
507 'name': name,
508 'content': io.open(file_path),
509 }
510 return data
511
512 def update(self, new, old):
513 return new
514
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200515
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100516class MaasConfig(MaasObject):
517 def __init__(self):
518 super(MaasConfig, self).__init__()
519 self._all_elements_url = None
520 self._create_url = (u'api/2.0/maas/', u'set_config')
521 self._config_path = 'region.maas_config'
522
523 def fill_data(self, name, value):
524 data = {
525 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100526 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100527 }
528 return data
529
530 def update(self, new, old):
531 self._update = False
532 return new
533
534
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200535class SSHPrefs(MaasObject):
536 def __init__(self):
537 super(SSHPrefs, self).__init__()
538 self._all_elements_url = None
539 self._create_url = u'api/2.0/account/prefs/sshkeys/'
540 self._config_path = 'region.sshprefs'
541 self._element_key = 'hostname'
542 self._update_key = 'system_id'
543
544 def fill_data(self, value):
545 data = {
546 'key': value,
547 }
548 return data
549
550 def process(self):
551 config = __salt__['config.get']('maas')
552 for part in self._config_path.split('.'):
553 config = config.get(part, {})
554 extra = {}
555 for name, url_call in self._extra_data_urls.iteritems():
556 key = 'id'
557 if isinstance(url_call, tuple):
558 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200559 json_res = json.loads(self._maas.get(url_call).read())
560 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200561 if self._all_elements_url:
562 all_elements = {}
563 elements = self._maas.get(self._all_elements_url).read()
564 res_json = json.loads(elements)
565 for element in res_json:
566 if isinstance(element, (str, unicode)):
567 all_elements[element] = {}
568 else:
569 all_elements[element[self._element_key]] = element
570 else:
571 all_elements = {}
572 ret = {
573 'success': [],
574 'errors': {},
575 'updated': [],
576 }
577 for config_data in config:
578 name = config_data[:10]
579 try:
580 data = self.fill_data(config_data, **extra)
581 self.send(data)
582 ret['success'].append(name)
583 except urllib2.HTTPError as e:
584 etxt = e.read()
585 LOG.exception('Failed for object %s reason %s', name, etxt)
586 ret['errors'][name] = str(etxt)
587 except Exception as e:
588 LOG.exception('Failed for object %s reason %s', name, e)
589 ret['errors'][name] = str(e)
590 if ret['errors']:
591 raise Exception(ret)
592 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200593
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200594
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200595class Domain(MaasObject):
596 def __init__(self):
597 super(Domain, self).__init__()
598 self._all_elements_url = u'/api/2.0/domains/'
599 self._create_url = u'/api/2.0/domains/'
600 self._config_path = 'region.domain'
601 self._update_url = u'/api/2.0/domains/{0}/'
602
603 def fill_data(self, value):
604 data = {
605 'name': value,
606 }
607 self._update = True
608 return data
609
610 def update(self, new, old):
611 new['id'] = str(old['id'])
612 new['authoritative'] = str(old['authoritative'])
613 return new
614
615 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200616 ret = {
617 'success': [],
618 'errors': {},
619 'updated': [],
620 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200621 config = __salt__['config.get']('maas')
622 for part in self._config_path.split('.'):
623 config = config.get(part, {})
624 extra = {}
625 for name, url_call in self._extra_data_urls.iteritems():
626 key = 'id'
627 if isinstance(url_call, tuple):
628 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200629 json_res = json.loads(self._maas.get(url_call).read())
630 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200631 if self._all_elements_url:
632 all_elements = {}
633 elements = self._maas.get(self._all_elements_url).read()
634 res_json = json.loads(elements)
635 for element in res_json:
636 if isinstance(element, (str, unicode)):
637 all_elements[element] = {}
638 else:
639 all_elements[element[self._element_key]] = element
640 else:
641 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200642 try:
643 data = self.fill_data(config, **extra)
644 data = self.update(data, all_elements.values()[0])
645 self.send(data)
646 ret['success'].append('domain')
647 except urllib2.HTTPError as e:
648 etxt = e.read()
649 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
650 ret['errors']['domain'] = str(etxt)
651 except Exception as e:
652 LOG.exception('Failed for object %s reason %s', 'domain', e)
653 ret['errors']['domain'] = str(e)
654 if ret['errors']:
655 raise Exception(ret)
656 return ret
657
658
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200659class MachinesStatus(MaasObject):
660 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200661 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200662 cls._maas = _create_maas_client()
663 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200664 json_result = json.loads(result.read())
665 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200666 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200667 if objects_name:
668 if ',' in objects_name:
669 objects_name = set(objects_name.split(','))
670 else:
671 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200672 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200673 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200674 continue
azvyagintsev7605a662017-11-03 19:05:04 +0200675 status = STATUS_NAME_DICT[machine['status'].lower()]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200676 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200677 res.append(
678 {'hostname': machine['hostname'],
679 'system_id': machine['system_id'],
680 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200681 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200682
azvyagintsev7605a662017-11-03 19:05:04 +0200683 @classmethod
684 def wait_for_machine_status(cls, **kwargs):
685 """
686 A function that wait for any requested status, for any set of maas
687 machines.
688
689 If no kwargs has been passed - will try to wait ALL
690 defined in salt::maas::region::machines
691
692 See readme file for more examples.
693 CLI Example:
694 .. code-block:: bash
695
696 salt-call state.apply maas.machines.wait_for_deployed
697
698 :param kwargs:
699 timeout: in s; Global timeout for wait
700 poll_time: in s;Sleep time, between retry
701 req_status: string; Polling status
702 machines: list; machine names
703 ignore_machines: list; machine names
704 :ret: True
705 Exception - if something fail/timeout reached
706 """
707 timeout = kwargs.get("timeout", 60 * 120)
708 poll_time = kwargs.get("poll_time", 30)
709 req_status = kwargs.get("req_status", "Ready")
710 to_discover = kwargs.get("machines", None)
711 ignore_machines = kwargs.get("ignore_machines", None)
712 if not to_discover:
713 try:
714 to_discover = __salt__['config.get']('maas')['region'][
715 'machines'].keys()
716 except KeyError:
717 LOG.warning("No defined machines!")
718 return True
719 total = copy.deepcopy(to_discover) or []
720 if ignore_machines and total:
721 total = [x for x in to_discover if x not in ignore_machines]
722 started_at = time.time()
723 while len(total) <= len(to_discover):
724 for m in to_discover:
725 for discovered in MachinesStatus.execute()['machines']:
726 if m == discovered['hostname'] and \
727 discovered['status'] == req_status.lower():
728 if m in total: total.remove(m)
729 if len(total) <= 0:
730 LOG.debug(
731 "Machines:{} are:{}".format(to_discover, req_status))
732 return True
733 if (timeout - (time.time() - started_at)) <= 0:
734 raise Exception(
735 'Machines:{}not in {} state'.format(total, req_status))
736 LOG.info(
737 "Waiting status:{} "
738 "for machines:{}"
739 "\nsleep for:{}s "
740 "Timeout:{}s".format(req_status, total, poll_time, timeout))
741 time.sleep(poll_time)
742
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200743
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100744def process_fabrics():
745 return Fabric().process()
746
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200747
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100748def process_subnets():
749 return Subnet().process()
750
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200751
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100752def process_dhcp_snippets():
753 return DHCPSnippet().process()
754
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200755
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100756def process_package_repositories():
757 return PacketRepository().process()
758
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200759
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +0200760def process_devices(*args):
761 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100762
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200763
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200764def process_machines(*args):
765 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100766
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200767
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200768def process_assign_machines_ip(*args):
769 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200770
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200771
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200772def machines_status(*args):
773 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200774
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200775
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200776def deploy_machines(*args):
777 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200778
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200779
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100780def process_boot_resources():
781 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100782
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200783
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100784def process_maas_config():
785 return MaasConfig().process()
786
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200787
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100788def process_commissioning_scripts():
789 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200790
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200791
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200792def process_domain():
793 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200794
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200795
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200796def process_sshprefs():
797 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +0200798
799
800def wait_for_machine_status(**kwargs):
801 return MachinesStatus.wait_for_machine_status(**kwargs)