Repository: apache/libcloud
Issue: Openstack dev installation error

## Summary

The libcloud driver does not work with the development installation of Openstack.

Instructions to install the dev installation are found [here](https://docs.openstack.org/devstack/latest/)

## Detailed Information

Initiating the driver with:

```python
from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver

Openstack = get_driver(Provider.OPENSTACK)

driver = Openstack("admin", "Password", api_version="2.2", ex_force_auth_url="http://<IP>/identity", ex_domain_name="Default", ex_tenant_name="admin")
```

Then running any command produces the following:

```python
driver.list_nodes()                                                                                                                                                                                                                                                      
---------------------------------------------------------------------------
BaseHTTPError                             Traceback (most recent call last)
<ipython-input-14-dc49087bfcd6> in <module>
----> 1 driver.list_nodes()

~/mist/mist-ee/api/libcloud/libcloud/compute/drivers/openstack.py in list_nodes(self, ex_all_tenants)
   3342             params = {"all_tenants": 1}
   3343         return self._to_nodes(
-> 3344             self._paginated_request(
   3345                 "/servers/detail", "servers", self.connection, params=params
   3346             )

~/mist/mist-ee/api/libcloud/libcloud/compute/drivers/openstack.py in _paginated_request(url, obj, connection, params)
    223         loop_count = 0
    224         while True:
--> 225             data = connection.request(url, params=params)
    226             values = data.object.get(obj, list())
    227             objects.extend(values)

~/mist/mist-ee/api/libcloud/libcloud/common/openstack.py in request(self, action, params, data, headers, method, raw)
    253             headers = {"Content-Type": default_content_type}
    254 
--> 255         return super(OpenStackBaseConnection, self).request(
    256             action=action,
    257             params=params,

~/mist/mist-ee/api/libcloud/libcloud/common/base.py in request(self, action, params, data, headers, method, raw, stream, json, enforce_unicode_response)
    587         )
    588 
--> 589         action = self.morph_action_hook(action)
    590         self.action = action
    591         self.method = method

~/mist/mist-ee/api/libcloud/libcloud/common/openstack.py in morph_action_hook(self, action)
    327 
    328     def morph_action_hook(self, action):
--> 329         self._populate_hosts_and_request_paths()
    330         return super(OpenStackBaseConnection, self).morph_action_hook(action)
    331 

~/mist/mist-ee/api/libcloud/libcloud/common/openstack.py in _populate_hosts_and_request_paths(self)
    361                 kwargs = {}
    362 
--> 363             osa = osa.authenticate(**kwargs)  # may throw InvalidCreds
    364 
    365             self.auth_token = osa.auth_token

~/mist/mist-ee/api/libcloud/libcloud/common/openstack_identity.py in authenticate(self, force)
   1120 
   1121         data = json.dumps(data)
-> 1122         response = self.request(
   1123             "/v3/auth/tokens",
   1124             data=data,

~/mist/mist-ee/api/libcloud/libcloud/common/base.py in request(self, action, params, data, headers, method, raw, stream, json, enforce_unicode_response)
    705 
    706         try:
--> 707             response = responseCls(**kwargs)
    708         finally:
    709             # Always reset the context after the request has completed

~/mist/mist-ee/api/libcloud/libcloud/common/base.py in __init__(self, response, connection)
    155 
    156         if not self.success():
--> 157             raise exception_from_message(
    158                 code=self.status, message=self.parse_error(), headers=self.headers
    159             )

BaseHTTPError: <!doctype html>
<html lang=en>
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>
```

To my understanding the [morph_action_hook](https://github.com/apache/libcloud/blob/trunk/libcloud/common/openstack_identity.py#L771 ) of the `OpenStackIdentityConnection` does not take into account the given action when the Auth url contains a path.

For example in the scenario above the Auth url is `http://<IP>/identity`, when the driver tries to  [authenticate](https://github.com/apache/libcloud/blob/trunk/libcloud/common/openstack_identity.py#L1258) the POST request path becomes `/identity`  instead of the correct one `/identity/v3/auth/tokens` .


Changed files:
libcloud/compute/drivers/kili.py, libcloud/test/common/test_openstack_identity.py, libcloud/compute/drivers/cloudwatt.py, libcloud/test/compute/test_kili.py, libcloud/common/openstack_identity.py

diff --git a/libcloud/common/openstack_identity.py b/libcloud/common/openstack_identity.py
index ecc0efd8c..6a26988d3 100644
--- a/libcloud/common/openstack_identity.py
+++ b/libcloud/common/openstack_identity.py
@@ -776,7 +776,7 @@ class OpenStackIdentityConnection(ConnectionUserAndKey):
             # method.
             return action
 
-        return request_path
+        return super(OpenStackIdentityConnection, self).morph_action_hook(action=action)
 
     def add_default_headers(self, headers):
         headers["Accept"] = "application/json"
diff --git a/libcloud/compute/drivers/cloudwatt.py b/libcloud/compute/drivers/cloudwatt.py
index 6039fbe73..3032b677f 100644
--- a/libcloud/compute/drivers/cloudwatt.py
+++ b/libcloud/compute/drivers/cloudwatt.py
@@ -47,6 +47,16 @@ class CloudwattAuthConnection(OpenStackIdentityConnection):
         self._ex_tenant_id = kwargs.pop("ex_tenant_id")
         super(CloudwattAuthConnection, self).__init__(*args, **kwargs)
 
+    def morph_action_hook(self, action):
+        (_, _, _, request_path) = self._tuple_from_url(self.auth_url)
+
+        if request_path == "":
+            # No path is provided in the auth_url, use action passed to this
+            # method.
+            return action
+
+        return request_path
+
     def authenticate(self, force=False):
         reqbody = json.dumps(
             {
diff --git a/libcloud/compute/drivers/kili.py b/libcloud/compute/drivers/kili.py
index 33ebba238..c846e7f38 100644
--- a/libcloud/compute/drivers/kili.py
+++ b/libcloud/compute/drivers/kili.py
@@ -26,7 +26,7 @@ __all__ = ["KiliCloudNodeDriver"]
 
 ENDPOINT_ARGS = {"service_type": "compute", "name": "nova", "region": "RegionOne"}
 
-AUTH_URL = "https://api.kili.io/keystone/v2.0/tokens"
+AUTH_URL = "https://api.kili.io/keystone"
 
 
 class KiliCloudConnection(OpenStack_1_1_Connection):
diff --git a/libcloud/test/common/test_openstack_identity.py b/libcloud/test/common/test_openstack_identity.py
index 7e077c958..4b65407b2 100644
--- a/libcloud/test/common/test_openstack_identity.py
+++ b/libcloud/test/common/test_openstack_identity.py
@@ -78,32 +78,29 @@ class OpenStackIdentityConnectionTestCase(unittest.TestCase):
             ),
         ]
 
-        APPEND = 0
-        NOTAPPEND = 1
-
         auth_urls = [
-            ("https://auth.api.example.com", APPEND, ""),
-            ("https://auth.api.example.com/", NOTAPPEND, "/"),
-            ("https://auth.api.example.com/foo/bar", NOTAPPEND, "/foo/bar"),
-            ("https://auth.api.example.com/foo/bar/", NOTAPPEND, "/foo/bar/"),
+            ("https://auth.api.example.com", ""),
+            ("https://auth.api.example.com/", "/"),
+            ("https://auth.api.example.com/foo/bar", "/foo/bar"),
+            ("https://auth.api.example.com/foo/bar/", "/foo/bar/"),
         ]
 
         actions = {
-            "1.0": "/v1.0",
-            "1.1": "/v1.1/auth",
-            "2.0": "/v2.0/tokens",
-            "2.0_apikey": "/v2.0/tokens",
-            "2.0_password": "/v2.0/tokens",
-            "3.x_password": "/v3/auth/tokens",
-            "3.x_appcred": "/v3/auth/tokens",
-            "3.x_oidc_access_token": "/v3/OS-FEDERATION/identity_providers/user_name/protocols/tenant-name/auth",
+            "1.0": "{url_path}/v1.0",
+            "1.1": "{url_path}/v1.1/auth",
+            "2.0": "{url_path}/v2.0/tokens",
+            "2.0_apikey": "{url_path}/v2.0/tokens",
+            "2.0_password": "{url_path}/v2.0/tokens",
+            "3.x_password": "{url_path}/v3/auth/tokens",
+            "3.x_appcred": "{url_path}/v3/auth/tokens",
+            "3.x_oidc_access_token": "{url_path}/v3/OS-FEDERATION/identity_providers/user_name/protocols/tenant-name/auth",
         }
 
         user_id = OPENSTACK_PARAMS[0]
         key = OPENSTACK_PARAMS[1]
 
         for (auth_version, mock_http_class, kwargs) in tuples:
-            for (url, should_append_default_path, expected_path) in auth_urls:
+            for (url, url_path) in auth_urls:
                 connection = self._get_mock_connection(
                     mock_http_class=mock_http_class, auth_url=url
                 )
@@ -123,10 +120,14 @@ class OpenStackIdentityConnectionTestCase(unittest.TestCase):
                 except Exception:
                     pass
 
-                if should_append_default_path == APPEND:
-                    expected_path = actions[auth_version]
+                expected_path = (
+                    actions[auth_version].format(url_path=url_path).replace("//", "/")
+                )
 
-                self.assertEqual(osa.action, expected_path)
+                self.assertEqual(
+                    osa.action,
+                    expected_path,
+                )
 
     def test_basic_authentication(self):
         tuples = [
diff --git a/libcloud/test/compute/test_kili.py b/libcloud/test/compute/test_kili.py
index ee9baa026..7824a1db7 100644
--- a/libcloud/test/compute/test_kili.py
+++ b/libcloud/test/compute/test_kili.py
@@ -23,7 +23,7 @@ def _ex_connection_class_kwargs(self):
     kwargs = self.openstack_connection_kwargs()
     kwargs["get_endpoint_args"] = ENDPOINT_ARGS
     # Remove keystone from the URL path so that the openstack base tests work
-    kwargs["ex_force_auth_url"] = "https://api.kili.io/v2.0/tokens"
+    kwargs["ex_force_auth_url"] = "https://api.kili.io/"
     kwargs["ex_tenant_name"] = self.tenant_name
 
     return kwargs