Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | ''' |
| 3 | Management of Ironic resources |
| 4 | =============================== |
| 5 | :depends: - ironicclient Python module |
| 6 | :configuration: See :py:mod:`salt.modules.ironic` for setup instructions. |
| 7 | .. code-block:: yaml |
| 8 | ironicng node present: |
| 9 | ironicng.node_present: |
| 10 | - name: node-1 |
| 11 | - provider_physical_network: PHysnet1 |
| 12 | - provider_network_type: vlan |
| 13 | ''' |
| 14 | import logging |
| 15 | from functools import wraps |
| 16 | LOG = logging.getLogger(__name__) |
| 17 | |
| 18 | |
| 19 | def __virtual__(): |
| 20 | ''' |
| 21 | Only load if ironic module is present in __salt__ |
| 22 | ''' |
| 23 | return 'ironicng' if 'ironicng.list_nodes' in __salt__ else False |
| 24 | |
| 25 | |
| 26 | def _test_call(method): |
| 27 | (resource, functionality) = method.func_name.split('_') |
| 28 | if functionality == 'present': |
| 29 | functionality = 'updated' |
| 30 | else: |
| 31 | functionality = 'removed' |
| 32 | |
| 33 | @wraps(method) |
| 34 | def check_for_testing(name, *args, **kwargs): |
| 35 | if __opts__.get('test', None): |
| 36 | return _no_change(name, resource, test=functionality) |
| 37 | return method(name, *args, **kwargs) |
| 38 | return check_for_testing |
| 39 | |
| 40 | |
| 41 | def _ironic_module_call(method, *args, **kwargs): |
| 42 | return __salt__['ironicng.{0}'.format(method)](*args, **kwargs) |
| 43 | |
| 44 | def _auth(profile=None, endpoint_type=None, |
| 45 | ironic_api_version=None): |
| 46 | ''' |
| 47 | Set up ironic credentials |
| 48 | ''' |
| 49 | if profile: |
| 50 | credentials = __salt__['config.option'](profile) |
| 51 | user = credentials['keystone.user'] |
| 52 | password = credentials['keystone.password'] |
| 53 | tenant = credentials['keystone.tenant'] |
| 54 | auth_url = credentials['keystone.auth_url'] |
| 55 | kwargs = { |
| 56 | 'connection_user': user, |
| 57 | 'connection_password': password, |
| 58 | 'connection_tenant': tenant, |
| 59 | 'connection_auth_url': auth_url, |
| 60 | 'connection_endpoint_type': endpoint_type, |
| 61 | 'connection_ironic_api_version': ironic_api_version, |
| 62 | 'profile': profile |
| 63 | } |
| 64 | |
| 65 | return kwargs |
| 66 | |
| 67 | @_test_call |
| 68 | def node_present(name, |
| 69 | driver, |
| 70 | driver_info=None, |
| 71 | properties=None, |
| 72 | extra=None, |
| 73 | console_enabled=None, |
| 74 | resource_class=None, |
| 75 | boot_interface=None, |
| 76 | console_interface=None, |
| 77 | deploy_interface=None, |
| 78 | inspect_interface=None, |
| 79 | management_interface=None, |
| 80 | network_interface=None, |
| 81 | power_interface=None, |
| 82 | raid_interface=None, |
| 83 | vendor_interface=None, |
| 84 | maintenance=None, |
| 85 | maintenance_reason=None, |
| 86 | ports=None, |
| 87 | profile=None, |
| 88 | endpoint_type=None, |
| 89 | ironic_api_version=None): |
| 90 | ''' |
| 91 | Ensure that the ironic node is present with the specified properties. |
| 92 | ''' |
| 93 | connection_args = _auth(profile, endpoint_type, |
| 94 | ironic_api_version=ironic_api_version) |
| 95 | |
| 96 | existing_nodes = _ironic_module_call( |
| 97 | 'list_nodes', detail=True, **connection_args)['nodes'] |
| 98 | existing_nodes = [node for node in existing_nodes if node['name'] == name] |
| 99 | |
| 100 | node_arguments = _get_non_null_args( |
| 101 | name=name, |
| 102 | driver=driver, |
| 103 | driver_info=driver_info, |
| 104 | properties=properties, |
| 105 | extra=extra, |
| 106 | console_enabled=console_enabled, |
| 107 | resource_class=resource_class, |
| 108 | boot_interface=boot_interface, |
| 109 | console_interface=console_interface, |
| 110 | deploy_interface=deploy_interface, |
| 111 | inspect_interface=inspect_interface, |
| 112 | management_interface=management_interface, |
| 113 | network_interface=network_interface, |
| 114 | power_interface=power_interface, |
| 115 | raid_interface=raid_interface, |
| 116 | vendor_interface=vendor_interface, |
| 117 | maintenance=maintenance, |
| 118 | maintenance_reason=maintenance_reason) |
| 119 | |
| 120 | # In ironic node names are unique |
| 121 | if len(existing_nodes) == 0: |
| 122 | node_arguments.update(connection_args) |
| 123 | res = _ironic_module_call( |
| 124 | 'create_node', **node_arguments) |
| 125 | |
| 126 | if res.get('name') == name: |
| 127 | return _created(name, 'node', res) |
| 128 | |
| 129 | elif len(existing_nodes) == 1: |
| 130 | # TODO(vsaienko) add update with deep comparison |
| 131 | return _no_change(name, 'node') |
| 132 | existing_node = existing_nodes[0] |
| 133 | return _create_failed(name, 'node') |
| 134 | |
| 135 | |
| 136 | @_test_call |
| 137 | def node_absent(name, uuid=None, profile=None, endpoint_type=None): |
| 138 | connection_args = _auth(profile, endpoint_type) |
| 139 | identifier = uuid or name |
| 140 | _ironic_module_call( |
| 141 | 'delete_node', identifier, **connection_args) |
| 142 | return _absent(identifier, 'node') |
| 143 | |
| 144 | @_test_call |
| 145 | def port_present(name, |
| 146 | address, |
| 147 | node_name=None, |
| 148 | node_uuid=None, |
| 149 | local_link_connection=None, |
| 150 | extra=None, |
| 151 | profile=None, |
| 152 | endpoint_type=None, ironic_api_version=None): |
| 153 | connection_args = _auth(profile, endpoint_type, |
| 154 | ironic_api_version=ironic_api_version) |
| 155 | identifier = node_uuid or node_name |
| 156 | existing_ports = _ironic_module_call('list_ports', detail=True, |
| 157 | address=address, |
| 158 | **connection_args)['ports'] |
| 159 | # we filtered ports by address, so if port found only one item will |
| 160 | # exist since address is unique. |
| 161 | existing_port = existing_ports[0] if len(existing_ports) else {} |
| 162 | |
| 163 | existing_node = _ironic_module_call('show_node', node_id=identifier, |
| 164 | **connection_args) |
| 165 | |
| 166 | port_arguments = _get_non_null_args( |
| 167 | address=address, |
| 168 | node_uuid=existing_node['uuid'], |
| 169 | local_link_connection=local_link_connection, |
| 170 | extra=extra) |
| 171 | |
| 172 | if not existing_port: |
| 173 | port_arguments.update(connection_args) |
| 174 | res = _ironic_module_call('create_port', **port_arguments) |
| 175 | return _created(address, 'port', res) |
| 176 | else: |
| 177 | # generate differential |
| 178 | # TODO(vsaienko) add update with deep comparison |
| 179 | return _no_change(address, 'port') |
| 180 | |
| 181 | def _created(name, resource, resource_definition): |
| 182 | changes_dict = {'name': name, |
| 183 | 'changes': resource_definition, |
| 184 | 'result': True, |
| 185 | 'comment': '{0} {1} created'.format(resource, name)} |
| 186 | return changes_dict |
| 187 | |
| 188 | def _updated(name, resource, resource_definition): |
| 189 | changes_dict = {'name': name, |
| 190 | 'changes': resource_definition, |
| 191 | 'result': True, |
| 192 | 'comment': '{0} {1} updated'.format(resource, name)} |
| 193 | return changes_dict |
| 194 | |
| 195 | def _no_change(name, resource, test=False): |
| 196 | changes_dict = {'name': name, |
| 197 | 'changes': {}, |
| 198 | 'result': True} |
| 199 | if test: |
| 200 | changes_dict['comment'] = \ |
| 201 | '{0} {1} will be {2}'.format(resource, name, test) |
| 202 | else: |
| 203 | changes_dict['comment'] = \ |
| 204 | '{0} {1} is in correct state'.format(resource, name) |
| 205 | return changes_dict |
| 206 | |
| 207 | |
| 208 | def _deleted(name, resource, resource_definition): |
| 209 | changes_dict = {'name': name, |
| 210 | 'changes': {}, |
| 211 | 'comment': '{0} {1} removed'.format(resource, name), |
| 212 | 'result': True} |
| 213 | return changes_dict |
| 214 | |
| 215 | |
| 216 | def _absent(name, resource): |
| 217 | changes_dict = {'name': name, |
| 218 | 'changes': {}, |
| 219 | 'comment': '{0} {1} not present'.format(resource, name), |
| 220 | 'result': True} |
| 221 | return changes_dict |
| 222 | |
| 223 | |
| 224 | def _delete_failed(name, resource): |
| 225 | changes_dict = {'name': name, |
| 226 | 'changes': {}, |
| 227 | 'comment': '{0} {1} failed to delete'.format(resource, |
| 228 | name), |
| 229 | 'result': False} |
| 230 | return changes_dict |
| 231 | |
| 232 | def _create_failed(name, resource): |
| 233 | changes_dict = {'name': name, |
| 234 | 'changes': {}, |
| 235 | 'comment': '{0} {1} failed to create'.format(resource, |
| 236 | name), |
| 237 | 'result': False} |
| 238 | return changes_dict |
| 239 | |
| 240 | def _update_failed(name, resource): |
| 241 | changes_dict = {'name': name, |
| 242 | 'changes': {}, |
| 243 | 'comment': '{0} {1} failed to update'.format(resource, |
| 244 | name), |
| 245 | 'result': False} |
| 246 | return changes_dict |
| 247 | |
| 248 | |
| 249 | def _get_non_null_args(**kwargs): |
| 250 | ''' |
| 251 | Return those kwargs which are not null |
| 252 | ''' |
| 253 | return dict((key, value,) for key, value in kwargs.iteritems() |
| 254 | if value is not None) |