blob: 2a615b7ca91e56d4d300f85c6e1d5ba8886d590b [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,
Alexandru Avadanii652e7552017-08-19 02:03:01 +0200209 'fabric': str(fabrics[subnet.get('fabric',
210 self._get_fabric_from_cidr(subnet.get('cidr')))]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100211 'cidr': subnet.get('cidr'),
212 'gateway_ip': subnet['gateway_ip'],
213 }
214 self._iprange = subnet['iprange']
215 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100216
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100217 def update(self, new, old):
218 new['id'] = str(old['id'])
219 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100220
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100221 def send(self, data):
222 response = super(Subnet, self).send(data)
223 res_json = json.loads(response)
224 self._process_iprange(res_json['id'])
225 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100226
Alexandru Avadanii652e7552017-08-19 02:03:01 +0200227 def _get_fabric_from_cidr(self, cidr):
228 subnets = json.loads(self._maas.get(u'api/2.0/subnets/').read())
229 for subnet in subnets:
230 if subnet['cidr'] == cidr:
231 return subnet['vlan']['fabric']
232 return ''
233
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100234 def _process_iprange(self, subnet_id):
235 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
236 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
237 update = False
238 old_data = None
239 for iprange in ipranges:
240 if iprange['subnet']['id'] == subnet_id:
241 update = True
242 old_data = iprange
243 break
244 data = {
245 'start_ip': self._iprange.get('start'),
246 'end_ip': self._iprange.get('end'),
247 'subnet': str(subnet_id),
248 'type': self._iprange.get('type', 'dynamic')
249 }
250 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
251 LOG.info('iprange %s', _format_data(data))
252 if update:
253 LOG.warn('UPDATING %s %s', data, old_data)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200254 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']),
255 **data)
smolaonc3385f82016-03-11 19:01:24 +0100256 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100257 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100258
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200259
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100260class DHCPSnippet(MaasObject):
261 def __init__(self):
262 super(DHCPSnippet, self).__init__()
263 self._all_elements_url = u'api/2.0/dhcp-snippets/'
264 self._create_url = u'api/2.0/dhcp-snippets/'
265 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
266 self._config_path = 'region.dhcp_snippets'
267 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100268
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100269 def fill_data(self, name, snippet, subnets):
270 data = {
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200271 'name': name,
272 'value': snippet['value'],
273 'description': snippet['description'],
274 'enabled': str(snippet['enabled'] and 1 or 0),
275 'subnet': str(subnets[snippet['subnet']]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100276 }
277 return data
smolaonc3385f82016-03-11 19:01:24 +0100278
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100279 def update(self, new, old):
280 new['id'] = str(old['id'])
281 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100282
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200283
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100284class PacketRepository(MaasObject):
285 def __init__(self):
286 super(PacketRepository, self).__init__()
287 self._all_elements_url = u'api/2.0/package-repositories/'
288 self._create_url = u'api/2.0/package-repositories/'
289 self._update_url = u'api/2.0/package-repositories/{0}/'
290 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100291
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100292 def fill_data(self, name, package_repository):
293 data = {
294 'name': name,
295 'url': package_repository['url'],
296 'distributions': package_repository['distributions'],
297 'components': package_repository['components'],
298 'arches': package_repository['arches'],
299 'key': package_repository['key'],
300 'enabled': str(package_repository['enabled'] and 1 or 0),
301 }
302 if 'disabled_pockets' in package_repository:
303 data['disabled_pockets'] = package_repository['disable_pockets'],
304 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100305
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100306 def update(self, new, old):
307 new['id'] = str(old['id'])
308 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100309
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200310
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100311class Device(MaasObject):
312 def __init__(self):
313 super(Device, self).__init__()
314 self._all_elements_url = u'api/2.0/devices/'
315 self._create_url = u'api/2.0/devices/'
316 self._update_url = u'api/2.0/devices/{0}/'
317 self._config_path = 'region.devices'
318 self._element_key = 'hostname'
319 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100320
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100321 def fill_data(self, name, device_data):
322 data = {
323 'mac_addresses': device_data['mac'],
324 'hostname': name,
325 }
326 self._interface = device_data['interface']
327 return data
328
329 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100330 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
331 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100332 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100333 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100334 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
335 else:
336 new[self._update_key] = str(old[self._update_key])
337 return new
338
339 def send(self, data):
340 response = super(Device, self).send(data)
341 resp_json = json.loads(response)
342 system_id = resp_json['system_id']
343 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100344 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100345 return response
346
347 def _link_interface(self, system_id, interface_id):
348 data = {
349 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100350 'subnet': self._interface['subnet'],
351 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100352 }
353 if 'default_gateway' in self._interface:
354 data['default_gateway'] = self._interface.get('default_gateway')
355 if self._update:
356 data['force'] = '1'
357 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200358 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100359 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
360 .format(system_id, interface_id), 'link_subnet',
361 **data)
362
363
364class Machine(MaasObject):
365 def __init__(self):
366 super(Machine, self).__init__()
367 self._all_elements_url = u'api/2.0/machines/'
368 self._create_url = u'api/2.0/machines/'
369 self._update_url = u'api/2.0/machines/{0}/'
370 self._config_path = 'region.machines'
371 self._element_key = 'hostname'
372 self._update_key = 'system_id'
373
374 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100375 power_data = machine_data['power_parameters']
376 data = {
377 'hostname': name,
378 'architecture': machine_data.get('architecture', 'amd64/generic'),
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200379 'mac_addresses': machine_data['interface']['mac'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100380 'power_type': machine_data.get('power_type', 'ipmi'),
381 'power_parameters_power_address': power_data['power_address'],
382 }
Ondrej Smola455003c2017-06-01 22:53:39 +0200383 if 'power_driver' in power_data:
384 data['power_parameters_power_driver'] = power_data['power_driver']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100385 if 'power_user' in power_data:
386 data['power_parameters_power_user'] = power_data['power_user']
387 if 'power_password' in power_data:
388 data['power_parameters_power_pass'] = \
389 power_data['power_password']
Petr Ruzicka5fe96742017-11-10 14:22:24 +0100390 if 'power_id' in power_data:
391 data['power_parameters_power_id'] = power_data['power_id']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100392 return data
393
394 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100395 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
396 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100397 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100398 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200399 self._maas.delete(u'api/2.0/machines/{0}/'
400 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100401 else:
402 new[self._update_key] = str(old[self._update_key])
403 return new
404
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200405
406class AssignMachinesIP(MaasObject):
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200407 READY = 4
Andreyef156992017-07-03 14:54:03 -0500408 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200409
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200410 def __init__(self):
411 super(AssignMachinesIP, self).__init__()
412 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200413 self._create_url = \
414 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
415 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200416 self._config_path = 'region.machines'
417 self._element_key = 'hostname'
418 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200419 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
420 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200421
422 def fill_data(self, name, data, machines):
423 interface = data['interface']
424 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500425 if machine['status'] == self.DEPLOYED:
426 return
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200427 if machine['status'] != self.READY:
azvyagintsev7605a662017-11-03 19:05:04 +0200428 raise Exception('Machine:{} not in READY state'.format(name))
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200429 if 'ip' not in interface:
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100430 return
431 data = {
432 'mode': 'STATIC',
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200433 'subnet': str(interface.get('subnet')),
434 'ip_address': str(interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100435 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200436 if 'default_gateway' in interface:
437 data['default_gateway'] = interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200438 data['force'] = '1'
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200439 data['system_id'] = str(machine['system_id'])
440 data['interface_id'] = str(machine['interface_set'][0]['id'])
441 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100442
443
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200444class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200445 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200446 READY = 4
Andreyef156992017-07-03 14:54:03 -0500447 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200448
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200449 def __init__(self):
450 super(DeployMachines, self).__init__()
451 self._all_elements_url = None
452 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
453 self._config_path = 'region.machines'
454 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200455 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
456 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200457
458 def fill_data(self, name, machine_data, machines):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200459 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500460 if machine['status'] == self.DEPLOYED:
461 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200462 if machine['status'] != self.READY:
463 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200464 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200465 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200466 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200467 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200468 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200469 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200470 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200471 return data
472
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200473 def send(self, data):
474 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200475 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200476 return self._maas.post(self._create_url[0].format(**data),
477 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200478
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100479class BootResource(MaasObject):
480 def __init__(self):
481 super(BootResource, self).__init__()
482 self._all_elements_url = u'api/2.0/boot-resources/'
483 self._create_url = u'api/2.0/boot-resources/'
484 self._update_url = u'api/2.0/boot-resources/{0}/'
485 self._config_path = 'region.boot_resources'
486
487 def fill_data(self, name, boot_data):
488 sha256 = hashlib.sha256()
489 sha256.update(file(boot_data['content']).read())
490 data = {
491 'name': name,
492 'title': boot_data['title'],
493 'architecture': boot_data['architecture'],
494 'filetype': boot_data['filetype'],
495 'size': str(os.path.getsize(boot_data['content'])),
496 'sha256': sha256.hexdigest(),
497 'content': io.open(boot_data['content']),
498 }
499 return data
500
501 def update(self, new, old):
502 self._update = False
503 return new
504
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200505
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100506class CommissioningScripts(MaasObject):
507 def __init__(self):
508 super(CommissioningScripts, self).__init__()
509 self._all_elements_url = u'api/2.0/commissioning-scripts/'
510 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100511 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100512 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100513 self._update_key = 'name'
514
515 def fill_data(self, name, file_path):
516 data = {
517 'name': name,
518 'content': io.open(file_path),
519 }
520 return data
521
522 def update(self, new, old):
523 return new
524
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200525
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100526class MaasConfig(MaasObject):
527 def __init__(self):
528 super(MaasConfig, self).__init__()
529 self._all_elements_url = None
530 self._create_url = (u'api/2.0/maas/', u'set_config')
531 self._config_path = 'region.maas_config'
532
533 def fill_data(self, name, value):
534 data = {
535 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100536 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100537 }
538 return data
539
540 def update(self, new, old):
541 self._update = False
542 return new
543
544
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200545class SSHPrefs(MaasObject):
546 def __init__(self):
547 super(SSHPrefs, self).__init__()
548 self._all_elements_url = None
549 self._create_url = u'api/2.0/account/prefs/sshkeys/'
550 self._config_path = 'region.sshprefs'
551 self._element_key = 'hostname'
552 self._update_key = 'system_id'
553
554 def fill_data(self, value):
555 data = {
556 'key': value,
557 }
558 return data
559
560 def process(self):
561 config = __salt__['config.get']('maas')
562 for part in self._config_path.split('.'):
563 config = config.get(part, {})
564 extra = {}
565 for name, url_call in self._extra_data_urls.iteritems():
566 key = 'id'
567 if isinstance(url_call, tuple):
568 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200569 json_res = json.loads(self._maas.get(url_call).read())
570 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200571 if self._all_elements_url:
572 all_elements = {}
573 elements = self._maas.get(self._all_elements_url).read()
574 res_json = json.loads(elements)
575 for element in res_json:
576 if isinstance(element, (str, unicode)):
577 all_elements[element] = {}
578 else:
579 all_elements[element[self._element_key]] = element
580 else:
581 all_elements = {}
582 ret = {
583 'success': [],
584 'errors': {},
585 'updated': [],
586 }
587 for config_data in config:
588 name = config_data[:10]
589 try:
590 data = self.fill_data(config_data, **extra)
591 self.send(data)
592 ret['success'].append(name)
593 except urllib2.HTTPError as e:
594 etxt = e.read()
595 LOG.exception('Failed for object %s reason %s', name, etxt)
596 ret['errors'][name] = str(etxt)
597 except Exception as e:
598 LOG.exception('Failed for object %s reason %s', name, e)
599 ret['errors'][name] = str(e)
600 if ret['errors']:
601 raise Exception(ret)
602 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200603
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200604
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200605class Domain(MaasObject):
606 def __init__(self):
607 super(Domain, self).__init__()
608 self._all_elements_url = u'/api/2.0/domains/'
609 self._create_url = u'/api/2.0/domains/'
610 self._config_path = 'region.domain'
611 self._update_url = u'/api/2.0/domains/{0}/'
612
613 def fill_data(self, value):
614 data = {
615 'name': value,
616 }
617 self._update = True
618 return data
619
620 def update(self, new, old):
621 new['id'] = str(old['id'])
622 new['authoritative'] = str(old['authoritative'])
623 return new
624
625 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200626 ret = {
627 'success': [],
628 'errors': {},
629 'updated': [],
630 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200631 config = __salt__['config.get']('maas')
632 for part in self._config_path.split('.'):
633 config = config.get(part, {})
634 extra = {}
635 for name, url_call in self._extra_data_urls.iteritems():
636 key = 'id'
637 if isinstance(url_call, tuple):
638 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200639 json_res = json.loads(self._maas.get(url_call).read())
640 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200641 if self._all_elements_url:
642 all_elements = {}
643 elements = self._maas.get(self._all_elements_url).read()
644 res_json = json.loads(elements)
645 for element in res_json:
646 if isinstance(element, (str, unicode)):
647 all_elements[element] = {}
648 else:
649 all_elements[element[self._element_key]] = element
650 else:
651 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200652 try:
653 data = self.fill_data(config, **extra)
654 data = self.update(data, all_elements.values()[0])
655 self.send(data)
656 ret['success'].append('domain')
657 except urllib2.HTTPError as e:
658 etxt = e.read()
659 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
660 ret['errors']['domain'] = str(etxt)
661 except Exception as e:
662 LOG.exception('Failed for object %s reason %s', 'domain', e)
663 ret['errors']['domain'] = str(e)
664 if ret['errors']:
665 raise Exception(ret)
666 return ret
667
668
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200669class MachinesStatus(MaasObject):
670 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200671 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200672 cls._maas = _create_maas_client()
673 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200674 json_result = json.loads(result.read())
675 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200676 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200677 if objects_name:
678 if ',' in objects_name:
679 objects_name = set(objects_name.split(','))
680 else:
681 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200682 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200683 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200684 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400685 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200686 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200687 res.append(
688 {'hostname': machine['hostname'],
689 'system_id': machine['system_id'],
690 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200691 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200692
azvyagintsev7605a662017-11-03 19:05:04 +0200693 @classmethod
694 def wait_for_machine_status(cls, **kwargs):
695 """
696 A function that wait for any requested status, for any set of maas
697 machines.
698
699 If no kwargs has been passed - will try to wait ALL
700 defined in salt::maas::region::machines
701
702 See readme file for more examples.
703 CLI Example:
704 .. code-block:: bash
705
706 salt-call state.apply maas.machines.wait_for_deployed
707
708 :param kwargs:
709 timeout: in s; Global timeout for wait
710 poll_time: in s;Sleep time, between retry
711 req_status: string; Polling status
712 machines: list; machine names
713 ignore_machines: list; machine names
714 :ret: True
715 Exception - if something fail/timeout reached
716 """
717 timeout = kwargs.get("timeout", 60 * 120)
718 poll_time = kwargs.get("poll_time", 30)
719 req_status = kwargs.get("req_status", "Ready")
720 to_discover = kwargs.get("machines", None)
721 ignore_machines = kwargs.get("ignore_machines", None)
722 if not to_discover:
723 try:
724 to_discover = __salt__['config.get']('maas')['region'][
725 'machines'].keys()
726 except KeyError:
727 LOG.warning("No defined machines!")
728 return True
729 total = copy.deepcopy(to_discover) or []
730 if ignore_machines and total:
731 total = [x for x in to_discover if x not in ignore_machines]
732 started_at = time.time()
733 while len(total) <= len(to_discover):
734 for m in to_discover:
735 for discovered in MachinesStatus.execute()['machines']:
736 if m == discovered['hostname'] and \
Michael Polenchuke438bd32017-11-09 20:42:42 +0400737 discovered['status'].lower() == req_status.lower():
738 if m in total:
739 total.remove(m)
740
azvyagintsev7605a662017-11-03 19:05:04 +0200741 if len(total) <= 0:
742 LOG.debug(
743 "Machines:{} are:{}".format(to_discover, req_status))
744 return True
745 if (timeout - (time.time() - started_at)) <= 0:
746 raise Exception(
747 'Machines:{}not in {} state'.format(total, req_status))
748 LOG.info(
749 "Waiting status:{} "
750 "for machines:{}"
751 "\nsleep for:{}s "
752 "Timeout:{}s".format(req_status, total, poll_time, timeout))
753 time.sleep(poll_time)
754
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200755
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100756def process_fabrics():
757 return Fabric().process()
758
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200759
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100760def process_subnets():
761 return Subnet().process()
762
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200763
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100764def process_dhcp_snippets():
765 return DHCPSnippet().process()
766
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200767
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100768def process_package_repositories():
769 return PacketRepository().process()
770
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200771
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +0200772def process_devices(*args):
773 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100774
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200775
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200776def process_machines(*args):
777 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100778
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200779
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200780def process_assign_machines_ip(*args):
781 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200782
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200783
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200784def machines_status(*args):
785 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200786
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200787
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200788def deploy_machines(*args):
789 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200790
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200791
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100792def process_boot_resources():
793 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100794
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200795
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100796def process_maas_config():
797 return MaasConfig().process()
798
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200799
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100800def process_commissioning_scripts():
801 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200802
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200803
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200804def process_domain():
805 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200806
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200807
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200808def process_sshprefs():
809 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +0200810
811
812def wait_for_machine_status(**kwargs):
813 return MachinesStatus.wait_for_machine_status(**kwargs)