blob: 150fd1bdfcff293faa826c8cebab516d9c5e80ce [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 = {
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100104 'skipped': [],
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200105 'success': [],
106 'errors': {},
107 'updated': [],
108 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200109 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200110 config = __salt__['config.get']('maas')
111 for part in self._config_path.split('.'):
112 config = config.get(part, {})
113 extra = {}
114 for name, url_call in self._extra_data_urls.iteritems():
115 key = 'id'
116 key_name = 'name'
117 if isinstance(url_call, tuple):
118 if len(url_call) == 2:
119 url_call, key = url_call[:]
120 else:
121 url_call, key, key_name = url_call[:]
122 json_res = json.loads(self._maas.get(url_call).read())
123 if key:
124 extra[name] = {v[key_name]: v[key] for v in json_res}
125 else:
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +0200126 extra[name] = {v[key_name]: v for v in json_res}
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200127 if self._all_elements_url:
128 all_elements = {}
129 elements = self._maas.get(self._all_elements_url).read()
130 res_json = json.loads(elements)
131 for element in res_json:
132 if isinstance(element, (str, unicode)):
133 all_elements[element] = {}
134 else:
135 all_elements[element[self._element_key]] = element
136 else:
137 all_elements = {}
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200138
139 def process_single(name, config_data):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200140 self._update = False
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100141 err_msgs = []
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200142 try:
143 data = self.fill_data(name, config_data, **extra)
144 if data is None:
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100145 ret['skipped'].append(name)
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200146 return
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200147 if name in all_elements:
148 self._update = True
149 data = self.update(data, all_elements[name])
150 self.send(data)
151 ret['updated'].append(name)
152 else:
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100153 # Check for duplicate mac_addresses
154 skip_send = False
155 for _obj_name, _obj_val in all_elements.iteritems():
156 if isinstance(_obj_val, dict) and 'boot_interface' in _obj_val:
157 if isinstance(data, dict) and isinstance(data['mac_addresses'], (unicode, str)):
158 for mac_addr in data['mac_addresses'].split(','):
159 if mac_addr == _obj_val['boot_interface']['mac_address']:
160 err_msg = 'Error: %s uses same mac [%s] as node %s in maas' % (data['hostname'], mac_addr, _obj_val['fqdn'])
161 err_msgs.append(err_msg)
162 LOG.error(err_msg)
163 skip_send = True
164 # Decision
165 if skip_send:
166 ret['errors'][name] = ','.join(err_msgs)
167 else:
168 self.send(data)
169 ret['success'].append(name)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200170 except urllib2.HTTPError as e:
171 etxt = e.read()
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200172 LOG.error('Failed for object %s reason %s', name, etxt)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200173 ret['errors'][name] = str(etxt)
174 except Exception as e:
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200175 LOG.error('Failed for object %s reason %s', name, e)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200176 ret['errors'][name] = str(e)
Dzmitry Stremkouski4394bd92020-03-14 15:58:21 +0100177
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200178 if objects_name is not None:
179 if ',' in objects_name:
180 objects_name = objects_name.split(',')
181 else:
182 objects_name = [objects_name]
183 for object_name in objects_name:
184 process_single(object_name, config[object_name])
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200185 else:
186 for name, config_data in config.iteritems():
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100187 if isinstance(config_data, dict) and isinstance(all_elements, dict) and name in all_elements and 'status_name' in all_elements[name] and all_elements[name]['status_name'] == 'Deployed':
Dzmitry Stremkouski4394bd92020-03-14 15:58:21 +0100188 LOG.info('Machine %s already deployed, skipping it.' % name)
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100189 ret['skipped'].append(name)
Dzmitry Stremkouski4394bd92020-03-14 15:58:21 +0100190 else:
191 process_single(name, config_data)
192
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200193 except Exception as e:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200194 LOG.exception('Error Global')
195 raise
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100196 if ret['errors']:
Jiri Broulike30a60f2018-04-09 21:15:10 +0200197 if 'already exists' in str(ret['errors']):
198 ret['success'] = ret['errors']
199 ret['errors'] = {}
200 else:
201 raise Exception(ret)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100202 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100203
204
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100205class Fabric(MaasObject):
206 def __init__(self):
207 super(Fabric, self).__init__()
208 self._all_elements_url = u'api/2.0/fabrics/'
209 self._create_url = u'api/2.0/fabrics/'
210 self._update_url = u'api/2.0/fabrics/{0}/'
211 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100212
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100213 def fill_data(self, name, fabric):
214 data = {
215 'name': name,
216 'description': fabric.get('description', ''),
217 }
218 if 'class_type' in fabric:
219 data['class_type'] = fabric.get('class_type'),
220 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100221
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100222 def update(self, new, old):
223 new['id'] = str(old['id'])
224 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100225
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200226
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100227class Subnet(MaasObject):
228 def __init__(self):
229 super(Subnet, self).__init__()
230 self._all_elements_url = u'api/2.0/subnets/'
231 self._create_url = u'api/2.0/subnets/'
232 self._update_url = u'api/2.0/subnets/{0}/'
233 self._config_path = 'region.subnets'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200234 self._extra_data_urls = {'fabrics': u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100235
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100236 def fill_data(self, name, subnet, fabrics):
237 data = {
238 'name': name,
Alexandru Avadanii652e7552017-08-19 02:03:01 +0200239 'fabric': str(fabrics[subnet.get('fabric',
240 self._get_fabric_from_cidr(subnet.get('cidr')))]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100241 'cidr': subnet.get('cidr'),
242 'gateway_ip': subnet['gateway_ip'],
243 }
244 self._iprange = subnet['iprange']
245 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100246
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100247 def update(self, new, old):
248 new['id'] = str(old['id'])
249 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100250
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100251 def send(self, data):
252 response = super(Subnet, self).send(data)
253 res_json = json.loads(response)
254 self._process_iprange(res_json['id'])
255 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100256
Alexandru Avadanii652e7552017-08-19 02:03:01 +0200257 def _get_fabric_from_cidr(self, cidr):
258 subnets = json.loads(self._maas.get(u'api/2.0/subnets/').read())
259 for subnet in subnets:
260 if subnet['cidr'] == cidr:
261 return subnet['vlan']['fabric']
262 return ''
263
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100264 def _process_iprange(self, subnet_id):
265 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
266 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
267 update = False
268 old_data = None
269 for iprange in ipranges:
270 if iprange['subnet']['id'] == subnet_id:
271 update = True
272 old_data = iprange
273 break
274 data = {
275 'start_ip': self._iprange.get('start'),
276 'end_ip': self._iprange.get('end'),
277 'subnet': str(subnet_id),
278 'type': self._iprange.get('type', 'dynamic')
279 }
280 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
281 LOG.info('iprange %s', _format_data(data))
282 if update:
283 LOG.warn('UPDATING %s %s', data, old_data)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200284 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']),
285 **data)
smolaonc3385f82016-03-11 19:01:24 +0100286 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100287 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100288
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200289
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100290class DHCPSnippet(MaasObject):
291 def __init__(self):
292 super(DHCPSnippet, self).__init__()
293 self._all_elements_url = u'api/2.0/dhcp-snippets/'
294 self._create_url = u'api/2.0/dhcp-snippets/'
295 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
296 self._config_path = 'region.dhcp_snippets'
297 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100298
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100299 def fill_data(self, name, snippet, subnets):
300 data = {
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200301 'name': name,
302 'value': snippet['value'],
303 'description': snippet['description'],
304 'enabled': str(snippet['enabled'] and 1 or 0),
305 'subnet': str(subnets[snippet['subnet']]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100306 }
307 return data
smolaonc3385f82016-03-11 19:01:24 +0100308
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100309 def update(self, new, old):
310 new['id'] = str(old['id'])
311 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100312
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200313
Jiri Broulike30a60f2018-04-09 21:15:10 +0200314class Boot_source(MaasObject):
315 def __init__(self):
316 super(Boot_source, self).__init__()
317 self._all_elements_url = u'api/2.0/boot-sources/'
318 self._create_url = u'api/2.0/boot-sources/'
319 self._update_url = u'api/2.0/boot-sources/{0}/'
320 self._config_path = 'region.boot_sources'
321 self._element_key = 'id'
322
323 def fill_data(self, name, boot_source):
324 data = {
325 'name': name,
326 'url': boot_source.get('url', ''),
327 'keyring_filename': boot_source.get('keyring_file', ''),
328 }
329 return data
330
331 def update(self, new, old):
332 new['id'] = str(old['id'])
333 return new
334
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100335class PacketRepository(MaasObject):
336 def __init__(self):
337 super(PacketRepository, self).__init__()
338 self._all_elements_url = u'api/2.0/package-repositories/'
339 self._create_url = u'api/2.0/package-repositories/'
340 self._update_url = u'api/2.0/package-repositories/{0}/'
341 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100342
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100343 def fill_data(self, name, package_repository):
344 data = {
345 'name': name,
346 'url': package_repository['url'],
347 'distributions': package_repository['distributions'],
348 'components': package_repository['components'],
349 'arches': package_repository['arches'],
350 'key': package_repository['key'],
351 'enabled': str(package_repository['enabled'] and 1 or 0),
352 }
353 if 'disabled_pockets' in package_repository:
Taras Khlivnyak89a59c22021-03-18 08:26:58 +0200354 data['disabled_pockets'] = package_repository['disabled_pockets'],
355 if 'disabled_components' in package_repository:
356 data['disabled_components'] = package_repository['disabled_components'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100357 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100358
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100359 def update(self, new, old):
360 new['id'] = str(old['id'])
361 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100362
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200363
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100364class Device(MaasObject):
365 def __init__(self):
366 super(Device, self).__init__()
367 self._all_elements_url = u'api/2.0/devices/'
368 self._create_url = u'api/2.0/devices/'
369 self._update_url = u'api/2.0/devices/{0}/'
370 self._config_path = 'region.devices'
371 self._element_key = 'hostname'
372 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100373
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100374 def fill_data(self, name, device_data):
375 data = {
376 'mac_addresses': device_data['mac'],
377 'hostname': name,
378 }
379 self._interface = device_data['interface']
380 return data
381
382 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100383 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
384 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100385 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100386 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100387 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
388 else:
389 new[self._update_key] = str(old[self._update_key])
390 return new
391
392 def send(self, data):
393 response = super(Device, self).send(data)
394 resp_json = json.loads(response)
395 system_id = resp_json['system_id']
396 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100397 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100398 return response
399
400 def _link_interface(self, system_id, interface_id):
401 data = {
402 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100403 'subnet': self._interface['subnet'],
404 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100405 }
406 if 'default_gateway' in self._interface:
407 data['default_gateway'] = self._interface.get('default_gateway')
408 if self._update:
409 data['force'] = '1'
410 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200411 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100412 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
413 .format(system_id, interface_id), 'link_subnet',
414 **data)
415
416
417class Machine(MaasObject):
418 def __init__(self):
419 super(Machine, self).__init__()
420 self._all_elements_url = u'api/2.0/machines/'
421 self._create_url = u'api/2.0/machines/'
422 self._update_url = u'api/2.0/machines/{0}/'
423 self._config_path = 'region.machines'
424 self._element_key = 'hostname'
425 self._update_key = 'system_id'
426
427 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100428 power_data = machine_data['power_parameters']
azvyagintsev06b71e72017-11-08 17:11:07 +0200429 machine_pxe_mac = machine_data.get('pxe_interface_mac', None)
430 if machine_data.get("interface", None):
431 LOG.warning(
432 "Old machine-describe detected! "
433 "Please read documentation for "
434 "'salt-formulas/maas' for migration!")
435 machine_pxe_mac = machine_data['interface'].get('mac', None)
436 if not machine_pxe_mac:
437 raise Exception("PXE MAC for machine:{} not defined".format(name))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100438 data = {
439 'hostname': name,
440 'architecture': machine_data.get('architecture', 'amd64/generic'),
azvyagintsev06b71e72017-11-08 17:11:07 +0200441 'mac_addresses': machine_pxe_mac,
mkraynove95cdb62018-05-08 14:17:18 +0400442 'power_type': power_data.get('power_type', 'manual'),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100443 }
azvyagintsev6543ec82018-11-01 23:45:09 +0200444 for k, v in power_data.items():
Denis Egorenko60963332018-10-23 19:24:28 +0400445 if k == 'power_type':
446 continue
azvyagintsev6543ec82018-11-01 23:45:09 +0200447 elif k == 'power_password':
448 data['power_parameters_power_pass'] = v
449 LOG.warning(
450 "Deprecated power parameter:power_password passed! "
451 "Please change key name to 'power_pass'! ")
452 continue
453 elif k == 'power_nova_id':
454 data['power_parameters_nova_id'] = v
455 LOG.warning(
456 "Deprecated power parameter:power_nova_id passed! "
457 "Please change key name to 'nova_id'! ")
458 continue
459 elif k == 'power_os_tenantname':
460 data['power_parameters_os_tenantname'] = v
461 LOG.warning(
462 "Deprecated power parameter:power_os_tenantname passed! "
463 "Please change key name to 'os_tenantname'! ")
464 continue
465 elif k == 'power_os_username':
466 data['power_parameters_os_username'] = v
467 LOG.warning(
468 "Deprecated power parameter:power_os_username passed! "
469 "Please change key name to 'os_username'! ")
470 continue
471 elif k == 'power_os_password':
472 data['power_parameters_os_password'] = v
473 LOG.warning(
474 "Deprecated power parameter:power_os_password passed! "
475 "Please change key name to 'os_password'! ")
476 continue
477 elif k == 'power_os_authurl':
478 data['power_parameters_os_authurl'] = v
479 LOG.warning(
480 "Deprecated power parameter:power_os_authurl passed! "
481 "Please change key name to 'os_authurl'! ")
482 continue
483
Denis Egorenko60963332018-10-23 19:24:28 +0400484 data_key = 'power_parameters_{}'.format(k)
485 data[data_key] = v
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100486 return data
487
488 def update(self, new, old):
Gabor Toth36bc0282018-08-18 10:39:51 +0200489 LOG.debug('Updating machine')
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100490 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
Gabor Toth36bc0282018-08-18 10:39:51 +0200491 LOG.debug('old_macs: %s' % old_macs)
492 if isinstance(new['mac_addresses'], list):
493 new_macs = set(v.lower() for v in new['mac_addresses'])
494 else:
495 new_macs = set([new['mac_addresses'].lower()])
496 LOG.debug('new_macs: %s' % new_macs)
497 intersect = list(new_macs.intersection(old_macs))
498 if not intersect:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100499 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100500 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200501 self._maas.delete(u'api/2.0/machines/{0}/'
502 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100503 else:
Gabor Toth36bc0282018-08-18 10:39:51 +0200504 new['mac_addresses'] = intersect
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100505 new[self._update_key] = str(old[self._update_key])
506 return new
507
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200508
509class AssignMachinesIP(MaasObject):
azvyagintsev06b71e72017-11-08 17:11:07 +0200510 # FIXME
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200511 READY = 4
Andreyef156992017-07-03 14:54:03 -0500512 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200513
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200514 def __init__(self):
515 super(AssignMachinesIP, self).__init__()
516 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200517 self._create_url = \
518 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
519 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200520 self._config_path = 'region.machines'
521 self._element_key = 'hostname'
522 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200523 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
524 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200525
azvyagintsev06b71e72017-11-08 17:11:07 +0200526 def _data_old(self, _interface, _machine):
527 """
528 _interface = {
529 "mac": "11:22:33:44:55:77",
530 "mode": "STATIC",
531 "ip": "2.2.3.15",
532 "subnet": "subnet1",
533 "gateway": "2.2.3.2",
534 }
535 :param data:
536 :return:
537 """
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100538 data = {
539 'mode': 'STATIC',
azvyagintsev06b71e72017-11-08 17:11:07 +0200540 'subnet': str(_interface.get('subnet')),
541 'ip_address': str(_interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100542 }
azvyagintsev06b71e72017-11-08 17:11:07 +0200543 if 'gateway' in _interface:
544 data['default_gateway'] = _interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200545 data['force'] = '1'
azvyagintsev06b71e72017-11-08 17:11:07 +0200546 data['system_id'] = str(_machine['system_id'])
547 data['interface_id'] = str(_machine['interface_set'][0]['id'])
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200548 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100549
azvyagintsev06b71e72017-11-08 17:11:07 +0200550 def _get_nic_id_by_mac(self, machine, req_mac=None):
551 data = {}
552 for nic in machine['interface_set']:
Pavel Cizinsky55d9d762019-01-15 14:33:37 +0100553 data[nic['mac_address'].lower()] = nic['id']
azvyagintsev06b71e72017-11-08 17:11:07 +0200554 if req_mac:
Pavel Cizinsky55d9d762019-01-15 14:33:37 +0100555 if req_mac.lower() in data.keys():
556 return data[req_mac.lower()]
azvyagintsev06b71e72017-11-08 17:11:07 +0200557 else:
558 raise Exception('NIC with mac:{} not found at '
559 'node:{}'.format(req_mac, machine['fqdn']))
560 return data
561
562 def _disconnect_all_nic(self, machine):
563 """
564 Maas will fail, in case same config's will be to apply
565 on different interfaces. In same time - not possible to push
566 whole network schema at once. Before configuring - need to clean-up
567 everything
568 :param machine:
569 :return:
570 """
571 for nic in machine['interface_set']:
572 LOG.debug("Disconnecting interface:{}".format(nic['mac_address']))
573 try:
574 self._maas.post(
575 u'/api/2.0/nodes/{}/interfaces/{}/'.format(
576 machine['system_id'], nic['id']), 'disconnect')
577 except Exception as e:
578 LOG.error("Failed to disconnect interface:{} on node:{}".format(
579 nic['mac_address'], machine['fqdn']))
580 raise Exception(str(e))
581
582 def _process_interface(self, nic_data, machine):
583 """
584 Process exactly one interface:
585 - update interface
586 - link to network
587 These functions are self-complementary, and do not require an
588 external "process" method. Those broke old-MaasObject logic,
589 though make functions more simple in case iterable tasks.
590 """
591 nic_id = self._get_nic_id_by_mac(machine, nic_data['mac'])
592
593 # Process op=link_subnet
594 link_data = {}
595 _mode = nic_data.get('mode', 'AUTO').upper()
596 if _mode == 'STATIC':
597 link_data = {
598 'mode': 'STATIC',
599 'subnet': str(nic_data.get('subnet')),
600 'ip_address': str(nic_data.get('ip')),
601 'default_gateway': str(nic_data.get('gateway', "")),
602 }
603 elif _mode == 'DHCP':
604 link_data = {
605 'mode': 'DHCP',
606 'subnet': str(nic_data.get('subnet')),
607 }
608 elif _mode == 'AUTO':
609 link_data = {'mode': 'AUTO',
610 'default_gateway': str(nic_data.get('gateway', "")), }
611 elif _mode in ('LINK_UP', 'UNCONFIGURED'):
612 link_data = {'mode': 'LINK_UP'}
613 else:
614 raise Exception('Wrong IP mode:{}'
615 ' for node:{}'.format(_mode, machine['fqdn']))
616 link_data['force'] = str(1)
617
618 physical_data = {"name": nic_data.get("name", ""),
619 "tags": nic_data.get('tags', ""),
620 "vlan": nic_data.get('vlan', "")}
621
622 try:
623 # Cleanup-old definition
624 LOG.debug("Processing {}:{},{}".format(nic_data['mac'], link_data,
625 physical_data))
626 # "link_subnet" and "fill all other data" - its 2 different
627 # operations. So, first we update NIC:
628 self._maas.put(
629 u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
630 nic_id),
631 **physical_data)
632 # And then, link subnet configuration:
633 self._maas.post(
634 u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
635 nic_id),
636 'link_subnet', **link_data)
637 except Exception as e:
638 LOG.error("Failed to process interface:{} on node:{}".format(
639 nic_data['mac'], machine['fqdn']))
640 raise Exception(str(e))
641
642 def fill_data(self, name, data, machines):
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100643 if name in machines:
644 machine = machines[name]
645 else:
646 LOG.debug("Skipping node:{} "
647 "since it is not in maas yet".format(name))
648 return
azvyagintsev06b71e72017-11-08 17:11:07 +0200649 if machine['status'] == self.DEPLOYED:
650 LOG.debug("Skipping node:{} "
651 "since it in status:DEPLOYED".format(name))
652 return
653 if machine['status'] != self.READY:
654 raise Exception('Machine:{} not in status:READY'.format(name))
655 # backward comparability, for old schema
656 if data.get("interface", None):
657 if 'ip' not in data["interface"]:
658 LOG.info("No IP NIC definition for:{}".format(name))
659 return
660 LOG.warning(
661 "Old machine-describe detected! "
662 "Please read documentation "
663 "'salt-formulas/maas' for migration!")
664 return self._data_old(data['interface'], machines[name])
665 # NewSchema processing:
666 # Warning: old-style MaasObject.process still be called, but
667 # with empty data for process.
668 interfaces = data.get('interfaces', {})
669 if len(interfaces.keys()) == 0:
670 LOG.info("No IP NIC definition for:{}".format(name))
671 return
672 LOG.info('%s for %s', self.__class__.__name__.lower(),
673 machine['fqdn'])
674 self._disconnect_all_nic(machine)
675 for key, value in sorted(interfaces.iteritems()):
676 self._process_interface(value, machine)
677
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100678
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200679class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200680 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200681 READY = 4
Andreyef156992017-07-03 14:54:03 -0500682 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200683
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200684 def __init__(self):
685 super(DeployMachines, self).__init__()
686 self._all_elements_url = None
687 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
688 self._config_path = 'region.machines'
689 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200690 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
691 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200692
693 def fill_data(self, name, machine_data, machines):
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100694 if name in machines:
695 machine = machines[name]
696 else:
697 LOG.debug("Skipping node:{} "
698 "since it is not in maas yet".format(name))
699 return
Andreyef156992017-07-03 14:54:03 -0500700 if machine['status'] == self.DEPLOYED:
701 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200702 if machine['status'] != self.READY:
703 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200704 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200705 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200706 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200707 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200708 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200709 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200710 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200711 return data
712
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200713 def send(self, data):
714 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200715 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200716 return self._maas.post(self._create_url[0].format(**data),
717 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200718
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100719class BootResource(MaasObject):
720 def __init__(self):
721 super(BootResource, self).__init__()
722 self._all_elements_url = u'api/2.0/boot-resources/'
723 self._create_url = u'api/2.0/boot-resources/'
724 self._update_url = u'api/2.0/boot-resources/{0}/'
725 self._config_path = 'region.boot_resources'
726
727 def fill_data(self, name, boot_data):
728 sha256 = hashlib.sha256()
729 sha256.update(file(boot_data['content']).read())
730 data = {
731 'name': name,
732 'title': boot_data['title'],
733 'architecture': boot_data['architecture'],
734 'filetype': boot_data['filetype'],
735 'size': str(os.path.getsize(boot_data['content'])),
736 'sha256': sha256.hexdigest(),
737 'content': io.open(boot_data['content']),
738 }
739 return data
740
741 def update(self, new, old):
742 self._update = False
743 return new
744
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200745
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100746class CommissioningScripts(MaasObject):
747 def __init__(self):
748 super(CommissioningScripts, self).__init__()
749 self._all_elements_url = u'api/2.0/commissioning-scripts/'
750 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100751 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100752 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100753 self._update_key = 'name'
754
755 def fill_data(self, name, file_path):
756 data = {
757 'name': name,
758 'content': io.open(file_path),
759 }
760 return data
761
762 def update(self, new, old):
763 return new
764
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200765
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100766class MaasConfig(MaasObject):
767 def __init__(self):
768 super(MaasConfig, self).__init__()
769 self._all_elements_url = None
770 self._create_url = (u'api/2.0/maas/', u'set_config')
771 self._config_path = 'region.maas_config'
772
773 def fill_data(self, name, value):
774 data = {
775 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100776 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100777 }
778 return data
779
780 def update(self, new, old):
781 self._update = False
782 return new
783
784
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200785class SSHPrefs(MaasObject):
786 def __init__(self):
787 super(SSHPrefs, self).__init__()
788 self._all_elements_url = None
789 self._create_url = u'api/2.0/account/prefs/sshkeys/'
790 self._config_path = 'region.sshprefs'
791 self._element_key = 'hostname'
792 self._update_key = 'system_id'
793
794 def fill_data(self, value):
795 data = {
796 'key': value,
797 }
798 return data
799
800 def process(self):
801 config = __salt__['config.get']('maas')
802 for part in self._config_path.split('.'):
803 config = config.get(part, {})
804 extra = {}
805 for name, url_call in self._extra_data_urls.iteritems():
806 key = 'id'
807 if isinstance(url_call, tuple):
808 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200809 json_res = json.loads(self._maas.get(url_call).read())
810 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200811 if self._all_elements_url:
812 all_elements = {}
813 elements = self._maas.get(self._all_elements_url).read()
814 res_json = json.loads(elements)
815 for element in res_json:
816 if isinstance(element, (str, unicode)):
817 all_elements[element] = {}
818 else:
819 all_elements[element[self._element_key]] = element
820 else:
821 all_elements = {}
822 ret = {
823 'success': [],
824 'errors': {},
825 'updated': [],
826 }
827 for config_data in config:
828 name = config_data[:10]
829 try:
830 data = self.fill_data(config_data, **extra)
831 self.send(data)
832 ret['success'].append(name)
833 except urllib2.HTTPError as e:
834 etxt = e.read()
835 LOG.exception('Failed for object %s reason %s', name, etxt)
836 ret['errors'][name] = str(etxt)
837 except Exception as e:
838 LOG.exception('Failed for object %s reason %s', name, e)
839 ret['errors'][name] = str(e)
840 if ret['errors']:
841 raise Exception(ret)
842 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200843
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200844
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200845class Domain(MaasObject):
846 def __init__(self):
847 super(Domain, self).__init__()
848 self._all_elements_url = u'/api/2.0/domains/'
849 self._create_url = u'/api/2.0/domains/'
850 self._config_path = 'region.domain'
851 self._update_url = u'/api/2.0/domains/{0}/'
852
853 def fill_data(self, value):
854 data = {
855 'name': value,
856 }
857 self._update = True
858 return data
859
860 def update(self, new, old):
861 new['id'] = str(old['id'])
862 new['authoritative'] = str(old['authoritative'])
863 return new
864
865 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200866 ret = {
867 'success': [],
868 'errors': {},
869 'updated': [],
870 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200871 config = __salt__['config.get']('maas')
872 for part in self._config_path.split('.'):
873 config = config.get(part, {})
874 extra = {}
875 for name, url_call in self._extra_data_urls.iteritems():
876 key = 'id'
877 if isinstance(url_call, tuple):
878 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200879 json_res = json.loads(self._maas.get(url_call).read())
880 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200881 if self._all_elements_url:
882 all_elements = {}
883 elements = self._maas.get(self._all_elements_url).read()
884 res_json = json.loads(elements)
885 for element in res_json:
886 if isinstance(element, (str, unicode)):
887 all_elements[element] = {}
888 else:
889 all_elements[element[self._element_key]] = element
890 else:
891 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200892 try:
893 data = self.fill_data(config, **extra)
894 data = self.update(data, all_elements.values()[0])
895 self.send(data)
896 ret['success'].append('domain')
897 except urllib2.HTTPError as e:
898 etxt = e.read()
899 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
900 ret['errors']['domain'] = str(etxt)
901 except Exception as e:
902 LOG.exception('Failed for object %s reason %s', 'domain', e)
903 ret['errors']['domain'] = str(e)
904 if ret['errors']:
905 raise Exception(ret)
906 return ret
907
908
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200909class MachinesStatus(MaasObject):
910 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200911 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200912 cls._maas = _create_maas_client()
913 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200914 json_result = json.loads(result.read())
915 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200916 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200917 if objects_name:
918 if ',' in objects_name:
919 objects_name = set(objects_name.split(','))
920 else:
921 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200922 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200923 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200924 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400925 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200926 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200927 res.append(
928 {'hostname': machine['hostname'],
929 'system_id': machine['system_id'],
930 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200931 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200932
azvyagintsev7605a662017-11-03 19:05:04 +0200933 @classmethod
934 def wait_for_machine_status(cls, **kwargs):
935 """
936 A function that wait for any requested status, for any set of maas
937 machines.
938
939 If no kwargs has been passed - will try to wait ALL
940 defined in salt::maas::region::machines
941
942 See readme file for more examples.
943 CLI Example:
944 .. code-block:: bash
945
946 salt-call state.apply maas.machines.wait_for_deployed
947
948 :param kwargs:
949 timeout: in s; Global timeout for wait
950 poll_time: in s;Sleep time, between retry
951 req_status: string; Polling status
952 machines: list; machine names
953 ignore_machines: list; machine names
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200954 attempts: max number of automatic hard retries
azvyagintsev7605a662017-11-03 19:05:04 +0200955 :ret: True
956 Exception - if something fail/timeout reached
957 """
958 timeout = kwargs.get("timeout", 60 * 120)
959 poll_time = kwargs.get("poll_time", 30)
960 req_status = kwargs.get("req_status", "Ready")
961 to_discover = kwargs.get("machines", None)
962 ignore_machines = kwargs.get("ignore_machines", None)
Martin Polreiche3183ad2019-10-30 13:01:50 +0100963 ignore_deployed_machines = kwargs.get("ignore_deployed_machines", False)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200964 attempts = kwargs.get("attempts", 0)
965 counter = {}
Martin Polreiche3183ad2019-10-30 13:01:50 +0100966 ignored_deployed = []
azvyagintsev7605a662017-11-03 19:05:04 +0200967 if not to_discover:
968 try:
969 to_discover = __salt__['config.get']('maas')['region'][
970 'machines'].keys()
971 except KeyError:
972 LOG.warning("No defined machines!")
973 return True
974 total = copy.deepcopy(to_discover) or []
975 if ignore_machines and total:
976 total = [x for x in to_discover if x not in ignore_machines]
977 started_at = time.time()
978 while len(total) <= len(to_discover):
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200979 for machine in to_discover:
azvyagintsev7605a662017-11-03 19:05:04 +0200980 for discovered in MachinesStatus.execute()['machines']:
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200981 if machine == discovered['hostname'] and machine in total:
Martin Polreiche3183ad2019-10-30 13:01:50 +0100982 if ignore_deployed_machines and discovered['status'].lower() == 'deployed':
983 total.remove(machine)
984 ignored_deployed.append(machine)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200985 if discovered['status'].lower() == req_status.lower():
986 total.remove(machine)
987 elif attempts > 0 and (machine not in counter or counter[machine] < attempts):
988 status = discovered['status']
989 sid = discovered['system_id']
990 cls._maas = _create_maas_client()
991 if status in ['Failed commissioning', 'New']:
992 cls._maas.delete(u'api/2.0/machines/{0}/'
993 .format(sid))
994 Machine().process()
995 LOG.info('Machine {0} deleted'.format(sid))
996 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
997 elif status in ['Failed testing']:
998 data = {}
999 action = 'override_failed_testing'
1000 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001001 .format(sid), action, **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001002 LOG.info('Machine {0} overriden'.format(sid))
1003 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
1004 elif status in ['Failed deployment', 'Allocated']:
1005 data = {}
1006 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001007 .format(sid), 'mark_broken', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001008 LOG.info('Machine {0} marked broken'.format(sid))
1009 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001010 .format(sid), 'mark_fixed', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001011 LOG.info('Machine {0} marked fixed'.format(sid))
1012 if machine in counter and counter[machine]:
1013 data['testing_scripts'] = 'fio'
1014 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001015 .format(sid), 'commission', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001016 LOG.info('Machine {0} fio test'.format(sid))
1017 DeployMachines().process()
1018 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
azvyagintsev7605a662017-11-03 19:05:04 +02001019 if len(total) <= 0:
Martin Polreiche3183ad2019-10-30 13:01:50 +01001020 if len(ignored_deployed) > 0:
1021 for mach in ignored_deployed:
1022 to_discover.remove(mach)
1023 if len(to_discover) > 0:
1024 LOG.debug(
1025 "Machines:{} are:{} and machines:{} were ignored as already deployed.".format(to_discover, req_status, ignored_deployed))
1026 else:
1027 LOG.debug(
1028 "All required machines already exist and were ignored as already deployed:{}".format(ignored_deployed))
1029 else:
1030 LOG.debug(
1031 "Machines:{} are:{}".format(to_discover, req_status))
azvyagintsev7605a662017-11-03 19:05:04 +02001032 return True
1033 if (timeout - (time.time() - started_at)) <= 0:
1034 raise Exception(
1035 'Machines:{}not in {} state'.format(total, req_status))
1036 LOG.info(
1037 "Waiting status:{} "
1038 "for machines:{}"
1039 "\nsleep for:{}s "
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001040 "Timeout:{}s ({}s left)"
1041 .format(req_status, total, poll_time, timeout,
1042 timeout - (time.time() - started_at)))
azvyagintsev7605a662017-11-03 19:05:04 +02001043 time.sleep(poll_time)
1044
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001045
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001046def process_fabrics():
1047 return Fabric().process()
1048
Jiri Broulike30a60f2018-04-09 21:15:10 +02001049def process_boot_sources():
1050 return Boot_source().process()
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001051
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001052def process_subnets():
1053 return Subnet().process()
1054
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001055
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001056def process_dhcp_snippets():
1057 return DHCPSnippet().process()
1058
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001059
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001060def process_package_repositories():
1061 return PacketRepository().process()
1062
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001063
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +02001064def process_devices(*args):
1065 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001066
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001067
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001068def process_machines(*args):
1069 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001070
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001071
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001072def process_assign_machines_ip(*args):
azvyagintsev06b71e72017-11-08 17:11:07 +02001073 """
1074 Manage interface configurations.
1075 See readme.rst for more info
1076 """
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001077 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001078
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001079
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001080def machines_status(*args):
1081 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001082
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001083
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001084def deploy_machines(*args):
1085 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +02001086
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001087
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001088def process_boot_resources():
1089 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001090
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001091
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001092def process_maas_config():
1093 return MaasConfig().process()
1094
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001095
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001096def process_commissioning_scripts():
1097 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +02001098
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001099
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +02001100def process_domain():
1101 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +02001102
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001103
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +02001104def process_sshprefs():
1105 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +02001106
1107
1108def wait_for_machine_status(**kwargs):
1109 return MachinesStatus.wait_for_machine_status(**kwargs)