Add API endpoint for retrieving capabilities

This endpoint will serve as a proxy for the openstack/defcore
repository. Instead of the UI sending requests to GitHub directly
where requests might be impeded due to cross-origin blockage or API
rate-limiting, the UI will be able to send requests to the Refstack
API in order to get capability data. The requests-cache library is
used so as not to keep sending requests to GitHub when likely
nothing has changed. With this, Refstack no longer has to worry about
mirroring capability files and keeping them updated.

GET /v1/capabilities - list all capability files
GET /v1/capabilities/2015.03 - get contents of file 2015.03.json

Change-Id: I6d327273f191e3339219e35eed8768da35c5bffb
This commit is contained in:
Paul Van Eck 2015-04-29 13:30:15 -07:00
parent ff156545f5
commit 961e46df6c
8 changed files with 211 additions and 1 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ ChangeLog
build/ build/
cover/ cover/
dist dist
*.sqlite

View File

@ -135,6 +135,15 @@
# The format for start_date and end_date parameters (string value) # The format for start_date and end_date parameters (string value)
#input_date_format = %Y-%m-%d %H:%M:%S #input_date_format = %Y-%m-%d %H:%M:%S
# The GitHub API URL of the repository and location of the DefCore
# capability files. This URL is used to get a listing of all capability
# files.
#github_api_capabilities_url = https://api.github.com/repos/openstack/defcore/contents
# The base URL that is used for retrieving specific capability files.
# Capability file names will be appended to this URL to get the contents
# of that file.
#github_raw_base_url = https://raw.githubusercontent.com/openstack/defcore/master/
[database] [database]

View File

@ -68,6 +68,20 @@ API_OPTS = [
default='http://refstack.net/output.html?test_id=%s', default='http://refstack.net/output.html?test_id=%s',
help='Template for test result url.' help='Template for test result url.'
), ),
cfg.StrOpt('github_api_capabilities_url',
default='https://api.github.com'
'/repos/openstack/defcore/contents',
help='The GitHub API URL of the repository and location of '
'the DefCore capability files. This URL is used to get '
'a listing of all capability files.'
),
cfg.StrOpt('github_raw_base_url',
default='https://raw.githubusercontent.com'
'/openstack/defcore/master/',
help='This is the base URL that is used for retrieving '
'specific capability files. Capability file names will '
'be appended to this URL to get the contents of that file.'
)
] ]
CONF = cfg.CONF CONF = cfg.CONF

View File

@ -16,10 +16,14 @@
"""Version 1 of the API.""" """Version 1 of the API."""
import json import json
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log from oslo_log import log
import pecan import pecan
from pecan import rest from pecan import rest
import re
import requests
import requests_cache
from refstack import db from refstack import db
from refstack.api import constants as const from refstack.api import constants as const
@ -43,6 +47,8 @@ CTRLS_OPTS = [
CONF = cfg.CONF CONF = cfg.CONF
CONF.register_opts(CTRLS_OPTS, group='api') CONF.register_opts(CTRLS_OPTS, group='api')
# Cached requests will expire after 10 minutes.
requests_cache.install_cache(cache_name='github_cache', expire_after=600)
class BaseRestControllerWithValidation(rest.RestController): class BaseRestControllerWithValidation(rest.RestController):
@ -182,8 +188,62 @@ class ResultsController(BaseRestControllerWithValidation):
return page return page
class CapabilitiesController(rest.RestController):
"""/v1/capabilities handler. This acts as a proxy for retrieving
capability files from the openstack/defcore Github repository."""
@pecan.expose('json')
def get(self):
"""Get a list of all available capabilities."""
try:
response = requests.get(CONF.api.github_api_capabilities_url)
LOG.debug("Response Status: %s / Used Requests Cache: %s" %
(response.status_code,
getattr(response, 'from_cache', False)))
if response.status_code == 200:
json = response.json()
regex = re.compile('^[0-9]{4}\.[0-9]{2}\.json$')
capability_files = []
for rfile in json:
if rfile["type"] == "file" and regex.search(rfile["name"]):
capability_files.append(rfile["name"])
return capability_files
else:
LOG.warning('Github returned non-success HTTP '
'code: %s' % response.status_code)
pecan.abort(response.status_code)
except requests.exceptions.RequestException as e:
LOG.warning('An error occurred trying to get GitHub '
'repository contents: %s' % e)
pecan.abort(500)
@pecan.expose('json')
def get_one(self, file_name):
"""Handler for getting contents of specific capability file."""
github_url = ''.join((CONF.api.github_raw_base_url.rstrip('/'),
'/', file_name, ".json"))
try:
response = requests.get(github_url)
LOG.debug("Response Status: %s / Used Requests Cache: %s" %
(response.status_code,
getattr(response, 'from_cache', False)))
if response.status_code == 200:
return response.json()
else:
LOG.warning('Github returned non-success HTTP '
'code: %s' % response.status_code)
pecan.abort(response.status_code)
except requests.exceptions.RequestException as e:
LOG.warning('An error occurred trying to get GitHub '
'capability file contents: %s' % e)
pecan.abort(500)
class V1Controller(object): class V1Controller(object):
"""Version 1 API controller root.""" """Version 1 API controller root."""
results = ResultsController() results = ResultsController()
capabilities = CapabilitiesController()

View File

@ -18,6 +18,7 @@
import json import json
import uuid import uuid
import httmock
from oslo_config import fixture as config_fixture from oslo_config import fixture as config_fixture
import six import six
import webtest.app import webtest.app
@ -220,3 +221,37 @@ class TestResultsController(api.FunctionalTest):
self.assertEqual(len(filtering_results), 3) self.assertEqual(len(filtering_results), 3)
for r in slice_results: for r in slice_results:
self.assertEqual(r, filtering_results) self.assertEqual(r, filtering_results)
class TestCapabilitiesController(api.FunctionalTest):
"""Test case for CapabilitiesController."""
URL = '/v1/capabilities/'
def test_get_capability_list(self):
@httmock.all_requests
def github_api_mock(url, request):
headers = {'content-type': 'application/json'}
content = [{'name': '2015.03.json', 'type': 'file'},
{'name': '2015.next.json', 'type': 'file'},
{'name': '2015.03', 'type': 'dir'}]
content = json.dumps(content)
return httmock.response(200, content, headers, None, 5, request)
with httmock.HTTMock(github_api_mock):
actual_response = self.get_json(self.URL)
expected_response = ['2015.03.json']
self.assertEqual(expected_response, actual_response)
def test_get_capability_file(self):
@httmock.all_requests
def github_mock(url, request):
content = {'foo': 'bar'}
return httmock.response(200, content, None, None, 5, request)
url = self.URL + "2015.03"
with httmock.HTTMock(github_mock):
actual_response = self.get_json(url)
expected_response = {'foo': 'bar'}
self.assertEqual(expected_response, actual_response)

View File

@ -15,9 +15,14 @@
"""Tests for API's controllers""" """Tests for API's controllers"""
import json
import sys
import httmock
import mock import mock
from oslo_config import fixture as config_fixture from oslo_config import fixture as config_fixture
from oslotest import base from oslotest import base
import requests
from refstack.api import constants as const from refstack.api import constants as const
from refstack.api import utils as api_utils from refstack.api import utils as api_utils
@ -25,6 +30,15 @@ from refstack.api.controllers import root
from refstack.api.controllers import v1 from refstack.api.controllers import v1
def safe_json_dump(content):
if isinstance(content, (dict, list)):
if sys.version_info[0] == 3:
content = bytes(json.dumps(content), 'utf-8')
else:
content = json.dumps(content)
return content
class RootControllerTestCase(base.BaseTestCase): class RootControllerTestCase(base.BaseTestCase):
def test_index(self): def test_index(self):
@ -230,6 +244,81 @@ class ResultsControllerTestCase(base.BaseTestCase):
db_get_test.assert_called_once_with(page_number, per_page, filters) db_get_test.assert_called_once_with(page_number, per_page, filters)
class CapabilitiesControllerTestCase(base.BaseTestCase):
def setUp(self):
super(CapabilitiesControllerTestCase, self).setUp()
self.controller = v1.CapabilitiesController()
def test_get_capabilities(self):
"""Test when getting a list of all capability files."""
@httmock.all_requests
def github_api_mock(url, request):
headers = {'content-type': 'application/json'}
content = [{'name': '2015.03.json', 'type': 'file'},
{'name': '2015.next.json', 'type': 'file'},
{'name': '2015.03', 'type': 'dir'}]
content = safe_json_dump(content)
return httmock.response(200, content, headers, None, 5, request)
with httmock.HTTMock(github_api_mock):
result = self.controller.get()
self.assertEqual(['2015.03.json'], result)
@mock.patch('pecan.abort')
def test_get_capabilities_error_code(self, mock_abort):
"""Test when the HTTP status code isn't a 200 OK. The status should
be propogated."""
@httmock.all_requests
def github_api_mock(url, request):
content = {'title': 'Not Found'}
return httmock.response(404, content, None, None, 5, request)
with httmock.HTTMock(github_api_mock):
self.controller.get()
mock_abort.assert_called_with(404)
@mock.patch('requests.get')
@mock.patch('pecan.abort')
def test_get_capabilities_exception(self, mock_abort, mock_request):
"""Test when the GET request raises an exception."""
mock_request.side_effect = requests.exceptions.RequestException()
self.controller.get()
mock_abort.assert_called_with(500)
def test_get_capability_file(self):
"""Test when getting a specific capability file"""
@httmock.all_requests
def github_mock(url, request):
content = {'foo': 'bar'}
return httmock.response(200, content, None, None, 5, request)
with httmock.HTTMock(github_mock):
result = self.controller.get_one('2015.03')
self.assertEqual({'foo': 'bar'}, result)
@mock.patch('pecan.abort')
def test_get_capability_file_error_code(self, mock_abort):
"""Test when the HTTP status code isn't a 200 OK. The status should
be propogated."""
@httmock.all_requests
def github_api_mock(url, request):
content = {'title': 'Not Found'}
return httmock.response(404, content, None, None, 5, request)
with httmock.HTTMock(github_api_mock):
self.controller.get_one('2010.03')
mock_abort.assert_called_with(404)
@mock.patch('requests.get')
@mock.patch('pecan.abort')
def test_get_capability_file_exception(self, mock_abort, mock_request):
"""Test when the GET request raises an exception."""
mock_request.side_effect = requests.exceptions.RequestException()
self.controller.get_one('2010.03')
mock_abort.assert_called_with(500)
class BaseRestControllerWithValidationTestCase(base.BaseTestCase): class BaseRestControllerWithValidationTestCase(base.BaseTestCase):
def setUp(self): def setUp(self):

View File

@ -8,6 +8,7 @@ oslo.log
pecan>=0.8.2 pecan>=0.8.2
pyOpenSSL==0.13 pyOpenSSL==0.13
pycrypto>=2.6 pycrypto>=2.6
requests==1.2.3 requests>=2.2.0,!=2.4.0
requests-cache>=0.4.9
jsonschema>=2.0.0,<3.0.0 jsonschema>=2.0.0,<3.0.0
PyMySQL>=0.6.2,!=0.6.4 PyMySQL>=0.6.2,!=0.6.4

View File

@ -2,6 +2,7 @@ coverage>=3.6
pep8==1.5.7 pep8==1.5.7
pyflakes==0.8.1 pyflakes==0.8.1
flake8==2.2.4 flake8==2.2.4
httmock
mock mock
oslotest>=1.2.0 # Apache-2.0 oslotest>=1.2.0 # Apache-2.0
python-subunit>=0.0.18 python-subunit>=0.0.18