blob: ee110c85411e8f10595b086f19796b494b2ef08c [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):
azvyagintsev06b71e72017-11-08 17:11:07 +0200103 # FIXME: probably, should be extended with "skipped" return.
104 # For example, currently "DEPLOYED" nodes are skipped, and no changes
105 # applied - but they fall into 'updated' list.
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200106 ret = {
107 'success': [],
108 'errors': {},
109 'updated': [],
110 }
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200111 try:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200112 config = __salt__['config.get']('maas')
113 for part in self._config_path.split('.'):
114 config = config.get(part, {})
115 extra = {}
116 for name, url_call in self._extra_data_urls.iteritems():
117 key = 'id'
118 key_name = 'name'
119 if isinstance(url_call, tuple):
120 if len(url_call) == 2:
121 url_call, key = url_call[:]
122 else:
123 url_call, key, key_name = url_call[:]
124 json_res = json.loads(self._maas.get(url_call).read())
125 if key:
126 extra[name] = {v[key_name]: v[key] for v in json_res}
127 else:
Krzysztof Szukiełojć52cc6b02017-04-06 09:53:43 +0200128 extra[name] = {v[key_name]: v for v in json_res}
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200129 if self._all_elements_url:
130 all_elements = {}
131 elements = self._maas.get(self._all_elements_url).read()
132 res_json = json.loads(elements)
133 for element in res_json:
134 if isinstance(element, (str, unicode)):
135 all_elements[element] = {}
136 else:
137 all_elements[element[self._element_key]] = element
138 else:
139 all_elements = {}
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200140
141 def process_single(name, config_data):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200142 self._update = False
143 try:
144 data = self.fill_data(name, config_data, **extra)
145 if data is None:
146 ret['updated'].append(name)
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200147 return
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200148 if name in all_elements:
149 self._update = True
150 data = self.update(data, all_elements[name])
151 self.send(data)
152 ret['updated'].append(name)
153 else:
154 self.send(data)
155 ret['success'].append(name)
156 except urllib2.HTTPError as e:
azvyagintsev06b71e72017-11-08 17:11:07 +0200157 # FIXME add exception's for response:
158 # '{"mode": ["Interface is already set to DHCP."]}
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200159 etxt = e.read()
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200160 LOG.error('Failed for object %s reason %s', name, etxt)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200161 ret['errors'][name] = str(etxt)
162 except Exception as e:
Krzysztof Szukiełojć199d5af2017-04-12 13:23:10 +0200163 LOG.error('Failed for object %s reason %s', name, e)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200164 ret['errors'][name] = str(e)
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200165 if objects_name is not None:
166 if ',' in objects_name:
167 objects_name = objects_name.split(',')
168 else:
169 objects_name = [objects_name]
170 for object_name in objects_name:
171 process_single(object_name, config[object_name])
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200172 else:
173 for name, config_data in config.iteritems():
174 process_single(name, config_data)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200175 except Exception as e:
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200176 LOG.exception('Error Global')
177 raise
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100178 if ret['errors']:
179 raise Exception(ret)
180 return ret
Ales Komarek663b85c2016-03-11 14:26:42 +0100181
182
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100183class Fabric(MaasObject):
184 def __init__(self):
185 super(Fabric, self).__init__()
186 self._all_elements_url = u'api/2.0/fabrics/'
187 self._create_url = u'api/2.0/fabrics/'
188 self._update_url = u'api/2.0/fabrics/{0}/'
189 self._config_path = 'region.fabrics'
Ales Komarek663b85c2016-03-11 14:26:42 +0100190
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100191 def fill_data(self, name, fabric):
192 data = {
193 'name': name,
194 'description': fabric.get('description', ''),
195 }
196 if 'class_type' in fabric:
197 data['class_type'] = fabric.get('class_type'),
198 return data
Ales Komarek663b85c2016-03-11 14:26:42 +0100199
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100200 def update(self, new, old):
201 new['id'] = str(old['id'])
202 return new
Ales Komarek663b85c2016-03-11 14:26:42 +0100203
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200204
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100205class Subnet(MaasObject):
206 def __init__(self):
207 super(Subnet, self).__init__()
208 self._all_elements_url = u'api/2.0/subnets/'
209 self._create_url = u'api/2.0/subnets/'
210 self._update_url = u'api/2.0/subnets/{0}/'
211 self._config_path = 'region.subnets'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200212 self._extra_data_urls = {'fabrics': u'api/2.0/fabrics/'}
Ales Komarek0fafa572016-03-11 14:56:44 +0100213
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100214 def fill_data(self, name, subnet, fabrics):
215 data = {
216 'name': name,
217 'fabric': str(fabrics[subnet.get('fabric', '')]),
218 'cidr': subnet.get('cidr'),
219 'gateway_ip': subnet['gateway_ip'],
220 }
221 self._iprange = subnet['iprange']
222 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100223
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100224 def update(self, new, old):
225 new['id'] = str(old['id'])
226 return new
Ales Komarek0fafa572016-03-11 14:56:44 +0100227
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100228 def send(self, data):
229 response = super(Subnet, self).send(data)
230 res_json = json.loads(response)
231 self._process_iprange(res_json['id'])
232 return response
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100233
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100234 def _process_iprange(self, subnet_id):
235 ipranges = json.loads(self._maas.get(u'api/2.0/ipranges/').read())
236 LOG.warn('all %s ipranges %s', subnet_id, ipranges)
237 update = False
238 old_data = None
239 for iprange in ipranges:
240 if iprange['subnet']['id'] == subnet_id:
241 update = True
242 old_data = iprange
243 break
244 data = {
245 'start_ip': self._iprange.get('start'),
246 'end_ip': self._iprange.get('end'),
247 'subnet': str(subnet_id),
248 'type': self._iprange.get('type', 'dynamic')
249 }
250 LOG.warn('INFO: %s\n OLD: %s', data, old_data)
251 LOG.info('iprange %s', _format_data(data))
252 if update:
253 LOG.warn('UPDATING %s %s', data, old_data)
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200254 self._maas.put(u'api/2.0/ipranges/{0}/'.format(old_data['id']),
255 **data)
smolaonc3385f82016-03-11 19:01:24 +0100256 else:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100257 self._maas.post(u'api/2.0/ipranges/', None, **data)
smolaonc3385f82016-03-11 19:01:24 +0100258
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200259
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100260class DHCPSnippet(MaasObject):
261 def __init__(self):
262 super(DHCPSnippet, self).__init__()
263 self._all_elements_url = u'api/2.0/dhcp-snippets/'
264 self._create_url = u'api/2.0/dhcp-snippets/'
265 self._update_url = u'api/2.0/dhcp-snippets/{0}/'
266 self._config_path = 'region.dhcp_snippets'
267 self._extra_data_urls = {'subnets': u'api/2.0/subnets/'}
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100268
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100269 def fill_data(self, name, snippet, subnets):
270 data = {
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200271 'name': name,
272 'value': snippet['value'],
273 'description': snippet['description'],
274 'enabled': str(snippet['enabled'] and 1 or 0),
275 'subnet': str(subnets[snippet['subnet']]),
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100276 }
277 return data
smolaonc3385f82016-03-11 19:01:24 +0100278
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100279 def update(self, new, old):
280 new['id'] = str(old['id'])
281 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100282
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200283
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100284class PacketRepository(MaasObject):
285 def __init__(self):
286 super(PacketRepository, self).__init__()
287 self._all_elements_url = u'api/2.0/package-repositories/'
288 self._create_url = u'api/2.0/package-repositories/'
289 self._update_url = u'api/2.0/package-repositories/{0}/'
290 self._config_path = 'region.package_repositories'
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100291
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100292 def fill_data(self, name, package_repository):
293 data = {
294 'name': name,
295 'url': package_repository['url'],
296 'distributions': package_repository['distributions'],
297 'components': package_repository['components'],
298 'arches': package_repository['arches'],
299 'key': package_repository['key'],
300 'enabled': str(package_repository['enabled'] and 1 or 0),
301 }
302 if 'disabled_pockets' in package_repository:
303 data['disabled_pockets'] = package_repository['disable_pockets'],
304 return data
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100305
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100306 def update(self, new, old):
307 new['id'] = str(old['id'])
308 return new
Krzysztof Szukiełojć15b62b72017-02-15 08:58:18 +0100309
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200310
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100311class Device(MaasObject):
312 def __init__(self):
313 super(Device, self).__init__()
314 self._all_elements_url = u'api/2.0/devices/'
315 self._create_url = u'api/2.0/devices/'
316 self._update_url = u'api/2.0/devices/{0}/'
317 self._config_path = 'region.devices'
318 self._element_key = 'hostname'
319 self._update_key = 'system_id'
smolaonc3385f82016-03-11 19:01:24 +0100320
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100321 def fill_data(self, name, device_data):
322 data = {
323 'mac_addresses': device_data['mac'],
324 'hostname': name,
325 }
326 self._interface = device_data['interface']
327 return data
328
329 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100330 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
331 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100332 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100333 LOG.info('Mac changed deleting old device %s', old['system_id'])
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100334 self._maas.delete(u'api/2.0/devices/{0}/'.format(old['system_id']))
335 else:
336 new[self._update_key] = str(old[self._update_key])
337 return new
338
339 def send(self, data):
340 response = super(Device, self).send(data)
341 resp_json = json.loads(response)
342 system_id = resp_json['system_id']
343 iface_id = resp_json['interface_set'][0]['id']
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100344 self._link_interface(system_id, iface_id)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100345 return response
346
347 def _link_interface(self, system_id, interface_id):
348 data = {
349 'mode': self._interface.get('mode', 'STATIC'),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100350 'subnet': self._interface['subnet'],
351 'ip_address': self._interface['ip_address'],
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100352 }
353 if 'default_gateway' in self._interface:
354 data['default_gateway'] = self._interface.get('default_gateway')
355 if self._update:
356 data['force'] = '1'
357 LOG.info('interfaces link_subnet %s %s %s', system_id, interface_id,
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200358 _format_data(data))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100359 self._maas.post(u'/api/2.0/nodes/{0}/interfaces/{1}/'
360 .format(system_id, interface_id), 'link_subnet',
361 **data)
362
363
364class Machine(MaasObject):
365 def __init__(self):
366 super(Machine, self).__init__()
367 self._all_elements_url = u'api/2.0/machines/'
368 self._create_url = u'api/2.0/machines/'
369 self._update_url = u'api/2.0/machines/{0}/'
370 self._config_path = 'region.machines'
371 self._element_key = 'hostname'
372 self._update_key = 'system_id'
373
374 def fill_data(self, name, machine_data):
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100375 power_data = machine_data['power_parameters']
azvyagintsev06b71e72017-11-08 17:11:07 +0200376 machine_pxe_mac = machine_data.get('pxe_interface_mac', None)
377 if machine_data.get("interface", None):
378 LOG.warning(
379 "Old machine-describe detected! "
380 "Please read documentation for "
381 "'salt-formulas/maas' for migration!")
382 machine_pxe_mac = machine_data['interface'].get('mac', None)
383 if not machine_pxe_mac:
384 raise Exception("PXE MAC for machine:{} not defined".format(name))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100385 data = {
386 'hostname': name,
387 'architecture': machine_data.get('architecture', 'amd64/generic'),
azvyagintsev06b71e72017-11-08 17:11:07 +0200388 'mac_addresses': machine_pxe_mac,
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100389 'power_type': machine_data.get('power_type', 'ipmi'),
390 'power_parameters_power_address': power_data['power_address'],
391 }
Ondrej Smola455003c2017-06-01 22:53:39 +0200392 if 'power_driver' in power_data:
393 data['power_parameters_power_driver'] = power_data['power_driver']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100394 if 'power_user' in power_data:
395 data['power_parameters_power_user'] = power_data['power_user']
396 if 'power_password' in power_data:
397 data['power_parameters_power_pass'] = \
398 power_data['power_password']
Petr Ruzicka5fe96742017-11-10 14:22:24 +0100399 if 'power_id' in power_data:
400 data['power_parameters_power_id'] = power_data['power_id']
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100401 return data
402
403 def update(self, new, old):
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100404 old_macs = set(v['mac_address'].lower() for v in old['interface_set'])
405 if new['mac_addresses'].lower() not in old_macs:
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100406 self._update = False
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100407 LOG.info('Mac changed deleting old machine %s', old['system_id'])
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200408 self._maas.delete(u'api/2.0/machines/{0}/'
409 .format(old['system_id']))
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100410 else:
411 new[self._update_key] = str(old[self._update_key])
412 return new
413
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200414
415class AssignMachinesIP(MaasObject):
azvyagintsev06b71e72017-11-08 17:11:07 +0200416 # FIXME
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200417 READY = 4
Andreyef156992017-07-03 14:54:03 -0500418 DEPLOYED = 6
Krzysztof Szukiełojćd6ee1a02017-04-07 14:01:30 +0200419
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200420 def __init__(self):
421 super(AssignMachinesIP, self).__init__()
422 self._all_elements_url = None
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200423 self._create_url = \
424 (u'/api/2.0/nodes/{system_id}/interfaces/{interface_id}/',
425 'link_subnet')
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200426 self._config_path = 'region.machines'
427 self._element_key = 'hostname'
428 self._update_key = 'system_id'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200429 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
430 None, 'hostname')}
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200431
azvyagintsev06b71e72017-11-08 17:11:07 +0200432 def _data_old(self, _interface, _machine):
433 """
434 _interface = {
435 "mac": "11:22:33:44:55:77",
436 "mode": "STATIC",
437 "ip": "2.2.3.15",
438 "subnet": "subnet1",
439 "gateway": "2.2.3.2",
440 }
441 :param data:
442 :return:
443 """
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100444 data = {
445 'mode': 'STATIC',
azvyagintsev06b71e72017-11-08 17:11:07 +0200446 'subnet': str(_interface.get('subnet')),
447 'ip_address': str(_interface.get('ip')),
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100448 }
azvyagintsev06b71e72017-11-08 17:11:07 +0200449 if 'gateway' in _interface:
450 data['default_gateway'] = _interface.get('gateway')
Krzysztof Szukiełojć9449af12017-04-11 14:01:30 +0200451 data['force'] = '1'
azvyagintsev06b71e72017-11-08 17:11:07 +0200452 data['system_id'] = str(_machine['system_id'])
453 data['interface_id'] = str(_machine['interface_set'][0]['id'])
Krzysztof Szukiełojćd57a32d2017-04-04 11:25:02 +0200454 return data
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100455
azvyagintsev06b71e72017-11-08 17:11:07 +0200456 def _get_nic_id_by_mac(self, machine, req_mac=None):
457 data = {}
458 for nic in machine['interface_set']:
459 data[nic['mac_address']] = nic['id']
460 if req_mac:
461 if req_mac in data.keys():
462 return data[req_mac]
463 else:
464 raise Exception('NIC with mac:{} not found at '
465 'node:{}'.format(req_mac, machine['fqdn']))
466 return data
467
468 def _disconnect_all_nic(self, machine):
469 """
470 Maas will fail, in case same config's will be to apply
471 on different interfaces. In same time - not possible to push
472 whole network schema at once. Before configuring - need to clean-up
473 everything
474 :param machine:
475 :return:
476 """
477 for nic in machine['interface_set']:
478 LOG.debug("Disconnecting interface:{}".format(nic['mac_address']))
479 try:
480 self._maas.post(
481 u'/api/2.0/nodes/{}/interfaces/{}/'.format(
482 machine['system_id'], nic['id']), 'disconnect')
483 except Exception as e:
484 LOG.error("Failed to disconnect interface:{} on node:{}".format(
485 nic['mac_address'], machine['fqdn']))
486 raise Exception(str(e))
487
488 def _process_interface(self, nic_data, machine):
489 """
490 Process exactly one interface:
491 - update interface
492 - link to network
493 These functions are self-complementary, and do not require an
494 external "process" method. Those broke old-MaasObject logic,
495 though make functions more simple in case iterable tasks.
496 """
497 nic_id = self._get_nic_id_by_mac(machine, nic_data['mac'])
498
499 # Process op=link_subnet
500 link_data = {}
501 _mode = nic_data.get('mode', 'AUTO').upper()
502 if _mode == 'STATIC':
503 link_data = {
504 'mode': 'STATIC',
505 'subnet': str(nic_data.get('subnet')),
506 'ip_address': str(nic_data.get('ip')),
507 'default_gateway': str(nic_data.get('gateway', "")),
508 }
509 elif _mode == 'DHCP':
510 link_data = {
511 'mode': 'DHCP',
512 'subnet': str(nic_data.get('subnet')),
513 }
514 elif _mode == 'AUTO':
515 link_data = {'mode': 'AUTO',
516 'default_gateway': str(nic_data.get('gateway', "")), }
517 elif _mode in ('LINK_UP', 'UNCONFIGURED'):
518 link_data = {'mode': 'LINK_UP'}
519 else:
520 raise Exception('Wrong IP mode:{}'
521 ' for node:{}'.format(_mode, machine['fqdn']))
522 link_data['force'] = str(1)
523
524 physical_data = {"name": nic_data.get("name", ""),
525 "tags": nic_data.get('tags', ""),
526 "vlan": nic_data.get('vlan', "")}
527
528 try:
529 # Cleanup-old definition
530 LOG.debug("Processing {}:{},{}".format(nic_data['mac'], link_data,
531 physical_data))
532 # "link_subnet" and "fill all other data" - its 2 different
533 # operations. So, first we update NIC:
534 self._maas.put(
535 u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
536 nic_id),
537 **physical_data)
538 # And then, link subnet configuration:
539 self._maas.post(
540 u'/api/2.0/nodes/{}/interfaces/{}/'.format(machine['system_id'],
541 nic_id),
542 'link_subnet', **link_data)
543 except Exception as e:
544 LOG.error("Failed to process interface:{} on node:{}".format(
545 nic_data['mac'], machine['fqdn']))
546 raise Exception(str(e))
547
548 def fill_data(self, name, data, machines):
549 machine = machines[name]
550 if machine['status'] == self.DEPLOYED:
551 LOG.debug("Skipping node:{} "
552 "since it in status:DEPLOYED".format(name))
553 return
554 if machine['status'] != self.READY:
555 raise Exception('Machine:{} not in status:READY'.format(name))
556 # backward comparability, for old schema
557 if data.get("interface", None):
558 if 'ip' not in data["interface"]:
559 LOG.info("No IP NIC definition for:{}".format(name))
560 return
561 LOG.warning(
562 "Old machine-describe detected! "
563 "Please read documentation "
564 "'salt-formulas/maas' for migration!")
565 return self._data_old(data['interface'], machines[name])
566 # NewSchema processing:
567 # Warning: old-style MaasObject.process still be called, but
568 # with empty data for process.
569 interfaces = data.get('interfaces', {})
570 if len(interfaces.keys()) == 0:
571 LOG.info("No IP NIC definition for:{}".format(name))
572 return
573 LOG.info('%s for %s', self.__class__.__name__.lower(),
574 machine['fqdn'])
575 self._disconnect_all_nic(machine)
576 for key, value in sorted(interfaces.iteritems()):
577 self._process_interface(value, machine)
578
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100579
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200580class DeployMachines(MaasObject):
azvyagintsev7605a662017-11-03 19:05:04 +0200581 # FIXME
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200582 READY = 4
Andreyef156992017-07-03 14:54:03 -0500583 DEPLOYED = 6
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200584
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200585 def __init__(self):
586 super(DeployMachines, self).__init__()
587 self._all_elements_url = None
588 self._create_url = (u'api/2.0/machines/{system_id}/', 'deploy')
589 self._config_path = 'region.machines'
590 self._element_key = 'hostname'
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200591 self._extra_data_urls = {'machines': (u'api/2.0/machines/',
592 None, 'hostname')}
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200593
594 def fill_data(self, name, machine_data, machines):
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200595 machine = machines[name]
Andreyef156992017-07-03 14:54:03 -0500596 if machine['status'] == self.DEPLOYED:
597 return
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200598 if machine['status'] != self.READY:
599 raise Exception('Not in ready state')
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200600 data = {
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200601 'system_id': machine['system_id'],
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200602 }
Krzysztof Szukiełojć32677bf2017-04-13 11:04:25 +0200603 if 'distro_series' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200604 data['distro_series'] = machine_data['distro_series']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200605 if 'hwe_kernel' in machine_data:
Krzysztof Szukiełojć008d7d42017-04-05 15:26:01 +0200606 data['hwe_kernel'] = machine_data['hwe_kernel']
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200607 return data
608
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200609 def send(self, data):
610 LOG.info('%s %s', self.__class__.__name__.lower(), _format_data(data))
Krzysztof Szukiełojć3b7516d2017-04-12 11:52:55 +0200611 self._maas.post(u'api/2.0/machines/', 'allocate', system_id=data['system_id']).read()
Krzysztof Szukiełojć33f9b592017-04-12 11:43:50 +0200612 return self._maas.post(self._create_url[0].format(**data),
613 *self._create_url[1:], **data).read()
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200614
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100615class BootResource(MaasObject):
616 def __init__(self):
617 super(BootResource, self).__init__()
618 self._all_elements_url = u'api/2.0/boot-resources/'
619 self._create_url = u'api/2.0/boot-resources/'
620 self._update_url = u'api/2.0/boot-resources/{0}/'
621 self._config_path = 'region.boot_resources'
622
623 def fill_data(self, name, boot_data):
624 sha256 = hashlib.sha256()
625 sha256.update(file(boot_data['content']).read())
626 data = {
627 'name': name,
628 'title': boot_data['title'],
629 'architecture': boot_data['architecture'],
630 'filetype': boot_data['filetype'],
631 'size': str(os.path.getsize(boot_data['content'])),
632 'sha256': sha256.hexdigest(),
633 'content': io.open(boot_data['content']),
634 }
635 return data
636
637 def update(self, new, old):
638 self._update = False
639 return new
640
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200641
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100642class CommissioningScripts(MaasObject):
643 def __init__(self):
644 super(CommissioningScripts, self).__init__()
645 self._all_elements_url = u'api/2.0/commissioning-scripts/'
646 self._create_url = u'api/2.0/commissioning-scripts/'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100647 self._config_path = 'region.commissioning_scripts'
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100648 self._update_url = u'api/2.0/commissioning-scripts/{0}'
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100649 self._update_key = 'name'
650
651 def fill_data(self, name, file_path):
652 data = {
653 'name': name,
654 'content': io.open(file_path),
655 }
656 return data
657
658 def update(self, new, old):
659 return new
660
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200661
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100662class MaasConfig(MaasObject):
663 def __init__(self):
664 super(MaasConfig, self).__init__()
665 self._all_elements_url = None
666 self._create_url = (u'api/2.0/maas/', u'set_config')
667 self._config_path = 'region.maas_config'
668
669 def fill_data(self, name, value):
670 data = {
671 'name': name,
Krzysztof Szukiełojća6352a42017-03-17 14:21:57 +0100672 'value': str(value),
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100673 }
674 return data
675
676 def update(self, new, old):
677 self._update = False
678 return new
679
680
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200681class SSHPrefs(MaasObject):
682 def __init__(self):
683 super(SSHPrefs, self).__init__()
684 self._all_elements_url = None
685 self._create_url = u'api/2.0/account/prefs/sshkeys/'
686 self._config_path = 'region.sshprefs'
687 self._element_key = 'hostname'
688 self._update_key = 'system_id'
689
690 def fill_data(self, value):
691 data = {
692 'key': value,
693 }
694 return data
695
696 def process(self):
697 config = __salt__['config.get']('maas')
698 for part in self._config_path.split('.'):
699 config = config.get(part, {})
700 extra = {}
701 for name, url_call in self._extra_data_urls.iteritems():
702 key = 'id'
703 if isinstance(url_call, tuple):
704 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200705 json_res = json.loads(self._maas.get(url_call).read())
706 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200707 if self._all_elements_url:
708 all_elements = {}
709 elements = self._maas.get(self._all_elements_url).read()
710 res_json = json.loads(elements)
711 for element in res_json:
712 if isinstance(element, (str, unicode)):
713 all_elements[element] = {}
714 else:
715 all_elements[element[self._element_key]] = element
716 else:
717 all_elements = {}
718 ret = {
719 'success': [],
720 'errors': {},
721 'updated': [],
722 }
723 for config_data in config:
724 name = config_data[:10]
725 try:
726 data = self.fill_data(config_data, **extra)
727 self.send(data)
728 ret['success'].append(name)
729 except urllib2.HTTPError as e:
730 etxt = e.read()
731 LOG.exception('Failed for object %s reason %s', name, etxt)
732 ret['errors'][name] = str(etxt)
733 except Exception as e:
734 LOG.exception('Failed for object %s reason %s', name, e)
735 ret['errors'][name] = str(e)
736 if ret['errors']:
737 raise Exception(ret)
738 return ret
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200739
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200740
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200741class Domain(MaasObject):
742 def __init__(self):
743 super(Domain, self).__init__()
744 self._all_elements_url = u'/api/2.0/domains/'
745 self._create_url = u'/api/2.0/domains/'
746 self._config_path = 'region.domain'
747 self._update_url = u'/api/2.0/domains/{0}/'
748
749 def fill_data(self, value):
750 data = {
751 'name': value,
752 }
753 self._update = True
754 return data
755
756 def update(self, new, old):
757 new['id'] = str(old['id'])
758 new['authoritative'] = str(old['authoritative'])
759 return new
760
761 def process(self):
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200762 ret = {
763 'success': [],
764 'errors': {},
765 'updated': [],
766 }
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200767 config = __salt__['config.get']('maas')
768 for part in self._config_path.split('.'):
769 config = config.get(part, {})
770 extra = {}
771 for name, url_call in self._extra_data_urls.iteritems():
772 key = 'id'
773 if isinstance(url_call, tuple):
774 url_call, key = url_call[:]
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200775 json_res = json.loads(self._maas.get(url_call).read())
776 extra[name] = {v['name']: v[key] for v in json_res}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200777 if self._all_elements_url:
778 all_elements = {}
779 elements = self._maas.get(self._all_elements_url).read()
780 res_json = json.loads(elements)
781 for element in res_json:
782 if isinstance(element, (str, unicode)):
783 all_elements[element] = {}
784 else:
785 all_elements[element[self._element_key]] = element
786 else:
787 all_elements = {}
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200788 try:
789 data = self.fill_data(config, **extra)
790 data = self.update(data, all_elements.values()[0])
791 self.send(data)
792 ret['success'].append('domain')
793 except urllib2.HTTPError as e:
794 etxt = e.read()
795 LOG.exception('Failed for object %s reason %s', 'domain', etxt)
796 ret['errors']['domain'] = str(etxt)
797 except Exception as e:
798 LOG.exception('Failed for object %s reason %s', 'domain', e)
799 ret['errors']['domain'] = str(e)
800 if ret['errors']:
801 raise Exception(ret)
802 return ret
803
804
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200805class MachinesStatus(MaasObject):
806 @classmethod
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200807 def execute(cls, objects_name=None):
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200808 cls._maas = _create_maas_client()
809 result = cls._maas.get(u'api/2.0/machines/')
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200810 json_result = json.loads(result.read())
811 res = []
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200812 summary = collections.Counter()
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200813 if objects_name:
814 if ',' in objects_name:
815 objects_name = set(objects_name.split(','))
816 else:
817 objects_name = set([objects_name])
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200818 for machine in json_result:
Krzysztof Szukiełojćf5062202017-04-11 12:33:36 +0200819 if objects_name and machine['hostname'] not in objects_name:
Krzysztof Szukiełojć2497cdb2017-04-11 09:50:28 +0200820 continue
Michael Polenchuke438bd32017-11-09 20:42:42 +0400821 status = STATUS_NAME_DICT[machine['status']]
Krzysztof Szukiełojć0be1a162017-04-04 11:59:09 +0200822 summary[status] += 1
azvyagintsev7605a662017-11-03 19:05:04 +0200823 res.append(
824 {'hostname': machine['hostname'],
825 'system_id': machine['system_id'],
826 'status': status})
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200827 return {'machines': res, 'summary': summary}
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200828
azvyagintsev7605a662017-11-03 19:05:04 +0200829 @classmethod
830 def wait_for_machine_status(cls, **kwargs):
831 """
832 A function that wait for any requested status, for any set of maas
833 machines.
834
835 If no kwargs has been passed - will try to wait ALL
836 defined in salt::maas::region::machines
837
838 See readme file for more examples.
839 CLI Example:
840 .. code-block:: bash
841
842 salt-call state.apply maas.machines.wait_for_deployed
843
844 :param kwargs:
845 timeout: in s; Global timeout for wait
846 poll_time: in s;Sleep time, between retry
847 req_status: string; Polling status
848 machines: list; machine names
849 ignore_machines: list; machine names
850 :ret: True
851 Exception - if something fail/timeout reached
852 """
853 timeout = kwargs.get("timeout", 60 * 120)
854 poll_time = kwargs.get("poll_time", 30)
855 req_status = kwargs.get("req_status", "Ready")
856 to_discover = kwargs.get("machines", None)
857 ignore_machines = kwargs.get("ignore_machines", None)
858 if not to_discover:
859 try:
860 to_discover = __salt__['config.get']('maas')['region'][
861 'machines'].keys()
862 except KeyError:
863 LOG.warning("No defined machines!")
864 return True
865 total = copy.deepcopy(to_discover) or []
866 if ignore_machines and total:
867 total = [x for x in to_discover if x not in ignore_machines]
868 started_at = time.time()
869 while len(total) <= len(to_discover):
870 for m in to_discover:
871 for discovered in MachinesStatus.execute()['machines']:
872 if m == discovered['hostname'] and \
Michael Polenchuke438bd32017-11-09 20:42:42 +0400873 discovered['status'].lower() == req_status.lower():
874 if m in total:
875 total.remove(m)
876
azvyagintsev7605a662017-11-03 19:05:04 +0200877 if len(total) <= 0:
878 LOG.debug(
879 "Machines:{} are:{}".format(to_discover, req_status))
880 return True
881 if (timeout - (time.time() - started_at)) <= 0:
882 raise Exception(
883 'Machines:{}not in {} state'.format(total, req_status))
884 LOG.info(
885 "Waiting status:{} "
886 "for machines:{}"
887 "\nsleep for:{}s "
888 "Timeout:{}s".format(req_status, total, poll_time, timeout))
889 time.sleep(poll_time)
890
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200891
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100892def process_fabrics():
893 return Fabric().process()
894
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200895
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100896def process_subnets():
897 return Subnet().process()
898
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200899
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100900def process_dhcp_snippets():
901 return DHCPSnippet().process()
902
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200903
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100904def process_package_repositories():
905 return PacketRepository().process()
906
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200907
Krzysztof Szukiełojć889eee92017-04-14 11:45:35 +0200908def process_devices(*args):
909 return Device().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100910
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200911
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200912def process_machines(*args):
913 return Machine().process(*args)
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100914
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200915
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200916def process_assign_machines_ip(*args):
azvyagintsev06b71e72017-11-08 17:11:07 +0200917 """
918 Manage interface configurations.
919 See readme.rst for more info
920 """
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200921 return AssignMachinesIP().process(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200922
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200923
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200924def machines_status(*args):
925 return MachinesStatus.execute(*args)
Krzysztof Szukiełojć04e18332017-04-04 11:51:44 +0200926
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200927
Krzysztof Szukiełojć222a3eb2017-04-11 09:39:07 +0200928def deploy_machines(*args):
929 return DeployMachines().process(*args)
Krzysztof Szukiełojć7c16e052017-04-05 10:04:45 +0200930
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200931
Krzysztof Szukiełojćc4b33092017-02-15 13:25:38 +0100932def process_boot_resources():
933 return BootResource().process()
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100934
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200935
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100936def process_maas_config():
937 return MaasConfig().process()
938
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200939
Krzysztof Szukiełojć43bc7e02017-03-17 10:32:07 +0100940def process_commissioning_scripts():
941 return CommissioningScripts().process()
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200942
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200943
Krzysztof Szukiełojć8cc32b42017-03-29 15:22:57 +0200944def process_domain():
945 return Domain().process()
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200946
Krzysztof Szukiełojćb3216752017-04-05 15:50:41 +0200947
Krzysztof Szukiełojća1bd77e2017-03-30 08:34:22 +0200948def process_sshprefs():
949 return SSHPrefs().process()
azvyagintsev7605a662017-11-03 19:05:04 +0200950
951
952def wait_for_machine_status(**kwargs):
953 return MachinesStatus.wait_for_machine_status(**kwargs)