Add module and states for Ironic API v1

Change-Id: I832381955e608875e87680211e8e7a3836facb40
Related-Prod: PROD-21813
diff --git a/_modules/ironicv1/__init__.py b/_modules/ironicv1/__init__.py
new file mode 100644
index 0000000..ece8bac
--- /dev/null
+++ b/_modules/ironicv1/__init__.py
@@ -0,0 +1,112 @@
+try:
+    import os_client_config
+    from keystoneauth1 import exceptions as ka_exceptions
+    REQUIREMENTS_MET = True
+except ImportError:
+    REQUIREMENTS_MET = False
+
+from ironicv1 import nodes
+from ironicv1 import ports
+from ironicv1 import drivers
+from ironicv1 import chassis
+from ironicv1 import volumes
+
+node_boot_device_get = nodes.node_boot_device_get
+node_boot_device_get_supported = nodes.node_boot_device_get_supported
+node_boot_device_set = nodes.node_boot_device_set
+node_console_get = nodes.node_console_get
+node_console_start_stop = nodes.node_console_start_stop
+node_create = nodes.node_create
+node_delete = nodes.node_delete
+node_get_details = nodes.node_get_details
+node_inject_nmi = nodes.node_inject_nmi
+node_list = nodes.node_list
+node_maintenance_flag_clear = nodes.node_maintenance_flag_clear
+node_maintenance_flag_set = nodes.node_maintenance_flag_set
+node_power_state_change = nodes.node_power_state_change
+node_provision_state_change = nodes.node_provision_state_change
+node_raid_config_set = nodes.node_raid_config_set
+node_state_summary = nodes.node_state_summary
+node_traits_delete = nodes.node_traits_delete
+node_traits_delete_single = nodes.node_traits_delete_single
+node_traits_list = nodes.node_traits_list
+node_traits_set = nodes.node_traits_set
+node_traits_set_single = nodes.node_traits_set_single
+node_update = nodes.node_update
+node_validate = nodes.node_validate
+node_vif_attach = nodes.node_vif_attach
+node_vif_detach = nodes.node_vif_detach
+node_vif_list = nodes.node_vif_list
+
+driver_get_details = drivers.driver_get_details
+driver_get_logical_disk_properties = drivers.driver_get_logical_disk_properties
+driver_get_properties = drivers.driver_get_properties
+driver_list = drivers.driver_list
+
+port_create = ports.port_create
+port_delete = ports.port_delete
+port_get_details = ports.port_get_details
+port_list = ports.port_list
+port_list_details = ports.port_list_details
+port_update = ports.port_update
+
+chassis_create = chassis.chassis_create
+chassis_delete = chassis.chassis_delete
+chassis_get_details = chassis.chassis_get_details
+chassis_list = chassis.chassis_list
+chassis_list_details = chassis.chassis_list_details
+chassis_update = chassis.chassis_update
+
+volume_connector_create = volumes.volume_connector_create
+volume_connector_delete = volumes.volume_connector_delete
+volume_connector_get_details = volumes.volume_connector_get_details
+volume_connector_list = volumes.volume_connector_list
+volume_connector_update = volumes.volume_connector_update
+volume_resource_list = volumes.volume_resource_list
+volume_target_create = volumes.volume_target_create
+volume_target_delete = volumes.volume_target_delete
+volume_target_get_details = volumes.volume_target_get_details
+volume_target_list = volumes.volume_target_list
+volume_target_update = volumes.volume_target_update
+
+
+__all__ = (
+    # node.py
+    'node_list', 'node_boot_device_get', 'node_boot_device_get_supported',
+    'node_boot_device_set', 'node_console_get', 'node_console_start_stop',
+    'node_create', 'node_delete', 'node_get_details', 'node_inject_nmi',
+    'node_maintenance_flag_clear', 'node_maintenance_flag_set',
+    'node_power_state_change', 'node_provision_state_change',
+    'node_raid_config_set', 'node_state_summary', 'node_traits_delete',
+    'node_traits_delete_single', 'node_traits_list', 'node_traits_set',
+    'node_traits_set_single', 'node_update', 'node_validate',
+    'node_vif_attach', 'node_vif_detach', 'node_vif_list',
+
+    # driver.py
+    'driver_get_details', 'driver_get_logical_disk_properties',
+    'driver_get_properties', 'driver_list',
+
+    # ports.py
+    'port_create', 'port_delete', 'port_get_details', 'port_list',
+    'port_update', 'port_list_details',
+
+    # chassis.py
+    'chassis_create', 'chassis_delete', 'chassis_get_details', 'chassis_list',
+    'chassis_list_details', 'chassis_update',
+
+    # volumes.py
+    'volume_connector_create', 'volume_connector_delete',
+    'volume_connector_get_details', 'volume_connector_list',
+    'volume_connector_update', 'volume_resource_list', 'volume_target_create',
+    'volume_target_delete', 'volume_target_get_details', 'volume_target_list',
+    'volume_target_update',
+)
+
+
+def __virtual__():
+    """Only load ironicv1 if requirements are available."""
+    if REQUIREMENTS_MET:
+        return 'ironicv1'
+    else:
+        return False, ("The ironicv1 execution module cannot be loaded: "
+                       "os_client_config or keystoneauth are unavailable.")
diff --git a/_modules/ironicv1/chassis.py b/_modules/ironicv1/chassis.py
new file mode 100644
index 0000000..5d747cf
--- /dev/null
+++ b/_modules/ironicv1/chassis.py
@@ -0,0 +1,41 @@
+from ironicv1.common import send
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@send('get')
+def chassis_list_details(**kwargs):
+    url = '/chassis/detail?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('get')
+def chassis_get_details(chassis_id, **kwargs):
+    url = '/chassis/{}?{}'.format(chassis_id, urlencode(kwargs))
+    return url, {}
+
+
+@send('patch')
+def chassis_update(chassis_id, properties, **kwargs):
+    url = '/chassis/{}'.format(chassis_id)
+    return url, {'json': properties}
+
+
+@send('delete')
+def chassis_delete(chassis_id, **kwargs):
+    url = '/chassis/{}'.format(chassis_id)
+    return url, {}
+
+
+@send('post')
+def chassis_create(chassis, **kwargs):
+    url = '/chassis'
+    return url, {'json': chassis}
+
+
+@send('get')
+def chassis_list(**kwargs):
+    url = '/chassis?{}'.format(urlencode(kwargs))
+    return url, {}
diff --git a/_modules/ironicv1/common.py b/_modules/ironicv1/common.py
new file mode 100644
index 0000000..b2a6071
--- /dev/null
+++ b/_modules/ironicv1/common.py
@@ -0,0 +1,77 @@
+import logging
+import os_client_config
+
+log = logging.getLogger(__name__)
+
+IRONIC_VERSION_HEADER = 'X-OpenStack-Ironic-API-Version'
+ADAPTER_VERSION = '1.0'
+
+
+class IronicException(Exception):
+
+    _msg = "Ironic module exception occurred."
+
+    def __init__(self, message=None, **kwargs):
+        super(IronicException, self).__init__(message or self._msg)
+
+
+class NoIronicEndpoint(IronicException):
+    _msg = "Ironic endpoint not found in keystone catalog."
+
+
+class NoAuthPluginConfigured(IronicException):
+    _msg = ("You are using keystoneauth auth plugin that does not support "
+            "fetching endpoint list from token (noauth or admin_token).")
+
+
+class NoCredentials(IronicException):
+    _msg = "Please provide cloud name present in clouds.yaml."
+
+
+def _get_raw_client(cloud_name):
+    service_type = 'baremetal'
+    config = os_client_config.OpenStackConfig()
+    cloud = config.get_one_cloud(cloud_name)
+    adapter = cloud.get_session_client(service_type)
+    adapter.version = ADAPTER_VERSION
+    try:
+        access_info = adapter.session.auth.get_access(adapter.session)
+        access_info.service_catalog.get_endpoints()
+    except (AttributeError, ValueError):
+        e = NoAuthPluginConfigured()
+        log.exception('%s' % e)
+        raise e
+    return adapter
+
+
+def send(method):
+    def wrap(func):
+        def wrapped_f(*args, **kwargs):
+            cloud_name = kwargs.pop('cloud_name')
+            if not cloud_name:
+                e = NoCredentials()
+                log.error('%s' % e)
+                raise e
+            adapter = _get_raw_client(cloud_name)
+            # Remove salt internal kwargs
+            kwarg_keys = list(kwargs.keys())
+            for k in kwarg_keys:
+                if k.startswith('__'):
+                    kwargs.pop(k)
+            microversion = kwargs.pop('microversion', None)
+            url, request_kwargs = func(*args, **kwargs)
+            if microversion:
+                if 'headers' not in request_kwargs:
+                    request_kwargs['headers'] = {}
+                request_kwargs['headers'][IRONIC_VERSION_HEADER] = \
+                    microversion
+            response = getattr(adapter, method)(url, **request_kwargs)
+            if not response.content:
+                return {}
+            try:
+                resp = response.json()
+            except ValueError:
+                resp = response.content
+            return resp
+        return wrapped_f
+    return wrap
diff --git a/_modules/ironicv1/drivers.py b/_modules/ironicv1/drivers.py
new file mode 100644
index 0000000..a4fd0e8
--- /dev/null
+++ b/_modules/ironicv1/drivers.py
@@ -0,0 +1,29 @@
+from ironicv1.common import send
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@send('get')
+def driver_list(**kwargs):
+    url = '/drivers?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('get')
+def driver_get_details(name, **kwargs):
+    url = '/drivers/{}'.format(name)
+    return url, {}
+
+
+@send('get')
+def driver_get_properties(name, **kwargs):
+    url = '/drivers/{}/properties'.format(name)
+    return url, {}
+
+
+@send('get')
+def driver_get_logical_disk_properties(name, **kwargs):
+    url = '/drivers/{}/raid/logical_disk_properties'.format(name)
+    return url, {}
diff --git a/_modules/ironicv1/nodes.py b/_modules/ironicv1/nodes.py
new file mode 100644
index 0000000..f47f629
--- /dev/null
+++ b/_modules/ironicv1/nodes.py
@@ -0,0 +1,196 @@
+from ironicv1.common import send
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+# NOTE(opetrenko): Each driver require different driver_info or do not require
+# it at all. To make things work, please use driver_get_properties with driver
+# you want to use to get list of required arguments for driver_info.
+# For more take a look at Baremetal API Reference
+@send('post')
+def node_create(driver, **kwargs):
+    url = '/nodes'
+    json = {
+        'driver': driver,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('get')
+def node_list(**kwargs):
+    url = '/nodes?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('get')
+def node_get_details(node_ident, **kwargs):
+    url = '/nodes/{}?{}'.format(node_ident, urlencode(kwargs))
+    return url, {}
+
+
+@send('patch')
+def node_update(node_ident, properties, **kwargs):
+    url = '/nodes/{}'.format(node_ident)
+    return url, {'json': properties}
+
+
+@send('delete')
+def node_delete(node_ident, **kwargs):
+    url = '/nodes/{}'.format(node_ident)
+    return url, {}
+
+
+# NOTE: Node management API
+@send('get')
+def node_validate(node_ident, **kwargs):
+    url = '/nodes/{}/validate'.format(node_ident)
+    return url, {}
+
+
+@send('put')
+def node_maintenance_flag_set(node_ident, **kwargs):
+    url = '/nodes/{}/maintenance'.format(node_ident)
+    json = {}
+    if 'reason' in kwargs:
+        json['reason'] = kwargs['reason']
+    return url, {'json': json}
+
+
+@send('delete')
+def node_maintenance_flag_clear(node_ident, **kwargs):
+    url = '/nodes/{}/maintenance'.format(node_ident)
+    return url, {}
+
+
+@send('put')
+def node_boot_device_set(node_ident, boot_device, **kwargs):
+    url = '/nodes/{}/management/boot_device'.format(node_ident)
+    json = {
+        'boot_device': boot_device,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('get')
+def node_boot_device_get(node_ident, **kwargs):
+    url = '/nodes/{}/management/boot_device'.format(node_ident)
+    return url, {}
+
+
+@send('get')
+def node_boot_device_get_supported(node_ident, **kwargs):
+    url = '/nodes/{}/management/boot_device/supported'.format(node_ident)
+    return url, {}
+
+
+@send('put')
+def node_inject_nmi(node_ident, **kwargs):
+    url = '/nodes/{}/management/inject_nmi'.format(node_ident)
+    return url, {'json': {}}
+
+
+@send('get')
+def node_state_summary(node_ident, **kwargs):
+    url = '/nodes/{}/states'.format(node_ident)
+    return url, {}
+
+
+@send('put')
+def node_power_state_change(node_ident, target, **kwargs):
+    url = '/nodes/{}/states/power'.format(node_ident)
+    json = {
+        'target': target,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('put')
+def node_provision_state_change(node_ident, target, **kwargs):
+    url = '/nodes/{}/states/provision'.format(node_ident)
+    json = {
+        'target': target,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('put')
+def node_raid_config_set(node_ident, target_raid_config, **kwargs):
+    url = 'nodes/{}/states/raid'.format(node_ident)
+    return url, {'json': target_raid_config}
+
+
+@send('get')
+def node_console_get(node_ident, **kwargs):
+    url = '/nodes/{}/states/console'.format(node_ident)
+    return url, {}
+
+
+@send('put')
+def node_console_start_stop(node_ident, enabled, **kwargs):
+    url = '/nodes/{}/states/console'.format(node_ident)
+    json = {
+        'enabled': enabled,
+    }
+    return url, {'json': json}
+
+
+# NOTE: Node Traits API
+@send('get')
+def node_traits_list(node_ident, **kwargs):
+    url = '/nodes/{}/traits'.format(node_ident)
+    return url, {}
+
+
+@send('put')
+def node_traits_set(node_ident, traits, **kwargs):
+    url = '/nodes/{}/traits'.format(node_ident)
+    json = {
+        'traits': traits,
+    }
+    return url, {'json': json}
+
+
+@send('put')
+def node_traits_set_single(node_ident, trait, **kwargs):
+    url = '/nodes/{}/traits/{}'.format(node_ident, trait)
+    return url, {'json': {}}
+
+
+@send('delete')
+def node_traits_delete(node_ident, **kwargs):
+    url = '/nodes/{}/traits'.format(node_ident)
+    return url, {}
+
+
+@send('delete')
+def node_traits_delete_single(node_ident, trait, **kwargs):
+    url = '/nodes/{}/traits/{}'.format(node_ident, trait)
+    return url, {}
+
+
+# NOTE: VIFs API
+@send('get')
+def node_vif_list(node_ident, **kwargs):
+    url = '/nodes/{}/vifs'.format(node_ident)
+    return url, {}
+
+
+@send('post')
+def node_vif_attach(node_ident, id, **kwargs):
+    url = '/nodes/{}/vifs'.format(node_ident)
+    json = {
+        'id': id,
+    }
+    return url, {'json': json}
+
+
+@send('delete')
+def node_vif_detach(node_ident, vif_ident, **kwargs):
+    url = '/nodes/{}/vifs/{}'.format(node_ident, vif_ident)
+    return url, {}
diff --git a/_modules/ironicv1/ports.py b/_modules/ironicv1/ports.py
new file mode 100644
index 0000000..5130185
--- /dev/null
+++ b/_modules/ironicv1/ports.py
@@ -0,0 +1,46 @@
+from ironicv1.common import send
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@send('get')
+def port_list(**kwargs):
+    url = '/ports?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('post')
+def port_create(node_uuid, address, **kwargs):
+    url = '/ports'
+    json = {
+        'node_uuid': node_uuid,
+        'address': address,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('get')
+def port_list_details(**kwargs):
+    url = '/ports/detail?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('get')
+def port_get_details(port_id, **kwargs):
+    url = '/ports/{}?{}'.format(port_id, urlencode(kwargs))
+    return url, {}
+
+
+@send('patch')
+def port_update(port_id, properties, **kwargs):
+    url = '/ports/{}'.format(port_id)
+    return url, {'json': properties}
+
+
+@send('delete')
+def port_delete(port_id, **kwargs):
+    url = '/ports/{}'.format(port_id)
+    return url, {}
\ No newline at end of file
diff --git a/_modules/ironicv1/volumes.py b/_modules/ironicv1/volumes.py
new file mode 100644
index 0000000..9732cad
--- /dev/null
+++ b/_modules/ironicv1/volumes.py
@@ -0,0 +1,87 @@
+from ironicv1.common import send
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@send('get')
+def volume_resource_list(**kwargs):
+    url = '/volume'
+    return url, {}
+
+
+@send('get')
+def volume_connector_list(**kwargs):
+    url = '/volume/connectors?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('post')
+def volume_connector_create(node_uuid, volume_type, connector_id, **kwargs):
+    url = '/volume/connectors'
+    json = {
+        'node_uuid': node_uuid,
+        'type': volume_type,
+        'connector_id': connector_id,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('get')
+def volume_connector_get_details(volume_connector_id, **kwargs):
+    url = '/volume/connectors/{}?{}'.format(
+        volume_connector_id, urlencode(kwargs))
+    return url, {}
+
+
+@send('patch')
+def volume_connector_update(volume_connector_id, properties, **kwargs):
+    url = '/volume/connectors/{}'.format(volume_connector_id)
+    return url, {'json': properties}
+
+
+@send('delete')
+def volume_connector_delete(volume_connector_id, **kwargs):
+    url = '/volume/connectors/{}'.format(volume_connector_id)
+    return url, {}
+
+
+@send('get')
+def volume_target_list(**kwargs):
+    url = '/volume/targets?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('post')
+def volume_target_create(node_uuid, volume_type, properties,
+                         boot_index, volume_id, **kwargs):
+    url = '/volume/targets'
+    json = {
+        'node_uuid': node_uuid,
+        'volume_type': volume_type,
+        'properties': properties,
+        'boot_index': boot_index,
+        'volume_id': volume_id,
+    }
+    json.update(kwargs)
+    return url, {'json': json}
+
+
+@send('get')
+def volume_target_get_details(target_id, **kwargs):
+    url = '/volume/targets/{}?{}'.format(target_id, urlencode(kwargs))
+    return url, {}
+
+
+@send('patch')
+def volume_target_update(target_id, properties, **kwargs):
+    url = '/volume/targets/{}'.format(target_id)
+    return url, {'json': properties}
+
+
+@send('delete')
+def volume_target_delete(target_id, **kwargs):
+    url = '/volume/targets/{}'.format(target_id)
+    return url, {}