blob: 0152ebab732ac3bcb4943eaec4a823b8676ca6ae [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:
354 data['disabled_pockets'] = package_repository['disable_pockets'],
355 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100356
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100357 def update(self, new, old):
358 new['id'] = str(old['id'])
359 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100360
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200361
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100362class Device(MaasObject):
363 def __init__(self):
364 super(Device, self).__init__()
365 self._all_elements_url = u'api/2.0/devices/'
366 self._create_url = u'api/2.0/devices/'
367 self._update_url = u'api/2.0/devices/{0}/'
368 self._config_path = 'region.devices'
369 self._element_key = 'hostname'
370 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100371
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100372 def fill_data(self, name, device_data):
373 data = {
374 'mac_addresses': device_data['mac'],
375 'hostname': name,
376 }
377 self._interface = device_data['interface']
378 return data
379
380 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100381 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
382 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100383 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100384 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100385 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
386 else:
387 new[self._update_key] = str(old[self._update_key])
388 return new
389
390 def send(self, data):
391 response = super(Device, self).send(data)
392 resp_json = json.loads(response)
393 system_id = resp_json['system_id']
394 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100395 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100396 return response
397
398 def _link_interface(self, system_id, interface_id):
399 data = {
400 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100401 'subnet': self._interface['subnet'],
402 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100403 }
404 if 'default_gateway' in self._interface:
405 data['default_gateway'] = self._interface.get('default_gateway')
406 if self._update:
407 data['force'] = '1'
408 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200409 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100410 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
411 .format(system_id, interface_id), 'link_subnet',
412 **data)
413
414
415class Machine(MaasObject):
416 def __init__(self):
417 super(Machine, self).__init__()
418 self._all_elements_url = u'api/2.0/machines/'
419 self._create_url = u'api/2.0/machines/'
420 self._update_url = u'api/2.0/machines/{0}/'
421 self._config_path = 'region.machines'
422 self._element_key = 'hostname'
423 self._update_key = 'system_id'
424
425 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100426 power_data = machine_data['power_parameters']
azvyagintsev06b71e72017-11-08 17:11:07 +0200427 machine_pxe_mac = machine_data.get('pxe_interface_mac', None)
428 if machine_data.get("interface", None):
429 LOG.warning(
430 "Old machine-describe detected! "
431 "Please read documentation for "
432 "'salt-formulas/maas' for migration!")
433 machine_pxe_mac = machine_data['interface'].get('mac', None)
434 if not machine_pxe_mac:
435 raise Exception("PXE MAC for machine:{} not defined".format(name))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100436 data = {
437 'hostname': name,
438 'architecture': machine_data.get('architecture', 'amd64/generic'),
azvyagintsev06b71e72017-11-08 17:11:07 +0200439 'mac_addresses': machine_pxe_mac,
mkraynove95cdb62018-05-08 14:17:18 +0400440 'power_type': power_data.get('power_type', 'manual'),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100441 }
azvyagintsev6543ec82018-11-01 23:45:09 +0200442 for k, v in power_data.items():
Denis Egorenko60963332018-10-23 19:24:28 +0400443 if k == 'power_type':
444 continue
azvyagintsev6543ec82018-11-01 23:45:09 +0200445 elif k == 'power_password':
446 data['power_parameters_power_pass'] = v
447 LOG.warning(
448 "Deprecated power parameter:power_password passed! "
449 "Please change key name to 'power_pass'! ")
450 continue
451 elif k == 'power_nova_id':
452 data['power_parameters_nova_id'] = v
453 LOG.warning(
454 "Deprecated power parameter:power_nova_id passed! "
455 "Please change key name to 'nova_id'! ")
456 continue
457 elif k == 'power_os_tenantname':
458 data['power_parameters_os_tenantname'] = v
459 LOG.warning(
460 "Deprecated power parameter:power_os_tenantname passed! "
461 "Please change key name to 'os_tenantname'! ")
462 continue
463 elif k == 'power_os_username':
464 data['power_parameters_os_username'] = v
465 LOG.warning(
466 "Deprecated power parameter:power_os_username passed! "
467 "Please change key name to 'os_username'! ")
468 continue
469 elif k == 'power_os_password':
470 data['power_parameters_os_password'] = v
471 LOG.warning(
472 "Deprecated power parameter:power_os_password passed! "
473 "Please change key name to 'os_password'! ")
474 continue
475 elif k == 'power_os_authurl':
476 data['power_parameters_os_authurl'] = v
477 LOG.warning(
478 "Deprecated power parameter:power_os_authurl passed! "
479 "Please change key name to 'os_authurl'! ")
480 continue
481
Denis Egorenko60963332018-10-23 19:24:28 +0400482 data_key = 'power_parameters_{}'.format(k)
483 data[data_key] = v
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100484 return data
485
486 def update(self, new, old):
Gabor Toth36bc0282018-08-18 10:39:51 +0200487 LOG.debug('Updating machine')
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100488 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
Gabor Toth36bc0282018-08-18 10:39:51 +0200489 LOG.debug('old_macs: %s' % old_macs)
490 if isinstance(new['mac_addresses'], list):
491 new_macs = set(v.lower() for v in new['mac_addresses'])
492 else:
493 new_macs = set([new['mac_addresses'].lower()])
494 LOG.debug('new_macs: %s' % new_macs)
495 intersect = list(new_macs.intersection(old_macs))
496 if not intersect:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100497 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100498 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200499 self._maas.delete(u'api/2.0/machines/{0}/'
500 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100501 else:
Gabor Toth36bc0282018-08-18 10:39:51 +0200502 new['mac_addresses'] = intersect
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100503 new[self._update_key] = str(old[self._update_key])
504 return new
505
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200506
507class AssignMachinesIP(MaasObject):
azvyagintsev06b71e72017-11-08 17:11:07 +0200508 # FIXME
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200509 READY = 4
Andreyef156992017-07-03 14:54:03 -0500510 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200511
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200512 def __init__(self):
513 super(AssignMachinesIP, self).__init__()
514 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200515 self._create_url = \
516 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
517 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200518 self._config_path = 'region.machines'
519 self._element_key = 'hostname'
520 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200521 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
522 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200523
azvyagintsev06b71e72017-11-08 17:11:07 +0200524 def _data_old(self, _interface, _machine):
525 """
526 _interface = {
527 "mac": "11:22:33:44:55:77",
528 "mode": "STATIC",
529 "ip": "2.2.3.15",
530 "subnet": "subnet1",
531 "gateway": "2.2.3.2",
532 }
533 :param data:
534 :return:
535 """
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100536 data = {
537 'mode': 'STATIC',
azvyagintsev06b71e72017-11-08 17:11:07 +0200538 'subnet': str(_interface.get('subnet')),
539 'ip_address': str(_interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100540 }
azvyagintsev06b71e72017-11-08 17:11:07 +0200541 if 'gateway' in _interface:
542 data['default_gateway'] = _interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200543 data['force'] = '1'
azvyagintsev06b71e72017-11-08 17:11:07 +0200544 data['system_id'] = str(_machine['system_id'])
545 data['interface_id'] = str(_machine['interface_set'][0]['id'])
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200546 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100547
azvyagintsev06b71e72017-11-08 17:11:07 +0200548 def _get_nic_id_by_mac(self, machine, req_mac=None):
549 data = {}
550 for nic in machine['interface_set']:
Pavel Cizinsky55d9d762019-01-15 14:33:37 +0100551 data[nic['mac_address'].lower()] = nic['id']
azvyagintsev06b71e72017-11-08 17:11:07 +0200552 if req_mac:
Pavel Cizinsky55d9d762019-01-15 14:33:37 +0100553 if req_mac.lower() in data.keys():
554 return data[req_mac.lower()]
azvyagintsev06b71e72017-11-08 17:11:07 +0200555 else:
556 raise Exception('NIC with mac:{} not found at '
557 'node:{}'.format(req_mac, machine['fqdn']))
558 return data
559
560 def _disconnect_all_nic(self, machine):
561 """
562 Maas will fail, in case same config's will be to apply
563 on different interfaces. In same time - not possible to push
564 whole network schema at once. Before configuring - need to clean-up
565 everything
566 :param machine:
567 :return:
568 """
569 for nic in machine['interface_set']:
570 LOG.debug("Disconnecting interface:{}".format(nic['mac_address']))
571 try:
572 self._maas.post(
573 u'/api/2.0/nodes/{}/interfaces/{}/'.format(
574 machine['system_id'], nic['id']), 'disconnect')
575 except Exception as e:
576 LOG.error("Failed to disconnect interface:{} on node:{}".format(
577 nic['mac_address'], machine['fqdn']))
578 raise Exception(str(e))
579
580 def _process_interface(self, nic_data, machine):
581 """
582 Process exactly one interface:
583 - update interface
584 - link to network
585 These functions are self-complementary, and do not require an
586 external "process" method. Those broke old-MaasObject logic,
587 though make functions more simple in case iterable tasks.
588 """
589 nic_id = self._get_nic_id_by_mac(machine, nic_data['mac'])
590
591 # Process op=link_subnet
592 link_data = {}
593 _mode = nic_data.get('mode', 'AUTO').upper()
594 if _mode == 'STATIC':
595 link_data = {
596 'mode': 'STATIC',
597 'subnet': str(nic_data.get('subnet')),
598 'ip_address': str(nic_data.get('ip')),
599 'default_gateway': str(nic_data.get('gateway', "")),
600 }
601 elif _mode == 'DHCP':
602 link_data = {
603 'mode': 'DHCP',
604 'subnet': str(nic_data.get('subnet')),
605 }
606 elif _mode == 'AUTO':
607 link_data = {'mode': 'AUTO',
608 'default_gateway': str(nic_data.get('gateway', "")), }
609 elif _mode in ('LINK_UP', 'UNCONFIGURED'):
610 link_data = {'mode': 'LINK_UP'}
611 else:
612 raise Exception('Wrong IP mode:{}'
613 ' for node:{}'.format(_mode, machine['fqdn']))
614 link_data['force'] = str(1)
615
616 physical_data = {"name": nic_data.get("name", ""),
617 "tags": nic_data.get('tags', ""),
618 "vlan": nic_data.get('vlan', "")}
619
620 try:
621 # Cleanup-old definition
622 LOG.debug("Processing {}:{},{}".format(nic_data['mac'], link_data,
623 physical_data))
624 # "link_subnet" and "fill all other data" - its 2 different
625 # operations. So, first we update NIC:
626 self._maas.put(
627 u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
628 nic_id),
629 **physical_data)
630 # And then, link subnet configuration:
631 self._maas.post(
632 u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
633 nic_id),
634 'link_subnet', **link_data)
635 except Exception as e:
636 LOG.error("Failed to process interface:{} on node:{}".format(
637 nic_data['mac'], machine['fqdn']))
638 raise Exception(str(e))
639
640 def fill_data(self, name, data, machines):
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100641 if name in machines:
642 machine = machines[name]
643 else:
644 LOG.debug("Skipping node:{} "
645 "since it is not in maas yet".format(name))
646 return
azvyagintsev06b71e72017-11-08 17:11:07 +0200647 if machine['status'] == self.DEPLOYED:
648 LOG.debug("Skipping node:{} "
649 "since it in status:DEPLOYED".format(name))
650 return
651 if machine['status'] != self.READY:
652 raise Exception('Machine:{} not in status:READY'.format(name))
653 # backward comparability, for old schema
654 if data.get("interface", None):
655 if 'ip' not in data["interface"]:
656 LOG.info("No IP NIC definition for:{}".format(name))
657 return
658 LOG.warning(
659 "Old machine-describe detected! "
660 "Please read documentation "
661 "'salt-formulas/maas' for migration!")
662 return self._data_old(data['interface'], machines[name])
663 # NewSchema processing:
664 # Warning: old-style MaasObject.process still be called, but
665 # with empty data for process.
666 interfaces = data.get('interfaces', {})
667 if len(interfaces.keys()) == 0:
668 LOG.info("No IP NIC definition for:{}".format(name))
669 return
670 LOG.info('%s for %s', self.__class__.__name__.lower(),
671 machine['fqdn'])
672 self._disconnect_all_nic(machine)
673 for key, value in sorted(interfaces.iteritems()):
674 self._process_interface(value, machine)
675
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100676
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200677class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200678 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200679 READY = 4
Andreyef156992017-07-03 14:54:03 -0500680 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200681
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200682 def __init__(self):
683 super(DeployMachines, self).__init__()
684 self._all_elements_url = None
685 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
686 self._config_path = 'region.machines'
687 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200688 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
689 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200690
691 def fill_data(self, name, machine_data, machines):
Dzmitry Stremkouskid015e022020-03-23 12:06:50 +0100692 if name in machines:
693 machine = machines[name]
694 else:
695 LOG.debug("Skipping node:{} "
696 "since it is not in maas yet".format(name))
697 return
Andreyef156992017-07-03 14:54:03 -0500698 if machine['status'] == self.DEPLOYED:
699 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200700 if machine['status'] != self.READY:
701 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200702 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200703 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200704 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200705 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200706 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200707 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200708 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200709 return data
710
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200711 def send(self, data):
712 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200713 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200714 return self._maas.post(self._create_url[0].format(**data),
715 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200716
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100717class BootResource(MaasObject):
718 def __init__(self):
719 super(BootResource, self).__init__()
720 self._all_elements_url = u'api/2.0/boot-resources/'
721 self._create_url = u'api/2.0/boot-resources/'
722 self._update_url = u'api/2.0/boot-resources/{0}/'
723 self._config_path = 'region.boot_resources'
724
725 def fill_data(self, name, boot_data):
726 sha256 = hashlib.sha256()
727 sha256.update(file(boot_data['content']).read())
728 data = {
729 'name': name,
730 'title': boot_data['title'],
731 'architecture': boot_data['architecture'],
732 'filetype': boot_data['filetype'],
733 'size': str(os.path.getsize(boot_data['content'])),
734 'sha256': sha256.hexdigest(),
735 'content': io.open(boot_data['content']),
736 }
737 return data
738
739 def update(self, new, old):
740 self._update = False
741 return new
742
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200743
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100744class CommissioningScripts(MaasObject):
745 def __init__(self):
746 super(CommissioningScripts, self).__init__()
747 self._all_elements_url = u'api/2.0/commissioning-scripts/'
748 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100749 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100750 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100751 self._update_key = 'name'
752
753 def fill_data(self, name, file_path):
754 data = {
755 'name': name,
756 'content': io.open(file_path),
757 }
758 return data
759
760 def update(self, new, old):
761 return new
762
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200763
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100764class MaasConfig(MaasObject):
765 def __init__(self):
766 super(MaasConfig, self).__init__()
767 self._all_elements_url = None
768 self._create_url = (u'api/2.0/maas/', u'set_config')
769 self._config_path = 'region.maas_config'
770
771 def fill_data(self, name, value):
772 data = {
773 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100774 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100775 }
776 return data
777
778 def update(self, new, old):
779 self._update = False
780 return new
781
782
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200783class SSHPrefs(MaasObject):
784 def __init__(self):
785 super(SSHPrefs, self).__init__()
786 self._all_elements_url = None
787 self._create_url = u'api/2.0/account/prefs/sshkeys/'
788 self._config_path = 'region.sshprefs'
789 self._element_key = 'hostname'
790 self._update_key = 'system_id'
791
792 def fill_data(self, value):
793 data = {
794 'key': value,
795 }
796 return data
797
798 def process(self):
799 config = __salt__['config.get']('maas')
800 for part in self._config_path.split('.'):
801 config = config.get(part, {})
802 extra = {}
803 for name, url_call in self._extra_data_urls.iteritems():
804 key = 'id'
805 if isinstance(url_call, tuple):
806 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200807 json_res = json.loads(self._maas.get(url_call).read())
808 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200809 if self._all_elements_url:
810 all_elements = {}
811 elements = self._maas.get(self._all_elements_url).read()
812 res_json = json.loads(elements)
813 for element in res_json:
814 if isinstance(element, (str, unicode)):
815 all_elements[element] = {}
816 else:
817 all_elements[element[self._element_key]] = element
818 else:
819 all_elements = {}
820 ret = {
821 'success': [],
822 'errors': {},
823 'updated': [],
824 }
825 for config_data in config:
826 name = config_data[:10]
827 try:
828 data = self.fill_data(config_data, **extra)
829 self.send(data)
830 ret['success'].append(name)
831 except urllib2.HTTPError as e:
832 etxt = e.read()
833 LOG.exception('Failed for object %s reason %s', name, etxt)
834 ret['errors'][name] = str(etxt)
835 except Exception as e:
836 LOG.exception('Failed for object %s reason %s', name, e)
837 ret['errors'][name] = str(e)
838 if ret['errors']:
839 raise Exception(ret)
840 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200841
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200842
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200843class Domain(MaasObject):
844 def __init__(self):
845 super(Domain, self).__init__()
846 self._all_elements_url = u'/api/2.0/domains/'
847 self._create_url = u'/api/2.0/domains/'
848 self._config_path = 'region.domain'
849 self._update_url = u'/api/2.0/domains/{0}/'
850
851 def fill_data(self, value):
852 data = {
853 'name': value,
854 }
855 self._update = True
856 return data
857
858 def update(self, new, old):
859 new['id'] = str(old['id'])
860 new['authoritative'] = str(old['authoritative'])
861 return new
862
863 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200864 ret = {
865 'success': [],
866 'errors': {},
867 'updated': [],
868 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200869 config = __salt__['config.get']('maas')
870 for part in self._config_path.split('.'):
871 config = config.get(part, {})
872 extra = {}
873 for name, url_call in self._extra_data_urls.iteritems():
874 key = 'id'
875 if isinstance(url_call, tuple):
876 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200877 json_res = json.loads(self._maas.get(url_call).read())
878 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200879 if self._all_elements_url:
880 all_elements = {}
881 elements = self._maas.get(self._all_elements_url).read()
882 res_json = json.loads(elements)
883 for element in res_json:
884 if isinstance(element, (str, unicode)):
885 all_elements[element] = {}
886 else:
887 all_elements[element[self._element_key]] = element
888 else:
889 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200890 try:
891 data = self.fill_data(config, **extra)
892 data = self.update(data, all_elements.values()[0])
893 self.send(data)
894 ret['success'].append('domain')
895 except urllib2.HTTPError as e:
896 etxt = e.read()
897 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
898 ret['errors']['domain'] = str(etxt)
899 except Exception as e:
900 LOG.exception('Failed for object %s reason %s', 'domain', e)
901 ret['errors']['domain'] = str(e)
902 if ret['errors']:
903 raise Exception(ret)
904 return ret
905
906
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200907class MachinesStatus(MaasObject):
908 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200909 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200910 cls._maas = _create_maas_client()
911 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200912 json_result = json.loads(result.read())
913 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200914 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200915 if objects_name:
916 if ',' in objects_name:
917 objects_name = set(objects_name.split(','))
918 else:
919 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200920 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200921 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200922 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400923 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200924 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200925 res.append(
926 {'hostname': machine['hostname'],
927 'system_id': machine['system_id'],
928 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200929 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200930
azvyagintsev7605a662017-11-03 19:05:04 +0200931 @classmethod
932 def wait_for_machine_status(cls, **kwargs):
933 """
934 A function that wait for any requested status, for any set of maas
935 machines.
936
937 If no kwargs has been passed - will try to wait ALL
938 defined in salt::maas::region::machines
939
940 See readme file for more examples.
941 CLI Example:
942 .. code-block:: bash
943
944 salt-call state.apply maas.machines.wait_for_deployed
945
946 :param kwargs:
947 timeout: in s; Global timeout for wait
948 poll_time: in s;Sleep time, between retry
949 req_status: string; Polling status
950 machines: list; machine names
951 ignore_machines: list; machine names
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200952 attempts: max number of automatic hard retries
azvyagintsev7605a662017-11-03 19:05:04 +0200953 :ret: True
954 Exception - if something fail/timeout reached
955 """
956 timeout = kwargs.get("timeout", 60 * 120)
957 poll_time = kwargs.get("poll_time", 30)
958 req_status = kwargs.get("req_status", "Ready")
959 to_discover = kwargs.get("machines", None)
960 ignore_machines = kwargs.get("ignore_machines", None)
Martin Polreiche3183ad2019-10-30 13:01:50 +0100961 ignore_deployed_machines = kwargs.get("ignore_deployed_machines", False)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200962 attempts = kwargs.get("attempts", 0)
963 counter = {}
Martin Polreiche3183ad2019-10-30 13:01:50 +0100964 ignored_deployed = []
azvyagintsev7605a662017-11-03 19:05:04 +0200965 if not to_discover:
966 try:
967 to_discover = __salt__['config.get']('maas')['region'][
968 'machines'].keys()
969 except KeyError:
970 LOG.warning("No defined machines!")
971 return True
972 total = copy.deepcopy(to_discover) or []
973 if ignore_machines and total:
974 total = [x for x in to_discover if x not in ignore_machines]
975 started_at = time.time()
976 while len(total) <= len(to_discover):
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200977 for machine in to_discover:
azvyagintsev7605a662017-11-03 19:05:04 +0200978 for discovered in MachinesStatus.execute()['machines']:
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200979 if machine == discovered['hostname'] and machine in total:
Martin Polreiche3183ad2019-10-30 13:01:50 +0100980 if ignore_deployed_machines and discovered['status'].lower() == 'deployed':
981 total.remove(machine)
982 ignored_deployed.append(machine)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +0200983 if discovered['status'].lower() == req_status.lower():
984 total.remove(machine)
985 elif attempts > 0 and (machine not in counter or counter[machine] < attempts):
986 status = discovered['status']
987 sid = discovered['system_id']
988 cls._maas = _create_maas_client()
989 if status in ['Failed commissioning', 'New']:
990 cls._maas.delete(u'api/2.0/machines/{0}/'
991 .format(sid))
992 Machine().process()
993 LOG.info('Machine {0} deleted'.format(sid))
994 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
995 elif status in ['Failed testing']:
996 data = {}
997 action = 'override_failed_testing'
998 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +0400999 .format(sid), action, **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001000 LOG.info('Machine {0} overriden'.format(sid))
1001 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
1002 elif status in ['Failed deployment', 'Allocated']:
1003 data = {}
1004 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001005 .format(sid), 'mark_broken', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001006 LOG.info('Machine {0} marked broken'.format(sid))
1007 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001008 .format(sid), 'mark_fixed', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001009 LOG.info('Machine {0} marked fixed'.format(sid))
1010 if machine in counter and counter[machine]:
1011 data['testing_scripts'] = 'fio'
1012 cls._maas.post(u'api/2.0/machines/{0}/'
Ivan Berezovskiyec560012019-10-11 18:05:15 +04001013 .format(sid), 'commission', **data)
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001014 LOG.info('Machine {0} fio test'.format(sid))
1015 DeployMachines().process()
1016 counter[machine] = 1 if machine not in counter else (counter[machine] + 1)
azvyagintsev7605a662017-11-03 19:05:04 +02001017 if len(total) <= 0:
Martin Polreiche3183ad2019-10-30 13:01:50 +01001018 if len(ignored_deployed) > 0:
1019 for mach in ignored_deployed:
1020 to_discover.remove(mach)
1021 if len(to_discover) > 0:
1022 LOG.debug(
1023 "Machines:{} are:{} and machines:{} were ignored as already deployed.".format(to_discover, req_status, ignored_deployed))
1024 else:
1025 LOG.debug(
1026 "All required machines already exist and were ignored as already deployed:{}".format(ignored_deployed))
1027 else:
1028 LOG.debug(
1029 "Machines:{} are:{}".format(to_discover, req_status))
azvyagintsev7605a662017-11-03 19:05:04 +02001030 return True
1031 if (timeout - (time.time() - started_at)) <= 0:
1032 raise Exception(
1033 'Machines:{}not in {} state'.format(total, req_status))
1034 LOG.info(
1035 "Waiting status:{} "
1036 "for machines:{}"
1037 "\nsleep for:{}s "
Alexandru Avadanii3206fe72018-09-23 03:57:27 +02001038 "Timeout:{}s ({}s left)"
1039 .format(req_status, total, poll_time, timeout,
1040 timeout - (time.time() - started_at)))
azvyagintsev7605a662017-11-03 19:05:04 +02001041 time.sleep(poll_time)
1042
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001043
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001044def process_fabrics():
1045 return Fabric().process()
1046
Jiri Broulike30a60f2018-04-09 21:15:10 +02001047def process_boot_sources():
1048 return Boot_source().process()
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001049
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001050def process_subnets():
1051 return Subnet().process()
1052
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001053
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001054def process_dhcp_snippets():
1055 return DHCPSnippet().process()
1056
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001057
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001058def process_package_repositories():
1059 return PacketRepository().process()
1060
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001061
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +02001062def process_devices(*args):
1063 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001064
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001065
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001066def process_machines(*args):
1067 return Machine().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_assign_machines_ip(*args):
azvyagintsev06b71e72017-11-08 17:11:07 +02001071 """
1072 Manage interface configurations.
1073 See readme.rst for more info
1074 """
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001075 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +02001076
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001077
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +02001078def machines_status(*args):
1079 return MachinesStatus.execute(*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 deploy_machines(*args):
1083 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +02001084
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001085
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +01001086def process_boot_resources():
1087 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001088
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001089
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001090def process_maas_config():
1091 return MaasConfig().process()
1092
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001093
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +01001094def process_commissioning_scripts():
1095 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +02001096
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001097
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +02001098def process_domain():
1099 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +02001100
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +02001101
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +02001102def process_sshprefs():
1103 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +02001104
1105
1106def wait_for_machine_status(**kwargs):
1107 return MachinesStatus.wait_for_machine_status(**kwargs)