Add octaviav2

V2 stands for using raw REST API requests rather than
python clients which creates problems with not
versioned salt formulas.

Also created executable modules needed and states
to maintain loadbalancers

Also added verify api step for upgrade

Change-Id: If62acd656bdb678e22acfa6f260b01eb73604676
Related-Prod: PROD-22187
diff --git a/_modules/octaviav2/__init__.py b/_modules/octaviav2/__init__.py
new file mode 100644
index 0000000..7793d21
--- /dev/null
+++ b/_modules/octaviav2/__init__.py
@@ -0,0 +1,30 @@
+try:
+    import os_client_config
+    from keystoneauth1 import exceptions as ka_exceptions
+    REQUIREMENTS_MET = True
+except ImportError:
+    REQUIREMENTS_MET = False
+
+from octaviav2 import loadbalancers
+
+
+loadbalancer_list = loadbalancers.loadbalancer_list
+loadbalancer_get_details = loadbalancers.loadbalancer_get_details
+loadbalancer_update = loadbalancers.loadbalancer_update
+loadbalancer_delete = loadbalancers.loadbalancer_delete
+loadbalancer_create = loadbalancers.loadbalancer_create
+
+
+__all__ = (
+    'loadbalancer_get_details', 'loadbalancer_update', 'loadbalancer_delete',
+    'loadbalancer_list', 'loadbalancer_create',
+)
+
+
+def __virtual__():
+    """Only load neutronv2 if requirements are available."""
+    if REQUIREMENTS_MET:
+        return 'octaviav2'
+    else:
+        return False, ("The octaviav2 execution module cannot be loaded: "
+                       "os_client_config or keystoneauth are unavailable.")
diff --git a/_modules/octaviav2/common.py b/_modules/octaviav2/common.py
new file mode 100644
index 0000000..7cccf0b
--- /dev/null
+++ b/_modules/octaviav2/common.py
@@ -0,0 +1,114 @@
+import logging
+import os_client_config
+from uuid import UUID
+
+log = logging.getLogger(__name__)
+
+
+class OctaviaException(Exception):
+
+    _msg = "Octavia module exception occured."
+
+    def __init__(self, message=None, **kwargs):
+        super(OctaviaException, self).__init__(message or self._msg)
+
+
+class NoAuthPluginConfigured(OctaviaException):
+    _msg = ("You are using keystoneauth auth plugin that does not support "
+            "fetching endpoint list from token (noauth or admin_token).")
+
+
+class NoCredentials(OctaviaException):
+    _msg = "Please provide cloud name present in clouds.yaml."
+
+
+class ResourceNotFound(OctaviaException):
+    _msg = "Uniq resource: {resource} with name: {name} not found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(ResourceNotFound, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+class MultipleResourcesFound(OctaviaException):
+    _msg = "Multiple resource: {resource} with name: {name} found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(MultipleResourcesFound, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+def _get_raw_client(cloud_name):
+    service_type = 'load-balancer'
+    config = os_client_config.OpenStackConfig()
+    cloud = config.get_one_cloud(cloud_name)
+    adapter = cloud.get_session_client(service_type)
+    # workaround for IndexError as Octavia doen's have a version discovery
+    # document till Rocky (https://review.openstack.org/#/c/559460/)
+    adapter.min_version = None
+    adapter.max_version = None
+    adapter.version = None
+    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)
+            response = getattr(adapter, method)(url, **request_kwargs)
+            if not response.content:
+                return {}
+            return response.json()
+        return wrapped_f
+    return wrap
+
+
+def _check_uuid(val):
+    try:
+        return str(UUID(val)).replace('-', '') == val.replace('-', '')
+    except (TypeError, ValueError, AttributeError):
+        return False
+
+
+def get_by_name_or_uuid(resource_list, resp_key, resp_id_key='id'):
+    def wrap(func):
+        def wrapped_f(*args, **kwargs):
+            if 'name' in kwargs:
+                ref = kwargs.pop('name', None)
+                start_arg = 0
+            else:
+                start_arg = 1
+                ref = args[0]
+            if _check_uuid(ref):
+                uuid = ref
+            else:
+                # Then we have name not uuid
+                cloud_name = kwargs['cloud_name']
+                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][resp_id_key]
+            return func(uuid, *args[start_arg:], **kwargs)
+        return wrapped_f
+    return wrap
diff --git a/_modules/octaviav2/loadbalancers.py b/_modules/octaviav2/loadbalancers.py
new file mode 100644
index 0000000..451880d
--- /dev/null
+++ b/_modules/octaviav2/loadbalancers.py
@@ -0,0 +1,47 @@
+from octaviav2.common import send
+from octaviav2.common import get_by_name_or_uuid
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+RESOURCE_LIST_KEY = 'loadbalancers'
+
+
+@send('get')
+def loadbalancer_list(**kwargs):
+    url = '/v2.0/lbaas/loadbalancers?{}'.format(urlencode(kwargs))
+    return url, {}
+
+@get_by_name_or_uuid(loadbalancer_list, RESOURCE_LIST_KEY)
+@send('get')
+def loadbalancer_get_details(loadbalancer_id, **kwargs):
+    url = '/v2.0/lbaas/loadbalancers/{}?{}'.format(loadbalancer_id, urlencode(kwargs))
+    return url, {}
+
+
+@get_by_name_or_uuid(loadbalancer_list, RESOURCE_LIST_KEY)
+@send('put')
+def loadbalancer_update(loadbalancer_id, **kwargs):
+    url = '/v2.0/lbaas/loadbalancers/{}'.format(loadbalancer_id)
+    json = {
+        'loadbalancer': kwargs,
+    }
+    return url, {'json': json}
+
+
+@get_by_name_or_uuid(loadbalancer_list, RESOURCE_LIST_KEY)
+@send('delete')
+def loadbalancer_delete(loadbalancer_id, **kwargs):
+    url = '/v2.0/lbaas/loadbalancers/{}'.format(loadbalancer_id)
+    return url, {}
+
+
+@send('post')
+def loadbalancer_create(**kwargs):
+    url = '/v2.0/lbaas/loadbalancers'
+    json = {
+        'loadbalancer': kwargs,
+    }
+    return url, {'json': json}
diff --git a/_states/octaviav2.py b/_states/octaviav2.py
new file mode 100644
index 0000000..4f9ef84
--- /dev/null
+++ b/_states/octaviav2.py
@@ -0,0 +1,117 @@
+import logging
+import random
+
+log = logging.getLogger(__name__)
+
+
+def __virtual__():
+    return 'octaviav2' if 'octaviav2.loadbalancer_list' in __salt__ else False
+
+
+def _octaviav2_call(fname, *args, **kwargs):
+    return __salt__['octaviav2.{}'.format(fname)](*args, **kwargs)
+
+
+def _resource_present(resource, name, changeable_params, cloud_name, **kwargs):
+    try:
+        method_name = '{}_get_details'.format(resource)
+        exact_resource = _octaviav2_call(
+            method_name, name, cloud_name=cloud_name
+        )[resource]
+    except Exception as e:
+        if 'ResourceNotFound' in repr(e):
+            try:
+                method_name = '{}_create'.format(resource)
+                resp = _octaviav2_call(
+                    method_name, name=name, cloud_name=cloud_name, **kwargs
+                )
+            except Exception as e:
+                log.exception('Octavia {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 = _octaviav2_call(
+            method_name, name, cloud_name=cloud_name, **to_update
+        )
+    except Exception as e:
+        log.exception('Octavia {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)
+        _octaviav2_call(
+            method_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)
+        _octaviav2_call(
+            method_name, name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        log.error('Octavia delete {0} failed with {1}'.format(resource, e))
+        return _failed('delete', name, resource)
+    return _succeeded('delete', name, resource)
+
+
+def loadbalancer_present(name, cloud_name, **kwargs):
+    changeable = (
+        'admin_state_up', 'vip_qos_policy_id', 'description',
+    )
+
+    return _resource_present('loadbalancer', name, changeable, cloud_name, **kwargs)
+
+
+def loadbalancer_absent(name, cloud_name):
+    return _resource_absent('loadbalancer', 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
diff --git a/octavia/upgrade/verify/_api.sls b/octavia/upgrade/verify/_api.sls
index e308a64..d8a5f7e 100644
--- a/octavia/upgrade/verify/_api.sls
+++ b/octavia/upgrade/verify/_api.sls
@@ -1,3 +1,20 @@
+{%- from "octavia/map.jinja" import api with context %}
+{%- from "keystone/map.jinja" import client as kclient with context %}
+
+
 octavia_upgrade_verify_api:
   test.show_notification:
-    - text: "Running octavia.upgrade.verify.api"
\ No newline at end of file
+    - text: "Running octavia.upgrade.verify.api"
+
+
+{%- if kclient.enabled and kclient.get('os_client_config', {}).get('enabled', False)  %}
+  {%- if api.enabled %}
+
+octaviav2_loadbalancer_list:
+  module.run:
+    - name: octaviav2.loadbalancer_list
+    - kwargs:
+        cloud_name: admin_identity
+
+  {%- endif %}
+{%- endif %}