blob: 150cc15aab87b44a8f8fd321f34fa2dbd8b1893e [file] [log] [blame]
Oleksiy Petrenko243475c2019-01-09 14:20:03 +02001import collections
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +03002import logging
Oleksiy Petrenko243475c2019-01-09 14:20:03 +02003import time
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +03004
5log = logging.getLogger(__name__)
6
7
8def __virtual__():
9 return 'ironicv1' if 'ironicv1.node_list' in __salt__ else False
10
11
12def _ironicv1_call(fname, *args, **kwargs):
13 return __salt__['ironicv1.{}'.format(fname)](*args, **kwargs)
14
15
16def node_present(name, cloud_name, driver, **kwargs):
17 resource = 'node'
18 microversion = kwargs.pop('microversion', '1.16')
19 try:
20 method_name = '{}_get_details'.format(resource)
21 exact_resource = _ironicv1_call(
22 method_name, name, cloud_name=cloud_name,
23 microversion=microversion
24 )
25 except Exception as e:
26 if 'Not Found' in str(e):
27 try:
28 method_name = '{}_create'.format(resource)
29 resp = _ironicv1_call(
30 method_name, driver, name=name, cloud_name=cloud_name,
31 microversion=microversion,
32 **kwargs
33 )
34 except Exception as e:
35 log.exception('Ironic {0} create failed with {1}'.
36 format('node', e))
37 return _failed('create', name, resource)
38 return _succeeded('create', name, resource, resp)
Vladyslav Drok0ad5d012018-10-05 17:51:34 +030039 raise
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +030040
41 to_change = []
42 for prop in kwargs:
43 path = prop.replace('~', '~0').replace('/', '~1')
44 if prop in exact_resource:
45 if exact_resource[prop] != kwargs[prop]:
46 to_change.append({
47 'op': 'replace',
48 'path': '/{}'.format(path),
49 'value': kwargs[prop],
50 })
51 else:
52 to_change.append({
53 'op': 'add',
54 'path': '/{}'.format(path),
55 'value': kwargs[prop],
56 })
57 if to_change:
58 try:
59 method_name = '{}_update'.format(resource)
60 resp = _ironicv1_call(
61 method_name, name, properties=to_change,
62 microversion=microversion, cloud_name=cloud_name,
63 )
64 except Exception as e:
65 log.exception(
66 'Ironic {0} update failed with {1}'.format(resource, e))
67 return _failed('update', name, resource)
68 return _succeeded('update', name, resource, resp)
69 return _succeeded('no_changes', name, resource)
70
71
72def node_absent(name, cloud_name, **kwargs):
73 resource = 'node'
74 microversion = kwargs.pop('microversion', '1.16')
75 try:
76 method_name = '{}_get_details'.format(resource)
77 _ironicv1_call(
78 method_name, name, cloud_name=cloud_name,
79 microversion=microversion
80 )
81 except Exception as e:
82 if 'Not Found' in str(e):
83 return _succeeded('absent', name, resource)
84 try:
85 method_name = '{}_delete'.format(resource)
86 _ironicv1_call(
87 method_name, name, cloud_name=cloud_name, microversion=microversion
88 )
89 except Exception as e:
90 log.error('Ironic delete {0} failed with {1}'.format(resource, e))
91 return _failed('delete', name, resource)
92 return _succeeded('delete', name, resource)
93
94
95def port_present(name, cloud_name, node, address, **kwargs):
96 resource = 'port'
97 microversion = kwargs.pop('microversion', '1.16')
98 method_name = '{}_list'.format(resource)
99 exact_resource = _ironicv1_call(
100 method_name, node=node, address=address,
101 cloud_name=cloud_name, microversion=microversion
102 )['ports']
103 if len(exact_resource) == 0:
104 try:
105 node_uuid = _ironicv1_call(
106 'node_get_details', node, cloud_name=cloud_name,
107 microversion=microversion
108 )['uuid']
109 except Exception as e:
110 return _failed('create', node, "port's node")
111 try:
112 method_name = '{}_create'.format(resource)
113 resp = _ironicv1_call(
114 method_name, node_uuid, address, cloud_name=cloud_name,
115 microversion=microversion, **kwargs)
116 except Exception as e:
117 log.exception('Ironic {0} create failed with {1}'.
118 format('node', e))
119 return _failed('create', name, resource)
120 return _succeeded('create', name, resource, resp)
121 if len(exact_resource) == 1:
122 exact_resource = exact_resource[0]
123 to_change = []
124 for prop in kwargs:
125 path = prop.replace('~', '~0').replace('/', '~1')
126 if prop in exact_resource:
127 if exact_resource[prop] != kwargs[prop]:
128 to_change.append({
129 'op': 'replace',
130 'path': '/{}'.format(path),
131 'value': kwargs[prop],
132 })
133 else:
134 to_change.append({
135 'op': 'add',
136 'path': '/{}'.format(path),
137 'value': kwargs[prop],
138 })
139 if to_change:
140 try:
141 method_name = '{}_update'.format(resource)
142 resp = _ironicv1_call(
Oleksiy Petrenkobf66fbd2018-12-17 19:04:47 +0200143 method_name, exact_resource['uuid'], properties=to_change,
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +0300144 microversion=microversion, cloud_name=cloud_name,
145 )
146 except Exception as e:
147 log.exception(
148 'Ironic {0} update failed with {1}'.format(resource, e))
149 return _failed('update', name, resource)
150 return _succeeded('update', name, resource, resp)
151 return _succeeded('no_changes', name, resource)
152 else:
153 return _failed('find', name, resource)
154
155
156def port_absent(name, cloud_name, node, address, **kwargs):
157 resource = 'port'
158 microversion = kwargs.pop('microversion', '1.16')
159 method_name = '{}_list'.format(resource)
160 exact_resource = _ironicv1_call(
161 method_name, node=node, address=address,
162 cloud_name=cloud_name, microversion=microversion
163 )['ports']
164 if len(exact_resource) == 0:
165 return _succeeded('absent', name, resource)
166 elif len(exact_resource) == 1:
167 port_id = exact_resource[0]['uuid']
168 try:
169 method_name = '{}_delete'.format(resource)
170 _ironicv1_call(
171 method_name, port_id, cloud_name=cloud_name,
172 microversion=microversion
173 )
174 except Exception as e:
175 log.error('Ironic delete {0} failed with {1}'.format(resource, e))
176 return _failed('delete', name, resource)
177 return _succeeded('delete', name, resource)
178 else:
179 return _failed('find', name, resource)
180
181
Oleksiy Petrenkobf66fbd2018-12-17 19:04:47 +0200182def volume_connector_present(name, node, volume_type, cloud_name,
183 **kwargs):
184 """
185
186 :param name: alias for connector_id because of how salt works
187 :param node: node_ident
188 :param volume_type: type of volume
189 """
190 resource = 'volume_connector'
191 microversion = kwargs.pop('microversion', '1.32')
192 method_name = '{}_list'.format(resource)
193 exact_resource = filter(
194 lambda data: data['connector_id'] == name,
195 _ironicv1_call(method_name, node=node,
196 cloud_name=cloud_name,
197 microversion=microversion)['connectors'])
198 if len(exact_resource) == 0:
199 try:
200 method_name = 'node_get_details'
201 node_uuid = _ironicv1_call(
202 method_name, node, cloud_name=cloud_name,
203 microversion=microversion
204 )['uuid']
205 except Exception as e:
206 if 'Not Found' in str(e):
207 return _failed('not_found', node, 'node')
208 raise
209 try:
210 method_name = '{}_create'.format(resource)
211 resp = _ironicv1_call(
212 method_name, node_uuid, volume_type, name,
213 cloud_name=cloud_name, microversion=microversion, **kwargs)
214 except Exception as e:
215 log.exception('Ironic {0} create failed with {1}'.
216 format('node', e))
217 return _failed('create', name, resource)
218 return _succeeded('create', name, resource, resp)
219 elif len(exact_resource) == 1:
220 exact_resource = exact_resource[0]
221 to_change = []
222 for prop in kwargs:
223 path = prop.replace('~', '~0').replace('/', '~1')
224 if prop in exact_resource:
225 if exact_resource[prop] != kwargs[prop]:
226 to_change.append({
227 'op': 'replace',
228 'path': '/{}'.format(path),
229 'value': kwargs[prop],
230 })
231 else:
232 to_change.append({
233 'op': 'add',
234 'path': '/{}'.format(path),
235 'value': kwargs[prop],
236 })
237 if to_change:
238 try:
239 method_name = '{}_update'.format(resource)
240 resp = _ironicv1_call(
241 method_name, exact_resource['uuid'], properties=to_change,
242 microversion=microversion, cloud_name=cloud_name,
243 )
244 except Exception as e:
245 log.exception(
246 'Ironic {0} update failed with {1}'.format(resource,
247 e))
248 return _failed('update', name, resource)
249 return _succeeded('update', name, resource, resp)
250 return _succeeded('no_changes', name, resource)
251 else:
252 return _failed('find', name, resource)
253
254
255def volume_connector_absent(name, cloud_name, node, **kwargs):
256 """
257
258 :param name: alias for connector_id because of how salt works
259 :param node: node ident
260 """
261 resource = 'volume_connector'
262 microversion = kwargs.pop('microversion', '1.32')
263 method_name = '{}_list'.format(resource)
264 exact_resource = filter(
265 lambda data: data['connector_id'] == name,
266 _ironicv1_call(method_name, node=node,
267 cloud_name=cloud_name,
268 microversion=microversion)['connectors'])
269 if len(exact_resource) == 0:
270 return _succeeded('absent', name, resource)
271 elif len(exact_resource) == 1:
272 connector_uuid = exact_resource[0]['uuid']
273 try:
274 method_name = '{}_delete'.format(resource)
275 _ironicv1_call(
276 method_name, connector_uuid, cloud_name=cloud_name,
277 microversion=microversion
278 )
279 except Exception as e:
280 log.error('Ironic delete {0} failed with {1}'.format(resource, e))
281 return _failed('delete', name, resource)
282 return _succeeded('delete', name, resource)
283 else:
284 return _failed('find', name, resource)
285
286
Vasyl Saienko3529b5e2019-01-19 09:14:36 +0000287def ensure_target_state(name, cloud_name, node_names=None,
288 provision_state=None, pool_size=3, sleep_time=5,
289 timeout=600, **kwargs):
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200290 """
Vasyl Saienko3529b5e2019-01-19 09:14:36 +0000291 Ensures nodes are moved to target state. As node distinguisher might take
292 either list of nodes specified in node names param or provision state.
293 Is designed to move nodes from enroll to available state for now.
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200294
295 :param name: name of target state
296 :param node_names: list of node names
Vasyl Saienko3529b5e2019-01-19 09:14:36 +0000297 :param provision_state: current provision_state to filter nodes by.
298 :param cloud_name: the mane of cloud in clouds.yml
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200299 :param pool_size: max size of nodes to change state in one moment
300 :param sleep_time: time between checking states
301 :param timeout: global timeout
302 """
Vasyl Saienko3529b5e2019-01-19 09:14:36 +0000303
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200304 microversion = kwargs.pop('microversion', '1.32')
Vasyl Saienko3529b5e2019-01-19 09:14:36 +0000305
306 if node_names is None:
307 nodes = _ironicv1_call('node_list', provision_state=provision_state,
308 cloud_name=cloud_name,
309 fields='name',
310 microversion=microversion)['nodes']
311 node_names = [n['name'] for n in nodes]
312
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200313 Transition = collections.namedtuple('Transition',
314 ('action', 'success', 'failures'))
315 transition_map = {
316 'enroll': Transition('manage', 'manageable', ('enroll',)),
317 'manageable': Transition('provide', 'available', ('clean failed',)),
318 'available': Transition('active', 'active', ('deploy failed',)),
319 }
320 nodes = [
321 {'name': node, 'status': 'new', 'result': None,
Vasyl Saienko3529b5e2019-01-19 09:14:36 +0000322 'current_state': provision_state or 'enroll'}
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200323 for node in node_names
324 ]
325 counter = 0
326 while nodes and timeout > 0:
327 for node in nodes:
328 api_node = _ironicv1_call('node_get_details', node['name'],
329 cloud_name=cloud_name,
330 microversion=microversion)
331 if api_node['provision_state'] == name:
332 node['status'] = 'done'
333 node['result'] = 'success'
334 counter -= 1
335 elif (api_node['provision_state']
336 == transition_map[node['current_state']].success):
337 new_state = transition_map[node['current_state']].success
338 _ironicv1_call('node_provision_state_change', node['name'],
339 transition_map[new_state].action,
340 cloud_name=cloud_name,
341 microversion=microversion)
342 node['current_state'] = new_state
343 elif (node['status'] == 'processing'
344 and not api_node['target_provision_state']
345 and (api_node['provision_state']
346 in transition_map[api_node['provision_state']].failures)
347 ):
348 node['status'] = 'done'
349 node['result'] = 'failure'
350 counter -= 1
351 elif counter < pool_size:
352 if node['status'] == 'new':
353 _ironicv1_call(
354 'node_provision_state_change', node['name'],
355 transition_map[node['current_state']].action,
356 cloud_name=cloud_name, microversion=microversion)
357 node['status'] = 'processing'
358 counter += 1
359 else:
360 continue
361 else:
362 break
363 nodes = filter(
364 lambda node: node['status'] in ['new', 'processing'], nodes)
365 time.sleep(sleep_time)
366 timeout -= sleep_time
367 return _succeeded('update', name, 'node_states',
368 {'result': filter(
369 lambda node: node['name'] in node_names,
370 _ironicv1_call('node_list', cloud_name=cloud_name,
371 microversion=microversion)['nodes'])})
372
373
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +0300374def _succeeded(op, name, resource, changes=None):
375 msg_map = {
376 'create': '{0} {1} created',
377 'delete': '{0} {1} removed',
378 'update': '{0} {1} updated',
379 'no_changes': '{0} {1} is in desired state',
380 'absent': '{0} {1} not present'
381 }
382 changes_dict = {
383 'name': name,
384 'result': True,
385 'comment': msg_map[op].format(resource, name),
386 'changes': changes or {},
387 }
388 return changes_dict
389
390
391def _failed(op, name, resource):
392 msg_map = {
393 'create': '{0} {1} failed to create',
394 'delete': '{0} {1} failed to delete',
395 'update': '{0} {1} failed to update',
Oleksiy Petrenkobf66fbd2018-12-17 19:04:47 +0200396 'find': '{0} {1} found multiple {0}',
397 'not found': '{0} {1} not found',
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +0300398 }
399 changes_dict = {
400 'name': name,
401 'result': False,
402 'comment': msg_map[op].format(resource, name),
403 'changes': {},
404 }
405 return changes_dict