blob: e279b702964f4fbdd523a6c971d8cfe9ed049066 [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']
Petr Ruzicka5fe96742017-11-10 14:22:24 +0100382 if 'power_id' in power_data:
383 data['power_parameters_power_id'] = power_data['power_id']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100384 return data
385
386 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100387 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
388 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100389 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100390 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200391 self._maas.delete(u'api/2.0/machines/{0}/'
392 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100393 else:
394 new[self._update_key] = str(old[self._update_key])
395 return new
396
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200397
398class AssignMachinesIP(MaasObject):
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200399 READY = 4
Andreyef156992017-07-03 14:54:03 -0500400 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200401
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200402 def __init__(self):
403 super(AssignMachinesIP, self).__init__()
404 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200405 self._create_url = \
406 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
407 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200408 self._config_path = 'region.machines'
409 self._element_key = 'hostname'
410 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200411 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
412 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200413
414 def fill_data(self, name, data, machines):
415 interface = data['interface']
416 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500417 if machine['status'] == self.DEPLOYED:
418 return
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200419 if machine['status'] != self.READY:
azvyagintsev7605a662017-11-03 19:05:04 +0200420 raise Exception('Machine:{} not in READY state'.format(name))
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200421 if 'ip' not in interface:
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100422 return
423 data = {
424 'mode': 'STATIC',
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200425 'subnet': str(interface.get('subnet')),
426 'ip_address': str(interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100427 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200428 if 'default_gateway' in interface:
429 data['default_gateway'] = interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200430 data['force'] = '1'
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200431 data['system_id'] = str(machine['system_id'])
432 data['interface_id'] = str(machine['interface_set'][0]['id'])
433 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100434
435
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200436class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200437 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200438 READY = 4
Andreyef156992017-07-03 14:54:03 -0500439 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200440
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200441 def __init__(self):
442 super(DeployMachines, self).__init__()
443 self._all_elements_url = None
444 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
445 self._config_path = 'region.machines'
446 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200447 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
448 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200449
450 def fill_data(self, name, machine_data, machines):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200451 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500452 if machine['status'] == self.DEPLOYED:
453 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200454 if machine['status'] != self.READY:
455 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200456 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200457 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200458 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200459 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200460 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200461 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200462 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200463 return data
464
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200465 def send(self, data):
466 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200467 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200468 return self._maas.post(self._create_url[0].format(**data),
469 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200470
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100471class BootResource(MaasObject):
472 def __init__(self):
473 super(BootResource, self).__init__()
474 self._all_elements_url = u'api/2.0/boot-resources/'
475 self._create_url = u'api/2.0/boot-resources/'
476 self._update_url = u'api/2.0/boot-resources/{0}/'
477 self._config_path = 'region.boot_resources'
478
479 def fill_data(self, name, boot_data):
480 sha256 = hashlib.sha256()
481 sha256.update(file(boot_data['content']).read())
482 data = {
483 'name': name,
484 'title': boot_data['title'],
485 'architecture': boot_data['architecture'],
486 'filetype': boot_data['filetype'],
487 'size': str(os.path.getsize(boot_data['content'])),
488 'sha256': sha256.hexdigest(),
489 'content': io.open(boot_data['content']),
490 }
491 return data
492
493 def update(self, new, old):
494 self._update = False
495 return new
496
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200497
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100498class CommissioningScripts(MaasObject):
499 def __init__(self):
500 super(CommissioningScripts, self).__init__()
501 self._all_elements_url = u'api/2.0/commissioning-scripts/'
502 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100503 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100504 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100505 self._update_key = 'name'
506
507 def fill_data(self, name, file_path):
508 data = {
509 'name': name,
510 'content': io.open(file_path),
511 }
512 return data
513
514 def update(self, new, old):
515 return new
516
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200517
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100518class MaasConfig(MaasObject):
519 def __init__(self):
520 super(MaasConfig, self).__init__()
521 self._all_elements_url = None
522 self._create_url = (u'api/2.0/maas/', u'set_config')
523 self._config_path = 'region.maas_config'
524
525 def fill_data(self, name, value):
526 data = {
527 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100528 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100529 }
530 return data
531
532 def update(self, new, old):
533 self._update = False
534 return new
535
536
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200537class SSHPrefs(MaasObject):
538 def __init__(self):
539 super(SSHPrefs, self).__init__()
540 self._all_elements_url = None
541 self._create_url = u'api/2.0/account/prefs/sshkeys/'
542 self._config_path = 'region.sshprefs'
543 self._element_key = 'hostname'
544 self._update_key = 'system_id'
545
546 def fill_data(self, value):
547 data = {
548 'key': value,
549 }
550 return data
551
552 def process(self):
553 config = __salt__['config.get']('maas')
554 for part in self._config_path.split('.'):
555 config = config.get(part, {})
556 extra = {}
557 for name, url_call in self._extra_data_urls.iteritems():
558 key = 'id'
559 if isinstance(url_call, tuple):
560 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200561 json_res = json.loads(self._maas.get(url_call).read())
562 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200563 if self._all_elements_url:
564 all_elements = {}
565 elements = self._maas.get(self._all_elements_url).read()
566 res_json = json.loads(elements)
567 for element in res_json:
568 if isinstance(element, (str, unicode)):
569 all_elements[element] = {}
570 else:
571 all_elements[element[self._element_key]] = element
572 else:
573 all_elements = {}
574 ret = {
575 'success': [],
576 'errors': {},
577 'updated': [],
578 }
579 for config_data in config:
580 name = config_data[:10]
581 try:
582 data = self.fill_data(config_data, **extra)
583 self.send(data)
584 ret['success'].append(name)
585 except urllib2.HTTPError as e:
586 etxt = e.read()
587 LOG.exception('Failed for object %s reason %s', name, etxt)
588 ret['errors'][name] = str(etxt)
589 except Exception as e:
590 LOG.exception('Failed for object %s reason %s', name, e)
591 ret['errors'][name] = str(e)
592 if ret['errors']:
593 raise Exception(ret)
594 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200595
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200596
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200597class Domain(MaasObject):
598 def __init__(self):
599 super(Domain, self).__init__()
600 self._all_elements_url = u'/api/2.0/domains/'
601 self._create_url = u'/api/2.0/domains/'
602 self._config_path = 'region.domain'
603 self._update_url = u'/api/2.0/domains/{0}/'
604
605 def fill_data(self, value):
606 data = {
607 'name': value,
608 }
609 self._update = True
610 return data
611
612 def update(self, new, old):
613 new['id'] = str(old['id'])
614 new['authoritative'] = str(old['authoritative'])
615 return new
616
617 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200618 ret = {
619 'success': [],
620 'errors': {},
621 'updated': [],
622 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200623 config = __salt__['config.get']('maas')
624 for part in self._config_path.split('.'):
625 config = config.get(part, {})
626 extra = {}
627 for name, url_call in self._extra_data_urls.iteritems():
628 key = 'id'
629 if isinstance(url_call, tuple):
630 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200631 json_res = json.loads(self._maas.get(url_call).read())
632 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200633 if self._all_elements_url:
634 all_elements = {}
635 elements = self._maas.get(self._all_elements_url).read()
636 res_json = json.loads(elements)
637 for element in res_json:
638 if isinstance(element, (str, unicode)):
639 all_elements[element] = {}
640 else:
641 all_elements[element[self._element_key]] = element
642 else:
643 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200644 try:
645 data = self.fill_data(config, **extra)
646 data = self.update(data, all_elements.values()[0])
647 self.send(data)
648 ret['success'].append('domain')
649 except urllib2.HTTPError as e:
650 etxt = e.read()
651 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
652 ret['errors']['domain'] = str(etxt)
653 except Exception as e:
654 LOG.exception('Failed for object %s reason %s', 'domain', e)
655 ret['errors']['domain'] = str(e)
656 if ret['errors']:
657 raise Exception(ret)
658 return ret
659
660
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200661class MachinesStatus(MaasObject):
662 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200663 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200664 cls._maas = _create_maas_client()
665 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200666 json_result = json.loads(result.read())
667 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200668 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200669 if objects_name:
670 if ',' in objects_name:
671 objects_name = set(objects_name.split(','))
672 else:
673 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200674 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200675 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200676 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400677 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200678 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200679 res.append(
680 {'hostname': machine['hostname'],
681 'system_id': machine['system_id'],
682 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200683 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200684
azvyagintsev7605a662017-11-03 19:05:04 +0200685 @classmethod
686 def wait_for_machine_status(cls, **kwargs):
687 """
688 A function that wait for any requested status, for any set of maas
689 machines.
690
691 If no kwargs has been passed - will try to wait ALL
692 defined in salt::maas::region::machines
693
694 See readme file for more examples.
695 CLI Example:
696 .. code-block:: bash
697
698 salt-call state.apply maas.machines.wait_for_deployed
699
700 :param kwargs:
701 timeout: in s; Global timeout for wait
702 poll_time: in s;Sleep time, between retry
703 req_status: string; Polling status
704 machines: list; machine names
705 ignore_machines: list; machine names
706 :ret: True
707 Exception - if something fail/timeout reached
708 """
709 timeout = kwargs.get("timeout", 60 * 120)
710 poll_time = kwargs.get("poll_time", 30)
711 req_status = kwargs.get("req_status", "Ready")
712 to_discover = kwargs.get("machines", None)
713 ignore_machines = kwargs.get("ignore_machines", None)
714 if not to_discover:
715 try:
716 to_discover = __salt__['config.get']('maas')['region'][
717 'machines'].keys()
718 except KeyError:
719 LOG.warning("No defined machines!")
720 return True
721 total = copy.deepcopy(to_discover) or []
722 if ignore_machines and total:
723 total = [x for x in to_discover if x not in ignore_machines]
724 started_at = time.time()
725 while len(total) <= len(to_discover):
726 for m in to_discover:
727 for discovered in MachinesStatus.execute()['machines']:
728 if m == discovered['hostname'] and \
Michael Polenchuke438bd32017-11-09 20:42:42 +0400729 discovered['status'].lower() == req_status.lower():
730 if m in total:
731 total.remove(m)
732
azvyagintsev7605a662017-11-03 19:05:04 +0200733 if len(total) <= 0:
734 LOG.debug(
735 "Machines:{} are:{}".format(to_discover, req_status))
736 return True
737 if (timeout - (time.time() - started_at)) <= 0:
738 raise Exception(
739 'Machines:{}not in {} state'.format(total, req_status))
740 LOG.info(
741 "Waiting status:{} "
742 "for machines:{}"
743 "\nsleep for:{}s "
744 "Timeout:{}s".format(req_status, total, poll_time, timeout))
745 time.sleep(poll_time)
746
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200747
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100748def process_fabrics():
749 return Fabric().process()
750
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200751
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100752def process_subnets():
753 return Subnet().process()
754
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200755
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100756def process_dhcp_snippets():
757 return DHCPSnippet().process()
758
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200759
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100760def process_package_repositories():
761 return PacketRepository().process()
762
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200763
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +0200764def process_devices(*args):
765 return Device().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_machines(*args):
769 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100770
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200771
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200772def process_assign_machines_ip(*args):
773 return AssignMachinesIP().process(*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 machines_status(*args):
777 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200778
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200779
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200780def deploy_machines(*args):
781 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200782
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200783
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100784def process_boot_resources():
785 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100786
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200787
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100788def process_maas_config():
789 return MaasConfig().process()
790
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200791
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100792def process_commissioning_scripts():
793 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200794
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200795
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200796def process_domain():
797 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200798
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200799
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200800def process_sshprefs():
801 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +0200802
803
804def wait_for_machine_status(**kwargs):
805 return MachinesStatus.wait_for_machine_status(**kwargs)