понедельник, 7 декабря 2015 г.

Проектирование API клиентов: попробуй использовать сам, прежде чем отдать другим

    Сегодня в очередной раз столкнулся с "сюрпризами", оставленными мне какими-то разработчиками, на этот раз объектом моих экспериментов стал OpenStack Keystone Python client (link).

   Взяв пару примеров из коротких руководств я попробовал получить список OpenStack Endpoints (ссылки для доступа к OpenStack сервисам) - но не тут-то было, примеры из документации отказываются работать.

    Поэкспериментировав и прочитав пару десятков страниц из гугла я не нашёл ничего "готового", однако решение было на виду.

    Разбор данного примера наглядно показывает как не надо проектировать python клиентов для сервисов, чтобы ваши пользователи не сходили с ума, пытаясь их использовать.

    Давайте разберём на примере.

    Вот пример кода инициализации клиента из документации (ссылка):
from keystoneclient.v2_0 import client


username='adminUser'
password='secretword'
tenant_name='openstackDemo'
auth_url='http://192.168.206.130:5000/v2.0'
keystone = client.Client(username=username, password=password,
                         tenant_name=tenant_name, auth_url=auth_url)
    Казалось бы - копируй и используй, так и делаем:
import os
from keystoneclient.v2_0 import client as keystone_client


OS_AUTH_URL = os.environ.get('OS_AUTH_URL')
OS_USERNAME = os.environ.get('OS_USERNAME')
OS_PASSWORD = os.environ.get('OS_PASSWORD')
OS_TENANT_NAME = os.environ.get('OS_PASSWORD')
OS_PROJECT_NAME = os.environ.get('OS_PROJECT_NAME')

keystone = keystone_client.Client(auth_url=OS_AUTH_URL,
                                  username=OS_USERNAME,
                                  password=OS_PASSWORD,
                                  tenat_name=OS_TENANT_NAME)
 
print keystone.endpoints.list()
и при выполнении получаем ошибку:
Traceback (most recent call last):
  File "test.py", line 19, in <module>
    print keystone.endpoints.list()
  File "/usr/lib/python2.7/dist-packages/keystoneclient/v2_0/endpoints.py", line 32, in list
    return self._list('/endpoints', 'endpoints')
  File "/usr/lib/python2.7/dist-packages/keystoneclient/base.py", line 124, in _list
    resp, body = self.client.get(url, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/adapter.py", line 170, in get
    return self.request(url, 'GET', **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/adapter.py", line 206, in request
    resp = super(LegacyJsonAdapter, self).request(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/adapter.py", line 95, in request
    return self.session.request(url, method, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/utils.py", line 337, in inner
    return func(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/session.py", line 328, in request
    raise exceptions.EndpointNotFound()
keystoneclient.exceptions.EndpointNotFound
    Какой-то Endpoint не найден... так мы же как раз у него и спрашиваем список Endpoints! Что теперь делать?

    Ладно, пробуем по-другому (спросим список сервисов, а не endpoints):
keystone = keystone_client.Client(auth_url=OS_AUTH_URL,
                                  username=OS_USERNAME,
                                  password=OS_PASSWORD,
                                  tenat_name=OS_TENANT_NAME) 
print keystone.services.list()
    И снова получаем тот же трейс... хм, что-то здесь не ладное.

    Может, надо указать endpoint при инициализации клиента? Пробуем (добавлен параметр во второй строке):
keystone = keystone_client.Client(auth_url=OS_AUTH_URL,
                                  endpoint=OS_ENDPOINT,
                                  username=OS_USERNAME,
                                  password=OS_PASSWORD,
                                  tenat_name=OS_TENANT_NAME)
 
print keystone.services.list()
    Получаем другой трейс, при выполнении той же строчки кода:
Traceback (most recent call last):
  File "test.py", line 22, in <module>
    print keystone.services.list()
  File "/usr/lib/python2.7/dist-packages/keystoneclient/v2_0/services.py", line 32, in list
    return self._list("/OS-KSADM/services", "OS-KSADM:services")
  File "/usr/lib/python2.7/dist-packages/keystoneclient/base.py", line 124, in _list
    resp, body = self.client.get(url, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/adapter.py", line 170, in get
    return self.request(url, 'GET', **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/adapter.py", line 206, in request
    resp = super(LegacyJsonAdapter, self).request(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/adapter.py", line 95, in request
    return self.session.request(url, method, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/utils.py", line 337, in inner
    return func(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/keystoneclient/session.py", line 308, in request
    raise exceptions.AuthorizationFailure(msg)
keystoneclient.exceptions.AuthorizationFailure: No valid authentication is available
    Про то, как ваши пользователи пытаются угадать что же нужно написать в коде, чтобы он хоть как-то начал работать, вы, возможно, узнаете только из того факта, что никто не будет пользоваться таким кодом.

    Для данного кода решение оказалось следующим: для того, чтобы при инициализации клиента из примера были доступны методы работы с Services & Endpoints, необходимо так же добавить параметр project_name при создании экземпляра класса Keystone Client:
import os
from keystoneclient.v2_0 import client as keystone_client

OS_AUTH_URL = os.environ.get('OS_AUTH_URL')
OS_USERNAME = os.environ.get('OS_USERNAME')
OS_PASSWORD = os.environ.get('OS_PASSWORD')
OS_TENANT_NAME = os.environ.get('OS_PASSWORD')
OS_PROJECT_NAME = os.environ.get('OS_PROJECT_NAME')

keystone = keystone_client.Client(auth_url=OS_AUTH_URL,
                                  username=OS_USERNAME,
                                  password=OS_PASSWORD,
                                  tenat_name=OS_TENANT_NAME,
                                  project_name=OS_PROJECT_NAME)

print keystone.endpoints.list()
print keystone.services.list()
   Так вот работает. Но что проблема именно в этом - совсем не очевидно, и в документации об этом тоже ничего нет.