blob: 53f7f36b50ca763705c9f7a08daba0edb92292ec [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
Oleksiy Petrenko243475c2019-01-09 14:20:03 +0200287def enroll_to_state(name, node_names, cloud_name,
288 pool_size=3, sleep_time=5, timeout=600, **kwargs):
289 """
290
291 :param name: name of target state
292 :param node_names: list of node names
293 :param cloud_name:
294 :param pool_size: max size of nodes to change state in one moment
295 :param sleep_time: time between checking states
296 :param timeout: global timeout
297 """
298 microversion = kwargs.pop('microversion', '1.32')
299 Transition = collections.namedtuple('Transition',
300 ('action', 'success', 'failures'))
301 transition_map = {
302 'enroll': Transition('manage', 'manageable', ('enroll',)),
303 'manageable': Transition('provide', 'available', ('clean failed',)),
304 'available': Transition('active', 'active', ('deploy failed',)),
305 }
306 nodes = [
307 {'name': node, 'status': 'new', 'result': None,
308 'current_state': 'enroll'}
309 for node in node_names
310 ]
311 counter = 0
312 while nodes and timeout > 0:
313 for node in nodes:
314 api_node = _ironicv1_call('node_get_details', node['name'],
315 cloud_name=cloud_name,
316 microversion=microversion)
317 if api_node['provision_state'] == name:
318 node['status'] = 'done'
319 node['result'] = 'success'
320 counter -= 1
321 elif (api_node['provision_state']
322 == transition_map[node['current_state']].success):
323 new_state = transition_map[node['current_state']].success
324 _ironicv1_call('node_provision_state_change', node['name'],
325 transition_map[new_state].action,
326 cloud_name=cloud_name,
327 microversion=microversion)
328 node['current_state'] = new_state
329 elif (node['status'] == 'processing'
330 and not api_node['target_provision_state']
331 and (api_node['provision_state']
332 in transition_map[api_node['provision_state']].failures)
333 ):
334 node['status'] = 'done'
335 node['result'] = 'failure'
336 counter -= 1
337 elif counter < pool_size:
338 if node['status'] == 'new':
339 _ironicv1_call(
340 'node_provision_state_change', node['name'],
341 transition_map[node['current_state']].action,
342 cloud_name=cloud_name, microversion=microversion)
343 node['status'] = 'processing'
344 counter += 1
345 else:
346 continue
347 else:
348 break
349 nodes = filter(
350 lambda node: node['status'] in ['new', 'processing'], nodes)
351 time.sleep(sleep_time)
352 timeout -= sleep_time
353 return _succeeded('update', name, 'node_states',
354 {'result': filter(
355 lambda node: node['name'] in node_names,
356 _ironicv1_call('node_list', cloud_name=cloud_name,
357 microversion=microversion)['nodes'])})
358
359
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +0300360def _succeeded(op, name, resource, changes=None):
361 msg_map = {
362 'create': '{0} {1} created',
363 'delete': '{0} {1} removed',
364 'update': '{0} {1} updated',
365 'no_changes': '{0} {1} is in desired state',
366 'absent': '{0} {1} not present'
367 }
368 changes_dict = {
369 'name': name,
370 'result': True,
371 'comment': msg_map[op].format(resource, name),
372 'changes': changes or {},
373 }
374 return changes_dict
375
376
377def _failed(op, name, resource):
378 msg_map = {
379 'create': '{0} {1} failed to create',
380 'delete': '{0} {1} failed to delete',
381 'update': '{0} {1} failed to update',
Oleksiy Petrenkobf66fbd2018-12-17 19:04:47 +0200382 'find': '{0} {1} found multiple {0}',
383 'not found': '{0} {1} not found',
Oleksiy Petrenkoa5eb0602018-07-26 15:12:25 +0300384 }
385 changes_dict = {
386 'name': name,
387 'result': False,
388 'comment': msg_map[op].format(resource, name),
389 'changes': {},
390 }
391 return changes_dict