Added neutronv2

Added _module and _state named neutronv2.
V2 stands for using raw REST API requests rather than
neutron python client which creates problems with not
versioned salt formulas (see related PROD).

Also created executable modules needed and states
to maintain networks, subnets, subnetpools

Change-Id: I8014b76244259218bd502b9d6722ce728413d8af
Related-Prod: PROD-16226
Related-Prod: PROD-19522
diff --git a/_modules/neutronv2/__init__.py b/_modules/neutronv2/__init__.py
new file mode 100644
index 0000000..6e37241
--- /dev/null
+++ b/_modules/neutronv2/__init__.py
@@ -0,0 +1,53 @@
+try:
+    import os_client_config
+    from keystoneauth1 import exceptions as ka_exceptions
+    REQUIREMENTS_MET = True
+except ImportError:
+    REQUIREMENTS_MET = False
+
+from neutronv2 import networks
+from neutronv2 import subnetpools
+from neutronv2 import auto_alloc
+from neutronv2 import subnets
+
+network_get_details = networks.network_get_details
+network_update = networks.network_update
+network_delete = networks.network_delete
+network_list = networks.network_list
+network_create = networks.network_create
+network_bulk_create = networks.network_bulk_create
+
+subnetpool_get_details = subnetpools.subnetpool_get_details
+subnetpool_update = subnetpools.subnetpool_update
+subnetpool_delete = subnetpools.subnetpool_delete
+subnetpool_list = subnetpools.subnetpool_list
+subnetpool_create = subnetpools.subnetpool_create
+
+auto_alloc_get_details = auto_alloc.auto_alloc_get_details
+auto_alloc_delete = auto_alloc.auto_alloc_delete
+
+subnet_list = subnets.subnet_list
+subnet_create = subnets.subnet_create
+subnet_bulk_create = subnets.subnet_bulk_create
+subnet_get_details = subnets.subnet_get_details
+subnet_update = subnets.subnet_update
+subnet_delete = subnets.subnet_delete
+
+
+__all__ = (
+    'network_get_details', 'network_update', 'network_delete', 'network_list',
+    'network_create', 'network_bulk_create', 'subnetpool_get_details',
+    'subnetpool_update', 'subnetpool_delete', 'subnetpool_list',
+    'subnetpool_create', 'auto_alloc_get_details', 'auto_alloc_delete',
+    'subnet_list', 'subnet_create', 'subnet_bulk_create', 'subnet_get_details',
+    'subnet_update', 'subnet_delete',
+)
+
+
+def __virtual__():
+    """Only load neutronv2 if requirements are available."""
+    if REQUIREMENTS_MET:
+        return 'neutronv2'
+    else:
+        return False, ("The neutronv2 execution module cannot be loaded: "
+                       "os_client_config or keystoneauth are unavailable.")
diff --git a/_modules/neutronv2/auto_alloc.py b/_modules/neutronv2/auto_alloc.py
new file mode 100644
index 0000000..34b2fc8
--- /dev/null
+++ b/_modules/neutronv2/auto_alloc.py
@@ -0,0 +1,20 @@
+from neutronv2.common import send
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@send('get')
+def auto_alloc_get_details(project_id, **kwargs):
+    url = '/auto-allocated-topology/{}?{}'.format(
+        project_id, urlencode(kwargs)
+    )
+    return url, {}
+
+
+@send('delete')
+def auto_alloc_delete(project_id, **kwargs):
+    url = '/auto-allocated-topology/{}'.format(project_id)
+    return url, {}
diff --git a/_modules/neutronv2/common.py b/_modules/neutronv2/common.py
new file mode 100644
index 0000000..adc3ff5
--- /dev/null
+++ b/_modules/neutronv2/common.py
@@ -0,0 +1,125 @@
+import logging
+import os_client_config
+from uuid import UUID
+
+log = logging.getLogger(__name__)
+
+NEUTRON_VERSION_HEADER = 'x-openstack-networking-version'
+ADAPTER_VERSION = '2.0'
+
+
+class NeutronException(Exception):
+
+    _msg = "Neutron module exception occured."
+
+    def __init__(self, message=None, **kwargs):
+        super(NeutronException, self).__init__(message or self._msg)
+
+
+class NoNeutronEndpoint(NeutronException):
+    _msg = "Neutron endpoint not found in keystone catalog."
+
+
+class NoAuthPluginConfigured(NeutronException):
+    _msg = ("You are using keystoneauth auth plugin that does not support "
+            "fetching endpoint list from token (noauth or admin_token).")
+
+
+class NoCredentials(NeutronException):
+    _msg = "Please provide cloud name present in clouds.yaml."
+
+
+class ResourceNotFound(NeutronException):
+    _msg = "Uniq resource: {resource} with name: {name} not found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(NeutronException, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+class MultipleResourcesFound(NeutronException):
+    _msg = "Multiple resource: {resource} with name: {name} found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(NeutronException, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+def _get_raw_client(cloud_name):
+    service_type = 'network'
+    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)
+            url, request_kwargs = func(*args, **kwargs)
+            if 'microversion' in kwargs:
+                request_kwargs['headers'][
+                    NEUTRON_VERSION_HEADER] = kwargs['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
+
+
+def _check_uuid(val):
+    try:
+        return str(UUID(val)) == val
+    except (TypeError, ValueError, AttributeError):
+        return False
+
+
+def get_by_name_or_uuid(resource_list, resp_key,
+                        res_id_key='name'):
+    def wrap(func):
+        def wrapped_f(*args, **kwargs):
+            if res_id_key in kwargs:
+                ref = kwargs.pop(res_id_key)
+                start_arg = 0
+            else:
+                start_arg = 1
+                ref = args[0]
+            cloud_name = kwargs['cloud_name']
+            if _check_uuid(ref):
+                uuid = ref
+            else:
+                # Then we have name not uuid
+                resp = resource_list(
+                    name=ref, cloud_name=cloud_name)[resp_key]
+                if len(resp) == 0:
+                    raise ResourceNotFound(resp_key, ref)
+                elif len(resp) > 1:
+                    raise MultipleResourcesFound(resp_key, ref)
+                uuid = resp[0]['id']
+            return func(uuid, *args[start_arg:], **kwargs)
+        return wrapped_f
+    return wrap
diff --git a/_modules/neutronv2/networks.py b/_modules/neutronv2/networks.py
new file mode 100644
index 0000000..d0e85f8
--- /dev/null
+++ b/_modules/neutronv2/networks.py
@@ -0,0 +1,55 @@
+from neutronv2.common import send, get_by_name_or_uuid
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+RESOURCE_LIST_KEY = 'networks'
+
+
+@send('get')
+def network_list(**kwargs):
+    url = '/networks?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@get_by_name_or_uuid(network_list, RESOURCE_LIST_KEY)
+@send('get')
+def network_get_details(network_id, **kwargs):
+    url = '/networks/{}?{}'.format(network_id, urlencode(kwargs))
+    return url, {}
+
+
+@get_by_name_or_uuid(network_list, RESOURCE_LIST_KEY)
+@send('put')
+def network_update(network_id, **kwargs):
+    url = '/networks/{}'.format(network_id)
+    json = {
+        'network': kwargs,
+    }
+    return url, {'json': json}
+
+
+@get_by_name_or_uuid(network_list, RESOURCE_LIST_KEY)
+@send('delete')
+def network_delete(network_id, **kwargs):
+    url = '/networks/{}'.format(network_id)
+    return url, {}
+
+
+@send('post')
+def network_create(**kwargs):
+    url = '/networks'
+    json = {
+        'network': kwargs,
+    }
+    return url, {'json': json}
+
+
+@send('post')
+def network_bulk_create(networks, **kwargs):
+    url = '/networks'
+    json = {
+        'networks': networks,
+    }
+    return url, {'json': json}
diff --git a/_modules/neutronv2/subnetpools.py b/_modules/neutronv2/subnetpools.py
new file mode 100644
index 0000000..fb1912b
--- /dev/null
+++ b/_modules/neutronv2/subnetpools.py
@@ -0,0 +1,52 @@
+from neutronv2.common import send, get_by_name_or_uuid
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+RESOURCE_LIST_KEY = 'subnetpools'
+
+
+@send('get')
+def subnetpool_list(**kwargs):
+    url = '/subnetpools?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@get_by_name_or_uuid(subnetpool_list, RESOURCE_LIST_KEY)
+@send('get')
+def subnetpool_get_details(subnetpool_id, **kwargs):
+    url = '/subnetpools/{}?{}'.format(
+        subnetpool_id, urlencode(kwargs)
+    )
+    return url, {}
+
+
+@get_by_name_or_uuid(subnetpool_list, RESOURCE_LIST_KEY)
+@send('put')
+def subnetpool_update(subnetpool_id, **kwargs):
+    url = '/subnetpools/{}'.format(subnetpool_id)
+    json = {
+        'subnetpool': kwargs,
+    }
+    return url, {'json': json}
+
+
+@get_by_name_or_uuid(subnetpool_list, RESOURCE_LIST_KEY)
+@send('delete')
+def subnetpool_delete(subnetpool_id, **kwargs):
+    url = '/subnetpools/{}'.format(subnetpool_id)
+    return url, {}
+
+
+@send('post')
+def subnetpool_create(name, prefixes, **kwargs):
+    url = '/subnetpools'
+    json = {
+        'subnetpool': {
+            'name': name,
+            'prefixes': prefixes,
+        }
+    }
+    json['subnetpool'].update(kwargs)
+    return url, {'json': json}
diff --git a/_modules/neutronv2/subnets.py b/_modules/neutronv2/subnets.py
new file mode 100644
index 0000000..3a29969
--- /dev/null
+++ b/_modules/neutronv2/subnets.py
@@ -0,0 +1,63 @@
+from neutronv2.common import send, get_by_name_or_uuid
+from neutronv2 import networks
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+RESOURCE_LIST_KEY = 'subnets'
+
+
+@send('get')
+def subnet_list(**kwargs):
+    url = '/subnets?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@get_by_name_or_uuid(networks.network_list, networks.RESOURCE_LIST_KEY,
+                     res_id_key='network_id')
+@send('post')
+def subnet_create(network_id, ip_version, cidr, **kwargs):
+    url = '/subnets'
+    json = {
+        'subnet': {
+            'network_id': network_id,
+            'ip_version': ip_version,
+            'cidr': cidr,
+        }
+    }
+    json['subnet'].update(kwargs)
+    return url, {'json': json}
+
+
+@send('post')
+def subnet_bulk_create(subnets, **kwargs):
+    url = '/subnets'
+    json = {
+        'subnets': subnets,
+    }
+    return url, {'json': json}
+
+
+@get_by_name_or_uuid(subnet_list, RESOURCE_LIST_KEY)
+@send('get')
+def subnet_get_details(subnet_id, **kwargs):
+    url = '/subnets/{}'.format(subnet_id)
+    return url, {}
+
+
+@get_by_name_or_uuid(subnet_list, RESOURCE_LIST_KEY)
+@send('put')
+def subnet_update(subnet_id, **kwargs):
+    url = '/subnets/{}'.format(subnet_id)
+    json = {
+        'subnet': kwargs,
+    }
+    return url, {'json': json}
+
+
+@get_by_name_or_uuid(subnet_list, RESOURCE_LIST_KEY)
+@send('delete')
+def subnet_delete(subnet_id, **kwargs):
+    url = '/subnets/{}'.format(subnet_id)
+    return url, {}
diff --git a/_states/neutronv2.py b/_states/neutronv2.py
new file mode 100644
index 0000000..06a0363
--- /dev/null
+++ b/_states/neutronv2.py
@@ -0,0 +1,150 @@
+import logging
+
+log = logging.getLogger(__name__)
+
+
+def __virtual__():
+    return 'neutronv2' if 'neutronv2.subnet_list' in __salt__ else False
+
+
+def _neutronv2_call(fname, *args, **kwargs):
+    return __salt__['neutronv2.{}'.format(fname)](*args, **kwargs)
+
+
+def _resource_present(resource, name, changeable_params, cloud_name, **kwargs):
+    try:
+        method_name = '{}_get_details'.format(resource)
+        exact_resource = _neutronv2_call(
+            method_name, name=name, cloud_name=cloud_name
+        )[resource]
+    except Exception as e:
+        if 'ResourceNotFound' in repr(e):
+            try:
+                method_name = '{}_create'.format(resource)
+                resp = _neutronv2_call(
+                    method_name, name=name, cloud_name=cloud_name, **kwargs
+                )
+            except Exception as e:
+                log.exception('Neutron {0} create failed with {1}'.
+                    format(resource, e))
+                return _failed('create', name, resource)
+            return _succeeded('create', name, resource, resp)
+        elif 'MultipleResourcesFound' in repr(e):
+            return _failed('find', name, resource)
+        else:
+            raise
+
+    to_update = {}
+    for key in kwargs:
+        if key in changeable_params and (key not in exact_resource
+                or kwargs[key] != exact_resource[key]):
+            to_update[key] = kwargs[key]
+    try:
+        method_name = '{}_update'.format(resource)
+        resp = _neutronv2_call(
+            method_name, name=name, cloud_name=cloud_name, **to_update
+        )
+    except Exception as e:
+        log.exception('Neutron {0} update failed with {1}'.format(resource, e))
+        return _failed('update', name, resource)
+    return _succeeded('update', name, resource, resp)
+
+
+def _resource_absent(resource, name, cloud_name):
+    try:
+        method_name = '{}_get_details'.format(resource)
+        _neutronv2_call(
+            method_name, name=name, cloud_name=cloud_name
+        )[resource]
+    except Exception as e:
+        if 'ResourceNotFound' in repr(e):
+            return _succeeded('absent', name, resource)
+        if 'MultipleResourcesFound' in repr(e):
+            return _failed('find', name, resource)
+    try:
+        method_name = '{}_delete'.format(resource)
+        _neutronv2_call(
+            method_name, name=name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        log.error('Neutron delete {0} failed with {1}'.format(resource, e))
+        return _failed('delete', name, resource)
+    return _succeeded('delete', name, resource)
+
+
+def network_present(name, cloud_name, **kwargs):
+    changeable = (
+        'admin_state_up', 'dns_domain', 'mtu', 'port_security_enabled',
+        'provider:network_type', 'provider:physical_network',
+        'provider:segmentation_id', 'qos_policy_id', 'router:external',
+        'segments', 'shared', 'description', 'is_default'
+    )
+
+    return _resource_present('network', name, changeable, cloud_name, **kwargs)
+
+
+def network_absent(name, cloud_name):
+    return _resource_absent('network', name, cloud_name)
+
+
+def subnet_present(name, cloud_name, network_id, ip_version, cidr, **kwargs):
+    kwargs.update({'network_id': network_id,
+                   'ip_version': ip_version,
+                   'cidr': cidr})
+    changeable = (
+        'name', 'enable_dhcp', 'dns_nameservers', 'allocation_pools',
+        'host_routes', 'gateway_ip', 'description', 'service_types',
+    )
+
+    return _resource_present('subnet', name, changeable, cloud_name, **kwargs)
+
+
+def subnet_absent(name, cloud_name):
+    return _resource_absent('subnet', name, cloud_name)
+
+
+def subnetpool_present(name, cloud_name, prefixes, **kwargs):
+    kwargs.update({'prefixes': prefixes})
+    changeable = (
+        'default_quota', 'min_prefixlen', 'address_scope_id',
+        'default_prefixlen', 'description'
+    )
+
+    return _resource_present('subnetpool', name, changeable, cloud_name, **kwargs)
+
+
+def subnetpool_absent(name, cloud_name):
+    return _resource_absent('subnetpool', name, cloud_name)
+
+
+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