blob: 8b07fbc25c04fe17de40258a43641befdc7ab947 [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
Oleksii Molchanov053aa462021-07-01 11:57:37 +0300653 # Shouldn't raise an error due to PROD-36319
azvyagintsev06b71e72017-11-08 17:11:07 +0200654 if machine['status'] != self.READY:
Oleksii Molchanov053aa462021-07-01 11:57:37 +0300655 LOG.debug('Machine:{} not in status:READY'.format(name))
656 return
azvyagintsev06b71e72017-11-08 17:11:07 +0200657 # backward comparability, for old schema
658 if data.get("interface", None):
659 if 'ip' not in data["interface"]:
660 LOG.info("No IP NIC definition for:{}".format(name))
661 return
662 LOG.warning(
663 "Old machine-describe detected! "
664 "Please read documentation "
665 "'salt-formulas/maas' for migration!")
666 return self._data_old(data['interface'], machines[name])
667 # NewSchema processing:
668 # Warning: old-style MaasObject.process still be called, but
669 # with empty data for process.
670 interfaces = data.get('interfaces', {})
671 if len(interfaces.keys()) == 0:
672 LOG.info("No IP NIC definition for:{}".format(name))
673 return
674 LOG.info('%s for %s', self.__class__.__name__.lower(),
675 machine['fqdn'])
676 self._disconnect_all_nic(machine)
677 for key, value in sorted(interfaces.iteritems()):
678 self._process_interface(value, machine)
679
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100680
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200681class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200682 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200683 READY = 4
Andreyef156992017-07-03 14:54:03 -0500684 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200685
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200686 def __init__(self):
687 super(DeployMachines, self).__init__()
688 self._all_elements_url = None
689 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
690 self._config_path = 'region.machines'
691 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200692 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
693 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200694
695 def fill_data(self, name, machine_data, machines):
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100696 if name in machines:
697 machine = machines[name]
698 else:
699 LOG.debug("Skipping node:{} "
700 "since it is not in maas yet".format(name))
701 return
Andreyef156992017-07-03 14:54:03 -0500702 if machine['status'] == self.DEPLOYED:
703 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200704 if machine['status'] != self.READY:
705 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200706 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200707 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200708 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200709 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200710 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200711 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200712 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200713 return data
714
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200715 def send(self, data):
716 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200717 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200718 return self._maas.post(self._create_url[0].format(**data),
719 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200720
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100721class BootResource(MaasObject):
722 def __init__(self):
723 super(BootResource, self).__init__()
724 self._all_elements_url = u'api/2.0/boot-resources/'
725 self._create_url = u'api/2.0/boot-resources/'
726 self._update_url = u'api/2.0/boot-resources/{0}/'
727 self._config_path = 'region.boot_resources'
728
729 def fill_data(self, name, boot_data):
730 sha256 = hashlib.sha256()
731 sha256.update(file(boot_data['content']).read())
732 data = {
733 'name': name,
734 'title': boot_data['title'],
735 'architecture': boot_data['architecture'],
736 'filetype': boot_data['filetype'],
737 'size': str(os.path.getsize(boot_data['content'])),
738 'sha256': sha256.hexdigest(),
739 'content': io.open(boot_data['content']),
740 }
741 return data
742
743 def update(self, new, old):
744 self._update = False
745 return new
746
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200747
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100748class CommissioningScripts(MaasObject):
749 def __init__(self):
750 super(CommissioningScripts, self).__init__()
751 self._all_elements_url = u'api/2.0/commissioning-scripts/'
752 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100753 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100754 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100755 self._update_key = 'name'
756
757 def fill_data(self, name, file_path):
758 data = {
759 'name': name,
760 'content': io.open(file_path),
761 }
762 return data
763
764 def update(self, new, old):
765 return new
766
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200767
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100768class MaasConfig(MaasObject):
769 def __init__(self):
770 super(MaasConfig, self).__init__()
771 self._all_elements_url = None
772 self._create_url = (u'api/2.0/maas/', u'set_config')
773 self._config_path = 'region.maas_config'
774
775 def fill_data(self, name, value):
776 data = {
777 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100778 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100779 }
780 return data
781
782 def update(self, new, old):
783 self._update = False
784 return new
785
786
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200787class SSHPrefs(MaasObject):
788 def __init__(self):
789 super(SSHPrefs, self).__init__()
790 self._all_elements_url = None
791 self._create_url = u'api/2.0/account/prefs/sshkeys/'
792 self._config_path = 'region.sshprefs'
793 self._element_key = 'hostname'
794 self._update_key = 'system_id'
795
796 def fill_data(self, value):
797 data = {
798 'key': value,
799 }
800 return data
801
802 def process(self):
803 config = __salt__['config.get']('maas')
804 for part in self._config_path.split('.'):
805 config = config.get(part, {})
806 extra = {}
807 for name, url_call in self._extra_data_urls.iteritems():
808 key = 'id'
809 if isinstance(url_call, tuple):
810 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200811 json_res = json.loads(self._maas.get(url_call).read())
812 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200813 if self._all_elements_url:
814 all_elements = {}
815 elements = self._maas.get(self._all_elements_url).read()
816 res_json = json.loads(elements)
817 for element in res_json:
818 if isinstance(element, (str, unicode)):
819 all_elements[element] = {}
820 else:
821 all_elements[element[self._element_key]] = element
822 else:
823 all_elements = {}
824 ret = {
825 'success': [],
826 'errors': {},
827 'updated': [],
828 }
829 for config_data in config:
830 name = config_data[:10]
831 try:
832 data = self.fill_data(config_data, **extra)
833 self.send(data)
834 ret['success'].append(name)
835 except urllib2.HTTPError as e:
836 etxt = e.read()
837 LOG.exception('Failed for object %s reason %s', name, etxt)
838 ret['errors'][name] = str(etxt)
839 except Exception as e:
840 LOG.exception('Failed for object %s reason %s', name, e)
841 ret['errors'][name] = str(e)
842 if ret['errors']:
843 raise Exception(ret)
844 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200845
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200846
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200847class Domain(MaasObject):
848 def __init__(self):
849 super(Domain, self).__init__()
850 self._all_elements_url = u'/api/2.0/domains/'
851 self._create_url = u'/api/2.0/domains/'
852 self._config_path = 'region.domain'
853 self._update_url = u'/api/2.0/domains/{0}/'
854
855 def fill_data(self, value):
856 data = {
857 'name': value,
858 }
859 self._update = True
860 return data
861
862 def update(self, new, old):
863 new['id'] = str(old['id'])
864 new['authoritative'] = str(old['authoritative'])
865 return new
866
867 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200868 ret = {
869 'success': [],
870 'errors': {},
871 'updated': [],
872 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200873 config = __salt__['config.get']('maas')
874 for part in self._config_path.split('.'):
875 config = config.get(part, {})
876 extra = {}
877 for name, url_call in self._extra_data_urls.iteritems():
878 key = 'id'
879 if isinstance(url_call, tuple):
880 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200881 json_res = json.loads(self._maas.get(url_call).read())
882 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200883 if self._all_elements_url:
884 all_elements = {}
885 elements = self._maas.get(self._all_elements_url).read()
886 res_json = json.loads(elements)
887 for element in res_json:
888 if isinstance(element, (str, unicode)):
889 all_elements[element] = {}
890 else:
891 all_elements[element[self._element_key]] = element
892 else:
893 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200894 try:
895 data = self.fill_data(config, **extra)
896 data = self.update(data, all_elements.values()[0])
897 self.send(data)
898 ret['success'].append('domain')
899 except urllib2.HTTPError as e:
900 etxt = e.read()
901 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
902 ret['errors']['domain'] = str(etxt)
903 except Exception as e:
904 LOG.exception('Failed for object %s reason %s', 'domain', e)
905 ret['errors']['domain'] = str(e)
906 if ret['errors']:
907 raise Exception(ret)
908 return ret
909
910
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200911class MachinesStatus(MaasObject):
912 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200913 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200914 cls._maas = _create_maas_client()
915 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200916 json_result = json.loads(result.read())
917 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200918 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200919 if objects_name:
920 if ',' in objects_name:
921 objects_name = set(objects_name.split(','))
922 else:
923 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200924 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200925 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200926 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400927 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200928 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200929 res.append(
930 {'hostname': machine['hostname'],
931 'system_id': machine['system_id'],
932 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200933 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200934
azvyagintsev7605a662017-11-03 19:05:04 +0200935 @classmethod
936 def wait_for_machine_status(cls, **kwargs):
937 """
938 A function that wait for any requested status, for any set of maas
939 machines.
940
941 If no kwargs has been passed - will try to wait ALL
942 defined in salt::maas::region::machines
943
944 See readme file for more examples.
945 CLI Example:
946 .. code-block:: bash
947
948 salt-call state.apply maas.machines.wait_for_deployed
949
950 :param kwargs:
951 timeout: in s; Global timeout for wait
952 poll_time: in s;Sleep time, between retry
953 req_status: string; Polling status
954 machines: list; machine names
955 ignore_machines: list; machine names
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200956 attempts: max number of automatic hard retries
azvyagintsev7605a662017-11-03 19:05:04 +0200957 :ret: True
958 Exception - if something fail/timeout reached
959 """
960 timeout = kwargs.get("timeout", 60 * 120)
961 poll_time = kwargs.get("poll_time", 30)
962 req_status = kwargs.get("req_status", "Ready")
963 to_discover = kwargs.get("machines", None)
964 ignore_machines = kwargs.get("ignore_machines", None)
Martin Polreiche3183ad2019-10-30 13:01:50 +0100965 ignore_deployed_machines = kwargs.get("ignore_deployed_machines", False)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200966 attempts = kwargs.get("attempts", 0)
967 counter = {}
Martin Polreiche3183ad2019-10-30 13:01:50 +0100968 ignored_deployed = []
azvyagintsev7605a662017-11-03 19:05:04 +0200969 if not to_discover:
970 try:
971 to_discover = __salt__['config.get']('maas')['region'][
972 'machines'].keys()
973 except KeyError:
974 LOG.warning("No defined machines!")
975 return True
976 total = copy.deepcopy(to_discover) or []
977 if ignore_machines and total:
978 total = [x for x in to_discover if x not in ignore_machines]
979 started_at = time.time()
980 while len(total) <= len(to_discover):
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200981 for machine in to_discover:
azvyagintsev7605a662017-11-03 19:05:04 +0200982 for discovered in MachinesStatus.execute()['machines']:
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200983 if machine == discovered['hostname'] and machine in total:
Martin Polreiche3183ad2019-10-30 13:01:50 +0100984 if ignore_deployed_machines and discovered['status'].lower() == 'deployed':
985 total.remove(machine)
986 ignored_deployed.append(machine)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200987 if discovered['status'].lower() == req_status.lower():
988 total.remove(machine)
989 elif attempts > 0 and (machine not in counter or counter[machine] < attempts):
990 status = discovered['status']
991 sid = discovered['system_id']
992 cls._maas = _create_maas_client()
993 if status in ['Failed commissioning', 'New']:
994 cls._maas.delete(u'api/2.0/machines/{0}/'
995 .format(sid))
996 Machine().process()
997 LOG.info('Machine {0} deleted'.format(sid))
998 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
999 elif status in ['Failed testing']:
1000 data = {}
1001 action = 'override_failed_testing'
1002 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001003 .format(sid), action, **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001004 LOG.info('Machine {0} overriden'.format(sid))
1005 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
1006 elif status in ['Failed deployment', 'Allocated']:
1007 data = {}
1008 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001009 .format(sid), 'mark_broken', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001010 LOG.info('Machine {0} marked broken'.format(sid))
1011 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001012 .format(sid), 'mark_fixed', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001013 LOG.info('Machine {0} marked fixed'.format(sid))
1014 if machine in counter and counter[machine]:
1015 data['testing_scripts'] = 'fio'
1016 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001017 .format(sid), 'commission', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001018 LOG.info('Machine {0} fio test'.format(sid))
1019 DeployMachines().process()
1020 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
azvyagintsev7605a662017-11-03 19:05:04 +02001021 if len(total) <= 0:
Martin Polreiche3183ad2019-10-30 13:01:50 +01001022 if len(ignored_deployed) > 0:
1023 for mach in ignored_deployed:
1024 to_discover.remove(mach)
1025 if len(to_discover) > 0:
1026 LOG.debug(
1027 "Machines:{} are:{} and machines:{} were ignored as already deployed.".format(to_discover, req_status, ignored_deployed))
1028 else:
1029 LOG.debug(
1030 "All required machines already exist and were ignored as already deployed:{}".format(ignored_deployed))
1031 else:
1032 LOG.debug(
1033 "Machines:{} are:{}".format(to_discover, req_status))
azvyagintsev7605a662017-11-03 19:05:04 +02001034 return True
1035 if (timeout - (time.time() - started_at)) <= 0:
1036 raise Exception(
1037 'Machines:{}not in {} state'.format(total, req_status))
1038 LOG.info(
1039 "Waiting status:{} "
1040 "for machines:{}"
1041 "\nsleep for:{}s "
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001042 "Timeout:{}s ({}s left)"
1043 .format(req_status, total, poll_time, timeout,
1044 timeout - (time.time() - started_at)))
azvyagintsev7605a662017-11-03 19:05:04 +02001045 time.sleep(poll_time)
1046
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001047
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001048def process_fabrics():
1049 return Fabric().process()
1050
Jiri Broulike30a60f2018-04-09 21:15:10 +02001051def process_boot_sources():
1052 return Boot_source().process()
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001053
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001054def process_subnets():
1055 return Subnet().process()
1056
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001057
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001058def process_dhcp_snippets():
1059 return DHCPSnippet().process()
1060
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001061
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001062def process_package_repositories():
1063 return PacketRepository().process()
1064
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001065
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +02001066def process_devices(*args):
1067 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001068
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001069
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001070def process_machines(*args):
1071 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001072
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001073
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001074def process_assign_machines_ip(*args):
azvyagintsev06b71e72017-11-08 17:11:07 +02001075 """
1076 Manage interface configurations.
1077 See readme.rst for more info
1078 """
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001079 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001080
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001081
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001082def machines_status(*args):
1083 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001084
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001085
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001086def deploy_machines(*args):
1087 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +02001088
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001089
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001090def process_boot_resources():
1091 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001092
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001093
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001094def process_maas_config():
1095 return MaasConfig().process()
1096
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001097
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001098def process_commissioning_scripts():
1099 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +02001100
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001101
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +02001102def process_domain():
1103 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +02001104
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001105
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +02001106def process_sshprefs():
1107 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +02001108
1109
1110def wait_for_machine_status(**kwargs):
1111 return MachinesStatus.wait_for_machine_status(**kwargs)