blob: 150cc15aab87b44a8f8fd321f34fa2dbd8b1893e [file] [log] [blame]
import collections
import logging
import time
log = logging.getLogger(__name__)
def __virtual__():
return 'ironicv1' if 'ironicv1.node_list' in __salt__ else False
def _ironicv1_call(fname, *args, **kwargs):
return __salt__['ironicv1.{}'.format(fname)](*args, **kwargs)
def node_present(name, cloud_name, driver, **kwargs):
resource = 'node'
microversion = kwargs.pop('microversion', '1.16')
try:
method_name = '{}_get_details'.format(resource)
exact_resource = _ironicv1_call(
method_name, name, cloud_name=cloud_name,
microversion=microversion
)
except Exception as e:
if 'Not Found' in str(e):
try:
method_name = '{}_create'.format(resource)
resp = _ironicv1_call(
method_name, driver, name=name, cloud_name=cloud_name,
microversion=microversion,
**kwargs
)
except Exception as e:
log.exception('Ironic {0} create failed with {1}'.
format('node', e))
return _failed('create', name, resource)
return _succeeded('create', name, resource, resp)
raise
to_change = []
for prop in kwargs:
path = prop.replace('~', '~0').replace('/', '~1')
if prop in exact_resource:
if exact_resource[prop] != kwargs[prop]:
to_change.append({
'op': 'replace',
'path': '/{}'.format(path),
'value': kwargs[prop],
})
else:
to_change.append({
'op': 'add',
'path': '/{}'.format(path),
'value': kwargs[prop],
})
if to_change:
try:
method_name = '{}_update'.format(resource)
resp = _ironicv1_call(
method_name, name, properties=to_change,
microversion=microversion, cloud_name=cloud_name,
)
except Exception as e:
log.exception(
'Ironic {0} update failed with {1}'.format(resource, e))
return _failed('update', name, resource)
return _succeeded('update', name, resource, resp)
return _succeeded('no_changes', name, resource)
def node_absent(name, cloud_name, **kwargs):
resource = 'node'
microversion = kwargs.pop('microversion', '1.16')
try:
method_name = '{}_get_details'.format(resource)
_ironicv1_call(
method_name, name, cloud_name=cloud_name,
microversion=microversion
)
except Exception as e:
if 'Not Found' in str(e):
return _succeeded('absent', name, resource)
try:
method_name = '{}_delete'.format(resource)
_ironicv1_call(
method_name, name, cloud_name=cloud_name, microversion=microversion
)
except Exception as e:
log.error('Ironic delete {0} failed with {1}'.format(resource, e))
return _failed('delete', name, resource)
return _succeeded('delete', name, resource)
def port_present(name, cloud_name, node, address, **kwargs):
resource = 'port'
microversion = kwargs.pop('microversion', '1.16')
method_name = '{}_list'.format(resource)
exact_resource = _ironicv1_call(
method_name, node=node, address=address,
cloud_name=cloud_name, microversion=microversion
)['ports']
if len(exact_resource) == 0:
try:
node_uuid = _ironicv1_call(
'node_get_details', node, cloud_name=cloud_name,
microversion=microversion
)['uuid']
except Exception as e:
return _failed('create', node, "port's node")
try:
method_name = '{}_create'.format(resource)
resp = _ironicv1_call(
method_name, node_uuid, address, cloud_name=cloud_name,
microversion=microversion, **kwargs)
except Exception as e:
log.exception('Ironic {0} create failed with {1}'.
format('node', e))
return _failed('create', name, resource)
return _succeeded('create', name, resource, resp)
if len(exact_resource) == 1:
exact_resource = exact_resource[0]
to_change = []
for prop in kwargs:
path = prop.replace('~', '~0').replace('/', '~1')
if prop in exact_resource:
if exact_resource[prop] != kwargs[prop]:
to_change.append({
'op': 'replace',
'path': '/{}'.format(path),
'value': kwargs[prop],
})
else:
to_change.append({
'op': 'add',
'path': '/{}'.format(path),
'value': kwargs[prop],
})
if to_change:
try:
method_name = '{}_update'.format(resource)
resp = _ironicv1_call(
method_name, exact_resource['uuid'], properties=to_change,
microversion=microversion, cloud_name=cloud_name,
)
except Exception as e:
log.exception(
'Ironic {0} update failed with {1}'.format(resource, e))
return _failed('update', name, resource)
return _succeeded('update', name, resource, resp)
return _succeeded('no_changes', name, resource)
else:
return _failed('find', name, resource)
def port_absent(name, cloud_name, node, address, **kwargs):
resource = 'port'
microversion = kwargs.pop('microversion', '1.16')
method_name = '{}_list'.format(resource)
exact_resource = _ironicv1_call(
method_name, node=node, address=address,
cloud_name=cloud_name, microversion=microversion
)['ports']
if len(exact_resource) == 0:
return _succeeded('absent', name, resource)
elif len(exact_resource) == 1:
port_id = exact_resource[0]['uuid']
try:
method_name = '{}_delete'.format(resource)
_ironicv1_call(
method_name, port_id, cloud_name=cloud_name,
microversion=microversion
)
except Exception as e:
log.error('Ironic delete {0} failed with {1}'.format(resource, e))
return _failed('delete', name, resource)
return _succeeded('delete', name, resource)
else:
return _failed('find', name, resource)
def volume_connector_present(name, node, volume_type, cloud_name,
**kwargs):
"""
:param name: alias for connector_id because of how salt works
:param node: node_ident
:param volume_type: type of volume
"""
resource = 'volume_connector'
microversion = kwargs.pop('microversion', '1.32')
method_name = '{}_list'.format(resource)
exact_resource = filter(
lambda data: data['connector_id'] == name,
_ironicv1_call(method_name, node=node,
cloud_name=cloud_name,
microversion=microversion)['connectors'])
if len(exact_resource) == 0:
try:
method_name = 'node_get_details'
node_uuid = _ironicv1_call(
method_name, node, cloud_name=cloud_name,
microversion=microversion
)['uuid']
except Exception as e:
if 'Not Found' in str(e):
return _failed('not_found', node, 'node')
raise
try:
method_name = '{}_create'.format(resource)
resp = _ironicv1_call(
method_name, node_uuid, volume_type, name,
cloud_name=cloud_name, microversion=microversion, **kwargs)
except Exception as e:
log.exception('Ironic {0} create failed with {1}'.
format('node', e))
return _failed('create', name, resource)
return _succeeded('create', name, resource, resp)
elif len(exact_resource) == 1:
exact_resource = exact_resource[0]
to_change = []
for prop in kwargs:
path = prop.replace('~', '~0').replace('/', '~1')
if prop in exact_resource:
if exact_resource[prop] != kwargs[prop]:
to_change.append({
'op': 'replace',
'path': '/{}'.format(path),
'value': kwargs[prop],
})
else:
to_change.append({
'op': 'add',
'path': '/{}'.format(path),
'value': kwargs[prop],
})
if to_change:
try:
method_name = '{}_update'.format(resource)
resp = _ironicv1_call(
method_name, exact_resource['uuid'], properties=to_change,
microversion=microversion, cloud_name=cloud_name,
)
except Exception as e:
log.exception(
'Ironic {0} update failed with {1}'.format(resource,
e))
return _failed('update', name, resource)
return _succeeded('update', name, resource, resp)
return _succeeded('no_changes', name, resource)
else:
return _failed('find', name, resource)
def volume_connector_absent(name, cloud_name, node, **kwargs):
"""
:param name: alias for connector_id because of how salt works
:param node: node ident
"""
resource = 'volume_connector'
microversion = kwargs.pop('microversion', '1.32')
method_name = '{}_list'.format(resource)
exact_resource = filter(
lambda data: data['connector_id'] == name,
_ironicv1_call(method_name, node=node,
cloud_name=cloud_name,
microversion=microversion)['connectors'])
if len(exact_resource) == 0:
return _succeeded('absent', name, resource)
elif len(exact_resource) == 1:
connector_uuid = exact_resource[0]['uuid']
try:
method_name = '{}_delete'.format(resource)
_ironicv1_call(
method_name, connector_uuid, cloud_name=cloud_name,
microversion=microversion
)
except Exception as e:
log.error('Ironic delete {0} failed with {1}'.format(resource, e))
return _failed('delete', name, resource)
return _succeeded('delete', name, resource)
else:
return _failed('find', name, resource)
def ensure_target_state(name, cloud_name, node_names=None,
provision_state=None, pool_size=3, sleep_time=5,
timeout=600, **kwargs):
"""
Ensures nodes are moved to target state. As node distinguisher might take
either list of nodes specified in node names param or provision state.
Is designed to move nodes from enroll to available state for now.
:param name: name of target state
:param node_names: list of node names
:param provision_state: current provision_state to filter nodes by.
:param cloud_name: the mane of cloud in clouds.yml
:param pool_size: max size of nodes to change state in one moment
:param sleep_time: time between checking states
:param timeout: global timeout
"""
microversion = kwargs.pop('microversion', '1.32')
if node_names is None:
nodes = _ironicv1_call('node_list', provision_state=provision_state,
cloud_name=cloud_name,
fields='name',
microversion=microversion)['nodes']
node_names = [n['name'] for n in nodes]
Transition = collections.namedtuple('Transition',
('action', 'success', 'failures'))
transition_map = {
'enroll': Transition('manage', 'manageable', ('enroll',)),
'manageable': Transition('provide', 'available', ('clean failed',)),
'available': Transition('active', 'active', ('deploy failed',)),
}
nodes = [
{'name': node, 'status': 'new', 'result': None,
'current_state': provision_state or 'enroll'}
for node in node_names
]
counter = 0
while nodes and timeout > 0:
for node in nodes:
api_node = _ironicv1_call('node_get_details', node['name'],
cloud_name=cloud_name,
microversion=microversion)
if api_node['provision_state'] == name:
node['status'] = 'done'
node['result'] = 'success'
counter -= 1
elif (api_node['provision_state']
== transition_map[node['current_state']].success):
new_state = transition_map[node['current_state']].success
_ironicv1_call('node_provision_state_change', node['name'],
transition_map[new_state].action,
cloud_name=cloud_name,
microversion=microversion)
node['current_state'] = new_state
elif (node['status'] == 'processing'
and not api_node['target_provision_state']
and (api_node['provision_state']
in transition_map[api_node['provision_state']].failures)
):
node['status'] = 'done'
node['result'] = 'failure'
counter -= 1
elif counter < pool_size:
if node['status'] == 'new':
_ironicv1_call(
'node_provision_state_change', node['name'],
transition_map[node['current_state']].action,
cloud_name=cloud_name, microversion=microversion)
node['status'] = 'processing'
counter += 1
else:
continue
else:
break
nodes = filter(
lambda node: node['status'] in ['new', 'processing'], nodes)
time.sleep(sleep_time)
timeout -= sleep_time
return _succeeded('update', name, 'node_states',
{'result': filter(
lambda node: node['name'] in node_names,
_ironicv1_call('node_list', cloud_name=cloud_name,
microversion=microversion)['nodes'])})
def _succeeded(op, name, resource, changes=None):
msg_map = {
'create': '{0} {1} created',
'delete': '{0} {1} removed',
'update': '{0} {1} updated',
'no_changes': '{0} {1} is in desired state',
'absent': '{0} {1} not present'
}
changes_dict = {
'name': name,
'result': True,
'comment': msg_map[op].format(resource, name),
'changes': changes or {},
}
return changes_dict
def _failed(op, name, resource):
msg_map = {
'create': '{0} {1} failed to create',
'delete': '{0} {1} failed to delete',
'update': '{0} {1} failed to update',
'find': '{0} {1} found multiple {0}',
'not found': '{0} {1} not found',
}
changes_dict = {
'name': name,
'result': False,
'comment': msg_map[op].format(resource, name),
'changes': {},
}
return changes_dict