Add module and states for Ironic API v1

Change-Id: I832381955e608875e87680211e8e7a3836facb40
Related-Prod: PROD-21813
diff --git a/_states/ironicv1.py b/_states/ironicv1.py
new file mode 100644
index 0000000..183dc9b
--- /dev/null
+++ b/_states/ironicv1.py
@@ -0,0 +1,209 @@
+import logging
+
+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)
+
+    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, 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)
+    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 _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}'
+    }
+    changes_dict = {
+        'name': name,
+        'result': False,
+        'comment': msg_map[op].format(resource, name),
+        'changes': {},
+    }
+    return changes_dict