blob: 1df4698399b91d29d33479bc7128416d3624762a [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'),
Alexandru Avadanii08ffc3f2017-12-17 06:30:27 +010055 (15, 'Failed disk erasing'), (16, 'Rescue mode'),
56 (17, 'Entering rescue mode'), (18, 'Failed to enter rescue mode'),
57 (19, 'Exiting rescue mode'), (20, 'Failed to exit rescue mode'),
58 (21, 'Testing'), (22, 'Failed testing')])
azvyagintsev7605a662017-11-03 19:05:04 +020059
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020060
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010061def _format_data(data):
62 class Lazy:
63 def __str__(self):
64 return ' '.join(['{0}={1}'.format(k, v)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020065 for k, v in data.iteritems()])
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +020066 return Lazy()
Ales Komarek663b85c2016-03-11 14:26:42 +010067
68
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010069def _create_maas_client():
70 global APIKEY_FILE
71 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020072 api_token = file(APIKEY_FILE).read().splitlines()[-1].strip()\
73 .split(':')
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010074 except:
75 LOG.exception('token')
76 auth = MAASOAuth(*api_token)
77 api_url = 'http://localhost:5240/MAAS'
Ales Komarek663b85c2016-03-11 14:26:42 +010078 dispatcher = MAASDispatcher()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010079 return MAASClient(auth, dispatcher, api_url)
Ales Komarek663b85c2016-03-11 14:26:42 +010080
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020081
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +010082class MaasObject(object):
83 def __init__(self):
84 self._maas = _create_maas_client()
85 self._extra_data_urls = {}
86 self._extra_data = {}
87 self._update = False
88 self._element_key = 'name'
89 self._update_key = 'id'
90
91 def send(self, data):
92 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
93 if self._update:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +020094 return self._maas.put(
95 self._update_url.format(data[self._update_key]), **data).read()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +010096 if isinstance(self._create_url, tuple):
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +020097 return self._maas.post(self._create_url[0].format(**data),
98 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć76d9a5c2017-04-14 12:00:42 +020099 return self._maas.post(self._create_url.format(**data),
100 None, **data).read()
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100101
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200102 def process(self, objects_name=None):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200103 ret = {
104 'success': [],
105 'errors': {},
106 'updated': [],
107 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200108 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200109 config = __salt__['config.get']('maas')
110 for part in self._config_path.split('.'):
111 config = config.get(part, {})
112 extra = {}
113 for name, url_call in self._extra_data_urls.iteritems():
114 key = 'id'
115 key_name = 'name'
116 if isinstance(url_call, tuple):
117 if len(url_call) == 2:
118 url_call, key = url_call[:]
119 else:
120 url_call, key, key_name = url_call[:]
121 json_res = json.loads(self._maas.get(url_call).read())
122 if key:
123 extra[name] = {v[key_name]: v[key] for v in json_res}
124 else:
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +0200125 extra[name] = {v[key_name]: v for v in json_res}
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200126 if self._all_elements_url:
127 all_elements = {}
128 elements = self._maas.get(self._all_elements_url).read()
129 res_json = json.loads(elements)
130 for element in res_json:
131 if isinstance(element, (str, unicode)):
132 all_elements[element] = {}
133 else:
134 all_elements[element[self._element_key]] = element
135 else:
136 all_elements = {}
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200137
138 def process_single(name, config_data):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200139 self._update = False
140 try:
141 data = self.fill_data(name, config_data, **extra)
142 if data is None:
143 ret['updated'].append(name)
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200144 return
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200145 if name in all_elements:
146 self._update = True
147 data = self.update(data, all_elements[name])
148 self.send(data)
149 ret['updated'].append(name)
150 else:
151 self.send(data)
152 ret['success'].append(name)
153 except urllib2.HTTPError as e:
154 etxt = e.read()
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200155 LOG.error('Failed for object %s reason %s', name, etxt)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200156 ret['errors'][name] = str(etxt)
157 except Exception as e:
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200158 LOG.error('Failed for object %s reason %s', name, e)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200159 ret['errors'][name] = str(e)
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200160 if objects_name is not None:
161 if ',' in objects_name:
162 objects_name = objects_name.split(',')
163 else:
164 objects_name = [objects_name]
165 for object_name in objects_name:
166 process_single(object_name, config[object_name])
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200167 else:
168 for name, config_data in config.iteritems():
169 process_single(name, config_data)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200170 except Exception as e:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200171 LOG.exception('Error Global')
172 raise
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100173 if ret['errors']:
174 raise Exception(ret)
175 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100176
177
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100178class Fabric(MaasObject):
179 def __init__(self):
180 super(Fabric, self).__init__()
181 self._all_elements_url = u'api/2.0/fabrics/'
182 self._create_url = u'api/2.0/fabrics/'
183 self._update_url = u'api/2.0/fabrics/{0}/'
184 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100185
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100186 def fill_data(self, name, fabric):
187 data = {
188 'name': name,
189 'description': fabric.get('description', ''),
190 }
191 if 'class_type' in fabric:
192 data['class_type'] = fabric.get('class_type'),
193 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100194
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100195 def update(self, new, old):
196 new['id'] = str(old['id'])
197 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100198
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200199
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100200class Subnet(MaasObject):
201 def __init__(self):
202 super(Subnet, self).__init__()
203 self._all_elements_url = u'api/2.0/subnets/'
204 self._create_url = u'api/2.0/subnets/'
205 self._update_url = u'api/2.0/subnets/{0}/'
206 self._config_path = 'region.subnets'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200207 self._extra_data_urls = {'fabrics': u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100208
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100209 def fill_data(self, name, subnet, fabrics):
210 data = {
211 'name': name,
212 'fabric': str(fabrics[subnet.get('fabric', '')]),
213 'cidr': subnet.get('cidr'),
214 'gateway_ip': subnet['gateway_ip'],
215 }
216 self._iprange = subnet['iprange']
217 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100218
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100219 def update(self, new, old):
220 new['id'] = str(old['id'])
221 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100222
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100223 def send(self, data):
224 response = super(Subnet, self).send(data)
225 res_json = json.loads(response)
226 self._process_iprange(res_json['id'])
227 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100228
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100229 def _process_iprange(self, subnet_id):
230 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
231 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
232 update = False
233 old_data = None
234 for iprange in ipranges:
235 if iprange['subnet']['id'] == subnet_id:
236 update = True
237 old_data = iprange
238 break
239 data = {
240 'start_ip': self._iprange.get('start'),
241 'end_ip': self._iprange.get('end'),
242 'subnet': str(subnet_id),
243 'type': self._iprange.get('type', 'dynamic')
244 }
245 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
246 LOG.info('iprange %s', _format_data(data))
247 if update:
248 LOG.warn('UPDATING %s %s', data, old_data)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200249 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']),
250 **data)
smolaonc3385f82016-03-11 19:01:24 +0100251 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100252 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100253
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200254
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100255class DHCPSnippet(MaasObject):
256 def __init__(self):
257 super(DHCPSnippet, self).__init__()
258 self._all_elements_url = u'api/2.0/dhcp-snippets/'
259 self._create_url = u'api/2.0/dhcp-snippets/'
260 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
261 self._config_path = 'region.dhcp_snippets'
262 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100263
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100264 def fill_data(self, name, snippet, subnets):
265 data = {
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200266 'name': name,
267 'value': snippet['value'],
268 'description': snippet['description'],
269 'enabled': str(snippet['enabled'] and 1 or 0),
270 'subnet': str(subnets[snippet['subnet']]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100271 }
272 return data
smolaonc3385f82016-03-11 19:01:24 +0100273
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100274 def update(self, new, old):
275 new['id'] = str(old['id'])
276 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100277
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200278
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100279class PacketRepository(MaasObject):
280 def __init__(self):
281 super(PacketRepository, self).__init__()
282 self._all_elements_url = u'api/2.0/package-repositories/'
283 self._create_url = u'api/2.0/package-repositories/'
284 self._update_url = u'api/2.0/package-repositories/{0}/'
285 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100286
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100287 def fill_data(self, name, package_repository):
288 data = {
289 'name': name,
290 'url': package_repository['url'],
291 'distributions': package_repository['distributions'],
292 'components': package_repository['components'],
293 'arches': package_repository['arches'],
294 'key': package_repository['key'],
295 'enabled': str(package_repository['enabled'] and 1 or 0),
296 }
297 if 'disabled_pockets' in package_repository:
298 data['disabled_pockets'] = package_repository['disable_pockets'],
299 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100300
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100301 def update(self, new, old):
302 new['id'] = str(old['id'])
303 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100304
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200305
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100306class Device(MaasObject):
307 def __init__(self):
308 super(Device, self).__init__()
309 self._all_elements_url = u'api/2.0/devices/'
310 self._create_url = u'api/2.0/devices/'
311 self._update_url = u'api/2.0/devices/{0}/'
312 self._config_path = 'region.devices'
313 self._element_key = 'hostname'
314 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100315
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100316 def fill_data(self, name, device_data):
317 data = {
318 'mac_addresses': device_data['mac'],
319 'hostname': name,
320 }
321 self._interface = device_data['interface']
322 return data
323
324 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100325 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
326 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100327 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100328 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100329 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
330 else:
331 new[self._update_key] = str(old[self._update_key])
332 return new
333
334 def send(self, data):
335 response = super(Device, self).send(data)
336 resp_json = json.loads(response)
337 system_id = resp_json['system_id']
338 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100339 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100340 return response
341
342 def _link_interface(self, system_id, interface_id):
343 data = {
344 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100345 'subnet': self._interface['subnet'],
346 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100347 }
348 if 'default_gateway' in self._interface:
349 data['default_gateway'] = self._interface.get('default_gateway')
350 if self._update:
351 data['force'] = '1'
352 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200353 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100354 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
355 .format(system_id, interface_id), 'link_subnet',
356 **data)
357
358
359class Machine(MaasObject):
360 def __init__(self):
361 super(Machine, self).__init__()
362 self._all_elements_url = u'api/2.0/machines/'
363 self._create_url = u'api/2.0/machines/'
364 self._update_url = u'api/2.0/machines/{0}/'
365 self._config_path = 'region.machines'
366 self._element_key = 'hostname'
367 self._update_key = 'system_id'
368
369 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100370 power_data = machine_data['power_parameters']
371 data = {
372 'hostname': name,
373 'architecture': machine_data.get('architecture', 'amd64/generic'),
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200374 'mac_addresses': machine_data['interface']['mac'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100375 'power_type': machine_data.get('power_type', 'ipmi'),
376 'power_parameters_power_address': power_data['power_address'],
377 }
Ondrej Smola455003c2017-06-01 22:53:39 +0200378 if 'power_driver' in power_data:
379 data['power_parameters_power_driver'] = power_data['power_driver']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100380 if 'power_user' in power_data:
381 data['power_parameters_power_user'] = power_data['power_user']
382 if 'power_password' in power_data:
383 data['power_parameters_power_pass'] = \
384 power_data['power_password']
Petr Ruzicka5fe96742017-11-10 14:22:24 +0100385 if 'power_id' in power_data:
386 data['power_parameters_power_id'] = power_data['power_id']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100387 return data
388
389 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100390 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
391 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100392 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100393 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200394 self._maas.delete(u'api/2.0/machines/{0}/'
395 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100396 else:
397 new[self._update_key] = str(old[self._update_key])
398 return new
399
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200400
401class AssignMachinesIP(MaasObject):
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200402 READY = 4
Andreyef156992017-07-03 14:54:03 -0500403 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200404
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200405 def __init__(self):
406 super(AssignMachinesIP, self).__init__()
407 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200408 self._create_url = \
409 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
410 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200411 self._config_path = 'region.machines'
412 self._element_key = 'hostname'
413 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200414 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
415 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200416
417 def fill_data(self, name, data, machines):
418 interface = data['interface']
419 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500420 if machine['status'] == self.DEPLOYED:
421 return
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200422 if machine['status'] != self.READY:
azvyagintsev7605a662017-11-03 19:05:04 +0200423 raise Exception('Machine:{} not in READY state'.format(name))
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200424 if 'ip' not in interface:
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100425 return
426 data = {
427 'mode': 'STATIC',
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200428 'subnet': str(interface.get('subnet')),
429 'ip_address': str(interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100430 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200431 if 'default_gateway' in interface:
432 data['default_gateway'] = interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200433 data['force'] = '1'
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200434 data['system_id'] = str(machine['system_id'])
435 data['interface_id'] = str(machine['interface_set'][0]['id'])
436 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100437
438
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200439class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200440 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200441 READY = 4
Andreyef156992017-07-03 14:54:03 -0500442 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200443
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200444 def __init__(self):
445 super(DeployMachines, self).__init__()
446 self._all_elements_url = None
447 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
448 self._config_path = 'region.machines'
449 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200450 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
451 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200452
453 def fill_data(self, name, machine_data, machines):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200454 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500455 if machine['status'] == self.DEPLOYED:
456 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200457 if machine['status'] != self.READY:
458 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200459 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200460 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200461 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200462 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200463 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200464 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200465 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200466 return data
467
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200468 def send(self, data):
469 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200470 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200471 return self._maas.post(self._create_url[0].format(**data),
472 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200473
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100474class BootResource(MaasObject):
475 def __init__(self):
476 super(BootResource, self).__init__()
477 self._all_elements_url = u'api/2.0/boot-resources/'
478 self._create_url = u'api/2.0/boot-resources/'
479 self._update_url = u'api/2.0/boot-resources/{0}/'
480 self._config_path = 'region.boot_resources'
481
482 def fill_data(self, name, boot_data):
483 sha256 = hashlib.sha256()
484 sha256.update(file(boot_data['content']).read())
485 data = {
486 'name': name,
487 'title': boot_data['title'],
488 'architecture': boot_data['architecture'],
489 'filetype': boot_data['filetype'],
490 'size': str(os.path.getsize(boot_data['content'])),
491 'sha256': sha256.hexdigest(),
492 'content': io.open(boot_data['content']),
493 }
494 return data
495
496 def update(self, new, old):
497 self._update = False
498 return new
499
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200500
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100501class CommissioningScripts(MaasObject):
502 def __init__(self):
503 super(CommissioningScripts, self).__init__()
504 self._all_elements_url = u'api/2.0/commissioning-scripts/'
505 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100506 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100507 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100508 self._update_key = 'name'
509
510 def fill_data(self, name, file_path):
511 data = {
512 'name': name,
513 'content': io.open(file_path),
514 }
515 return data
516
517 def update(self, new, old):
518 return new
519
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200520
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100521class MaasConfig(MaasObject):
522 def __init__(self):
523 super(MaasConfig, self).__init__()
524 self._all_elements_url = None
525 self._create_url = (u'api/2.0/maas/', u'set_config')
526 self._config_path = 'region.maas_config'
527
528 def fill_data(self, name, value):
529 data = {
530 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100531 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100532 }
533 return data
534
535 def update(self, new, old):
536 self._update = False
537 return new
538
539
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200540class SSHPrefs(MaasObject):
541 def __init__(self):
542 super(SSHPrefs, self).__init__()
543 self._all_elements_url = None
544 self._create_url = u'api/2.0/account/prefs/sshkeys/'
545 self._config_path = 'region.sshprefs'
546 self._element_key = 'hostname'
547 self._update_key = 'system_id'
548
549 def fill_data(self, value):
550 data = {
551 'key': value,
552 }
553 return data
554
555 def process(self):
556 config = __salt__['config.get']('maas')
557 for part in self._config_path.split('.'):
558 config = config.get(part, {})
559 extra = {}
560 for name, url_call in self._extra_data_urls.iteritems():
561 key = 'id'
562 if isinstance(url_call, tuple):
563 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200564 json_res = json.loads(self._maas.get(url_call).read())
565 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200566 if self._all_elements_url:
567 all_elements = {}
568 elements = self._maas.get(self._all_elements_url).read()
569 res_json = json.loads(elements)
570 for element in res_json:
571 if isinstance(element, (str, unicode)):
572 all_elements[element] = {}
573 else:
574 all_elements[element[self._element_key]] = element
575 else:
576 all_elements = {}
577 ret = {
578 'success': [],
579 'errors': {},
580 'updated': [],
581 }
582 for config_data in config:
583 name = config_data[:10]
584 try:
585 data = self.fill_data(config_data, **extra)
586 self.send(data)
587 ret['success'].append(name)
588 except urllib2.HTTPError as e:
589 etxt = e.read()
590 LOG.exception('Failed for object %s reason %s', name, etxt)
591 ret['errors'][name] = str(etxt)
592 except Exception as e:
593 LOG.exception('Failed for object %s reason %s', name, e)
594 ret['errors'][name] = str(e)
595 if ret['errors']:
596 raise Exception(ret)
597 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200598
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200599
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200600class Domain(MaasObject):
601 def __init__(self):
602 super(Domain, self).__init__()
603 self._all_elements_url = u'/api/2.0/domains/'
604 self._create_url = u'/api/2.0/domains/'
605 self._config_path = 'region.domain'
606 self._update_url = u'/api/2.0/domains/{0}/'
607
608 def fill_data(self, value):
609 data = {
610 'name': value,
611 }
612 self._update = True
613 return data
614
615 def update(self, new, old):
616 new['id'] = str(old['id'])
617 new['authoritative'] = str(old['authoritative'])
618 return new
619
620 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200621 ret = {
622 'success': [],
623 'errors': {},
624 'updated': [],
625 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200626 config = __salt__['config.get']('maas')
627 for part in self._config_path.split('.'):
628 config = config.get(part, {})
629 extra = {}
630 for name, url_call in self._extra_data_urls.iteritems():
631 key = 'id'
632 if isinstance(url_call, tuple):
633 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200634 json_res = json.loads(self._maas.get(url_call).read())
635 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200636 if self._all_elements_url:
637 all_elements = {}
638 elements = self._maas.get(self._all_elements_url).read()
639 res_json = json.loads(elements)
640 for element in res_json:
641 if isinstance(element, (str, unicode)):
642 all_elements[element] = {}
643 else:
644 all_elements[element[self._element_key]] = element
645 else:
646 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200647 try:
648 data = self.fill_data(config, **extra)
649 data = self.update(data, all_elements.values()[0])
650 self.send(data)
651 ret['success'].append('domain')
652 except urllib2.HTTPError as e:
653 etxt = e.read()
654 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
655 ret['errors']['domain'] = str(etxt)
656 except Exception as e:
657 LOG.exception('Failed for object %s reason %s', 'domain', e)
658 ret['errors']['domain'] = str(e)
659 if ret['errors']:
660 raise Exception(ret)
661 return ret
662
663
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200664class MachinesStatus(MaasObject):
665 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200666 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200667 cls._maas = _create_maas_client()
668 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200669 json_result = json.loads(result.read())
670 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200671 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200672 if objects_name:
673 if ',' in objects_name:
674 objects_name = set(objects_name.split(','))
675 else:
676 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200677 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200678 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200679 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400680 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200681 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200682 res.append(
683 {'hostname': machine['hostname'],
684 'system_id': machine['system_id'],
685 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200686 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200687
azvyagintsev7605a662017-11-03 19:05:04 +0200688 @classmethod
689 def wait_for_machine_status(cls, **kwargs):
690 """
691 A function that wait for any requested status, for any set of maas
692 machines.
693
694 If no kwargs has been passed - will try to wait ALL
695 defined in salt::maas::region::machines
696
697 See readme file for more examples.
698 CLI Example:
699 .. code-block:: bash
700
701 salt-call state.apply maas.machines.wait_for_deployed
702
703 :param kwargs:
704 timeout: in s; Global timeout for wait
705 poll_time: in s;Sleep time, between retry
706 req_status: string; Polling status
707 machines: list; machine names
708 ignore_machines: list; machine names
709 :ret: True
710 Exception - if something fail/timeout reached
711 """
712 timeout = kwargs.get("timeout", 60 * 120)
713 poll_time = kwargs.get("poll_time", 30)
714 req_status = kwargs.get("req_status", "Ready")
715 to_discover = kwargs.get("machines", None)
716 ignore_machines = kwargs.get("ignore_machines", None)
717 if not to_discover:
718 try:
719 to_discover = __salt__['config.get']('maas')['region'][
720 'machines'].keys()
721 except KeyError:
722 LOG.warning("No defined machines!")
723 return True
724 total = copy.deepcopy(to_discover) or []
725 if ignore_machines and total:
726 total = [x for x in to_discover if x not in ignore_machines]
727 started_at = time.time()
728 while len(total) <= len(to_discover):
729 for m in to_discover:
730 for discovered in MachinesStatus.execute()['machines']:
731 if m == discovered['hostname'] and \
Michael Polenchuke438bd32017-11-09 20:42:42 +0400732 discovered['status'].lower() == req_status.lower():
733 if m in total:
734 total.remove(m)
735
azvyagintsev7605a662017-11-03 19:05:04 +0200736 if len(total) <= 0:
737 LOG.debug(
738 "Machines:{} are:{}".format(to_discover, req_status))
739 return True
740 if (timeout - (time.time() - started_at)) <= 0:
741 raise Exception(
742 'Machines:{}not in {} state'.format(total, req_status))
743 LOG.info(
744 "Waiting status:{} "
745 "for machines:{}"
746 "\nsleep for:{}s "
747 "Timeout:{}s".format(req_status, total, poll_time, timeout))
748 time.sleep(poll_time)
749
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200750
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100751def process_fabrics():
752 return Fabric().process()
753
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200754
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100755def process_subnets():
756 return Subnet().process()
757
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200758
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100759def process_dhcp_snippets():
760 return DHCPSnippet().process()
761
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200762
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100763def process_package_repositories():
764 return PacketRepository().process()
765
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200766
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +0200767def process_devices(*args):
768 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100769
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200770
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200771def process_machines(*args):
772 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100773
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200774
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200775def process_assign_machines_ip(*args):
776 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200777
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200778
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200779def machines_status(*args):
780 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200781
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200782
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200783def deploy_machines(*args):
784 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200785
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200786
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100787def process_boot_resources():
788 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100789
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200790
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100791def process_maas_config():
792 return MaasConfig().process()
793
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200794
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100795def process_commissioning_scripts():
796 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200797
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200798
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200799def process_domain():
800 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200801
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200802
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200803def process_sshprefs():
804 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +0200805
806
807def wait_for_machine_status(**kwargs):
808 return MachinesStatus.wait_for_machine_status(**kwargs)