Refactor capabilities

Rename the 'capabilities' endpoint to 'guidelines' to align more
with DefCore terminology. Several references to 'capabilities' were
thus updated to 'guidelines'.
Split out the actual requests to the DefCore repository so
that it can be used by other components.
Also, the cache expiration time was increased to 12 hours from 10 minutes.

Change-Id: I031798ffc32cdd66428af83a1ee610f00385c524
This commit is contained in:
Paul Van Eck 2016-03-10 17:01:12 -08:00
parent 59af5daa2d
commit 48d36fa26f
20 changed files with 371 additions and 256 deletions

View File

@ -43,10 +43,10 @@
url: '/about',
templateUrl: '/components/about/about.html'
}).
state('capabilities', {
url: '/capabilities',
templateUrl: '/components/capabilities/capabilities.html',
controller: 'CapabilitiesController as ctrl'
state('guidelines', {
url: '/guidelines',
templateUrl: '/components/guidelines/guidelines.html',
controller: 'GuidelinesController as ctrl'
}).
state('communityResults', {
url: '/community_results',

View File

@ -1,6 +1,6 @@
<h3>DefCore Guidelines</h3>
<!-- Capability Filters -->
<!-- Guideline Filters -->
<div class="row">
<div class="col-md-3">
<strong>Version:</strong>
@ -21,15 +21,15 @@
</div>
<br />
<div ng-if="ctrl.capabilities">
<div ng-if="ctrl.guidelines">
<strong>Guideline Status:</strong>
{{ctrl.capabilities.status | capitalize}}
{{ctrl.guidelines.status | capitalize}}
</div>
<div ng-show="ctrl.capabilities">
<div ng-show="ctrl.guidelines">
<strong>Corresponding OpenStack Releases:</strong>
<ul class="list-inline">
<li ng-repeat="release in ctrl.capabilities.releases">
<li ng-repeat="release in ctrl.guidelines.releases">
{{release | capitalize}}
</li>
</ul>

View File

@ -17,16 +17,16 @@
angular
.module('refstackApp')
.controller('CapabilitiesController', CapabilitiesController);
.controller('GuidelinesController', GuidelinesController);
CapabilitiesController.$inject = ['$http', '$uibModal', 'refstackApiUrl'];
GuidelinesController.$inject = ['$http', '$uibModal', 'refstackApiUrl'];
/**
* RefStack Capabilities Controller
* This controller is for the '/capabilities' page where a user can browse
* RefStack Guidelines Controller
* This controller is for the '/guidelines' page where a user can browse
* through tests belonging to DefCore-defined capabilities.
*/
function CapabilitiesController($http, $uibModal, refstackApiUrl) {
function GuidelinesController($http, $uibModal, refstackApiUrl) {
var ctrl = this;
ctrl.getVersionList = getVersionList;
@ -51,11 +51,11 @@
/**
* The template to load for displaying capability details.
*/
ctrl.detailsTemplate = 'components/capabilities/partials/' +
'capabilityDetails.html';
ctrl.detailsTemplate = 'components/guidelines/partials/' +
'guidelineDetails.html';
/**
* Retrieve an array of available capability files from the Refstack
* Retrieve an array of available guideline files from the Refstack
* API server, sort this array reverse-alphabetically, and store it in
* a scoped variable. The scope's selected version is initialized to
* the latest (i.e. first) version here as well. After a successful API
@ -63,7 +63,7 @@
* Sample API return array: ["2015.03.json", "2015.04.json"]
*/
function getVersionList() {
var content_url = refstackApiUrl + '/capabilities';
var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest =
$http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse();
@ -78,19 +78,19 @@
/**
* This will contact the Refstack API server to retrieve the JSON
* content of the capability file corresponding to the selected
* content of the guideline file corresponding to the selected
* version.
*/
function update() {
var content_url = refstackApiUrl + '/capabilities/' + ctrl.version;
var content_url = refstackApiUrl + '/guidelines/' + ctrl.version;
ctrl.capsRequest =
$http.get(content_url).success(function (data) {
ctrl.capabilities = data;
ctrl.guidelines = data;
ctrl.updateTargetCapabilities();
}).error(function (error) {
ctrl.showError = true;
ctrl.capabilities = null;
ctrl.error = 'Error retrieving capabilities: ' +
ctrl.guidelines = null;
ctrl.error = 'Error retrieving guideline content: ' +
angular.toJson(error);
});
}
@ -103,14 +103,14 @@
*/
function updateTargetCapabilities() {
ctrl.targetCapabilities = {};
var components = ctrl.capabilities.components;
var components = ctrl.guidelines.components;
var targetCaps = ctrl.targetCapabilities;
// The 'platform' target is comprised of multiple components, so
// we need to get the capabilities belonging to each of its
// components.
if (ctrl.target === 'platform') {
var platform_components = ctrl.capabilities.platform.required;
var platform_components = ctrl.guidelines.platform.required;
// This will contain status priority values, where lower
// values mean higher priorities.
@ -189,12 +189,12 @@
* the selected status(es).
*/
function getTestList() {
var caps = ctrl.capabilities.capabilities;
var caps = ctrl.guidelines.capabilities;
var tests = [];
angular.forEach(ctrl.targetCapabilities,
function (status, cap) {
if (ctrl.status[status]) {
if (ctrl.capabilities.schema === '1.2') {
if (ctrl.guidelines.schema === '1.2') {
tests.push.apply(tests, caps[cap].tests);
}
else {
@ -214,7 +214,7 @@
*/
function openTestListModal() {
$uibModal.open({
templateUrl: '/components/capabilities/partials' +
templateUrl: '/components/guidelines/partials' +
'/testListModal.html',
backdrop: true,
windowClass: 'modal',
@ -249,7 +249,7 @@
/**
* Test List Modal Controller
* This controller is for the modal that appears if a user wants to see the
* ftest list corresponding to DefCore capabilities with the selected
* test list corresponding to DefCore capabilities with the selected
* statuses.
*/
function TestListModalController($uibModalInstance, $window, tests,

View File

@ -1,11 +1,11 @@
<!--
HTML for capabilites page for all Defcore capabilities schemas
This expects the JSON data of the capability file to be stored in scope
variable 'capabilities'.
HTML for guidelines page for all Defcore guideline schemas
This expects the JSON data of the guidelines file to be stored in scope
variable 'guidelines'.
-->
<ol ng-show="ctrl.capabilities" class="capabilities">
<li class="capability-list-item" ng-repeat="capability in ctrl.capabilities.capabilities | arrayConverter | filter:ctrl.filterStatus | orderBy:'id'">
<ol ng-show="ctrl.guidelines" class="capabilities">
<li class="capability-list-item" ng-repeat="capability in ctrl.guidelines.capabilities | arrayConverter | filter:ctrl.filterStatus | orderBy:'id'">
<span class="capability-name">{{capability.id}}</span><br />
<em>{{capability.description}}</em><br />
Status: <span class="{{ctrl.targetCapabilities[capability.id]}}">{{ctrl.targetCapabilities[capability.id]}}</span><br />
@ -19,11 +19,11 @@ variable 'capabilities'.
<a ng-click="showTests = !showTests">Tests ({{ctrl.getObjectLength(capability.tests)}})</a>
<ul uib-collapse="!showTests">
<li ng-if="ctrl.capabilities.schema === '1.2'" ng-repeat="test in capability.tests">
<li ng-if="ctrl.guidelines.schema === '1.2'" ng-repeat="test in capability.tests">
<span ng-class="{'glyphicon glyphicon-flag text-warning': capability.flagged.indexOf(test) > -1}"></span>
{{test}}
</li>
<li ng-if="ctrl.capabilities.schema > '1.2'" ng-repeat="(testName, testDetails) in capability.tests">
<li ng-if="ctrl.guidelines.schema > '1.2'" ng-repeat="(testName, testDetails) in capability.tests">
<span ng-class="{'glyphicon glyphicon-flag text-warning': testDetails.flagged}" title="{{testDetails.flagged.reason}}"></span>
{{testName}}
<div class="test-detail" ng-if="testDetails.aliases">
@ -35,12 +35,12 @@ variable 'capabilities'.
</li>
</ol>
<div ng-show="ctrl.capabilities" class="criteria">
<div ng-show="ctrl.guidelines" class="criteria">
<hr>
<h4><a ng-click="showCriteria = !showCriteria">Criteria</a></h4>
<div uib-collapse="showCriteria">
<ul>
<li ng-repeat="(key, criterion) in ctrl.capabilities.criteria">
<li ng-repeat="(key, criterion) in ctrl.guidelines.criteria">
<span class="criterion-name">{{criterion.name}}</span><br />
<em>{{criterion.Description}}</em><br />
Weight: {{criterion.weight}}

View File

@ -21,7 +21,7 @@ report page.
ng-if="ctrl.isCapabilityShown(capability)">
<a ng-click="showTests = !showTests"
title="{{ctrl.capabilityData.capabilities[capability.id].description}}">
title="{{ctrl.guidelineData.capabilities[capability.id].description}}">
{{capability.id}}
</a>
<span ng-class="{'text-success': ctrl.testStatus === 'passed',
@ -50,8 +50,8 @@ report page.
aria-hidden="true">
</span>
<span ng-class="{'glyphicon glyphicon-flag text-warning':
ctrl.isTestFlagged(test, ctrl.capabilityData.capabilities[capability.id])}"
title="{{ctrl.getFlaggedReason(test, ctrl.capabilityData.capabilities[capability.id])}}">
ctrl.isTestFlagged(test, ctrl.guidelineData.capabilities[capability.id])}"
title="{{ctrl.getFlaggedReason(test, ctrl.guidelineData.capabilities[capability.id])}}">
</span>
{{test}}
</li>
@ -63,8 +63,8 @@ report page.
<span class="glyphicon glyphicon-remove text-danger" aria-hidden="true"></span>
<span ng-class="{'glyphicon glyphicon-flag text-warning':
ctrl.isTestFlagged(test, ctrl.capabilityData.capabilities[capability.id])}"
title="{{ctrl.getFlaggedReason(test, ctrl.capabilityData.capabilities[capability.id])}}">
ctrl.isTestFlagged(test, ctrl.guidelineData.capabilities[capability.id])}"
title="{{ctrl.getFlaggedReason(test, ctrl.guidelineData.capabilities[capability.id])}}">
</span>
{{test}}
</li>

View File

@ -36,7 +36,7 @@
<strong>Guideline Version:</strong>
<!-- Slicing the version file name here gets rid of the '.json' file extension -->
<select ng-model="ctrl.version"
ng-change="ctrl.updateCapabilities()"
ng-change="ctrl.updateGuidelines()"
class="form-control"
ng-options="versionFile.slice(0,-5) for versionFile in ctrl.versionList">
</select>
@ -53,20 +53,20 @@
<!-- End User Options -->
<br />
<div ng-if="ctrl.capabilityData">
<div ng-if="ctrl.guidelineData">
<strong>Guideline Status:</strong>
{{ctrl.capabilityData.status | capitalize}}
{{ctrl.guidelineData.status | capitalize}}
</div>
<strong>Corresponding OpenStack Releases:</strong>
<ul class="list-inline">
<li ng-repeat="release in ctrl.capabilityData.releases">
<li ng-repeat="release in ctrl.guidelineData.releases">
{{release | capitalize}}
</li>
</ul>
<hr >
<div ng-show="ctrl.capabilityData">
<div ng-show="ctrl.guidelineData">
<strong>Status:</strong>
<p>This cloud passes <strong>{{ctrl.requiredPassPercent | number:1}}% </strong>
({{ctrl.caps.required.passedCount}}/{{ctrl.caps.required.count}})

View File

@ -40,7 +40,7 @@
ctrl.isShared = isShared;
ctrl.shareTestRun = shareTestRun;
ctrl.deleteTestRun = deleteTestRun;
ctrl.updateCapabilities = updateCapabilities;
ctrl.updateGuidelines = updateGuidelines;
ctrl.getTargetCapabilities = getTargetCapabilities;
ctrl.buildCapabilityV1_2 = buildCapabilityV1_2;
ctrl.buildCapabilityV1_3 = buildCapabilityV1_3;
@ -66,7 +66,7 @@
'object': 'OpenStack Powered Object Storage'
};
/** The schema version of the currently selected capabilities data. */
/** The schema version of the currently selected guideline data. */
ctrl.schemaVersion = null;
/** The selected test status used for test filtering. */
@ -77,7 +77,7 @@
'reportDetails.html';
/**
* Retrieve an array of available capability files from the Refstack
* Retrieve an array of available guideline files from the Refstack
* API server, sort this array reverse-alphabetically, and store it in
* a scoped variable. The scope's selected version is initialized to
* the latest (i.e. first) version here as well. After a successful API
@ -85,14 +85,14 @@
* Sample API return array: ["2015.03.json", "2015.04.json"]
*/
function getVersionList() {
var content_url = refstackApiUrl + '/capabilities';
var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest =
$http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse();
if (!ctrl.version) {
ctrl.version = ctrl.versionList[0];
}
ctrl.updateCapabilities();
ctrl.updateGuidelines();
}).error(function (error) {
ctrl.showError = true;
ctrl.error = 'Error retrieving version list: ' +
@ -189,24 +189,24 @@
/**
* This will contact the Refstack API server to retrieve the JSON
* content of the capability file corresponding to the selected
* content of the guideline file corresponding to the selected
* version. A function to construct an object from the capability
* date will be called upon successful retrieval.
* data will be called upon successful retrieval.
*/
function updateCapabilities() {
ctrl.capabilityData = null;
function updateGuidelines() {
ctrl.guidelineData = null;
ctrl.showError = false;
var content_url = refstackApiUrl + '/capabilities/' +
var content_url = refstackApiUrl + '/guidelines/' +
ctrl.version;
ctrl.capsRequest =
$http.get(content_url).success(function (data) {
ctrl.capabilityData = data;
ctrl.guidelineData = data;
ctrl.schemaVersion = data.schema;
ctrl.buildCapabilitiesObject();
}).error(function (error) {
ctrl.showError = true;
ctrl.capabilityData = null;
ctrl.error = 'Error retrieving capabilities: ' +
ctrl.guidelineData = null;
ctrl.error = 'Error retrieving guideline date: ' +
angular.toJson(error);
});
}
@ -217,7 +217,7 @@
* @returns {Object} Object containing each capability and their status
*/
function getTargetCapabilities() {
var components = ctrl.capabilityData.components;
var components = ctrl.guidelineData.components;
var targetCaps = {};
// The 'platform' target is comprised of multiple components, so
@ -225,7 +225,7 @@
// components.
if (ctrl.target === 'platform') {
var platform_components =
ctrl.capabilityData.platform.required;
ctrl.guidelineData.platform.required;
// This will contain status priority values, where lower
// values mean higher priorities.
@ -285,7 +285,7 @@
'passedFlagged': [],
'notPassedFlagged': []
};
var capDetails = ctrl.capabilityData.capabilities[capId];
var capDetails = ctrl.guidelineData.capabilities[capId];
// Loop through each test belonging to the capability.
angular.forEach(capDetails.tests,
function (testId) {
@ -322,7 +322,7 @@
'notPassedFlagged': []
};
// Loop through each test belonging to the capability.
angular.forEach(ctrl.capabilityData.capabilities[capId].tests,
angular.forEach(ctrl.guidelineData.capabilities[capId].tests,
function (details, testId) {
var passed = false;
@ -390,8 +390,8 @@
break;
default:
ctrl.showError = true;
ctrl.capabilityData = null;
ctrl.error = 'The schema version for the capabilities ' +
ctrl.guidelineData = null;
ctrl.error = 'The schema version for the guideline ' +
'file selected (' + ctrl.schemaVersion +
') is currently not supported.';
return;

View File

@ -196,7 +196,7 @@
if (ctrl.versionList) {
return;
}
var content_url = refstackApiUrl + '/capabilities';
var content_url = refstackApiUrl + '/guidelines';
ctrl.versionsRequest =
$http.get(content_url).success(function (data) {
ctrl.versionList = data.sort().reverse();

View File

@ -38,7 +38,7 @@
<!-- Controllers -->
<script src="shared/header/headerController.js"></script>
<script src="shared/alerts/alertModalFactory.js"></script>
<script src="components/capabilities/capabilitiesController.js"></script>
<script src="components/guidelines/guidelinesController.js"></script>
<script src="components/results/resultsController.js"></script>
<script src="components/results-report/resultsReportController.js"></script>
<script src="components/profile/profileController.js"></script>

View File

@ -17,7 +17,7 @@ RefStack
<ul class="nav navbar-nav">
<li ng-class="{ active: header.isActive('/')}"><a ui-sref="home">Home</a></li>
<li ng-class="{ active: header.isActive('/about')}"><a ui-sref="about">About</a></li>
<li ng-class="{ active: header.isActive('/capabilities')}"><a ui-sref="capabilities">DefCore Guidelines</a></li>
<li ng-class="{ active: header.isActive('/guidelines')}"><a ui-sref="guidelines">DefCore Guidelines</a></li>
<li ng-class="{ active: header.isActive('/community_results')}"><a ui-sref="communityResults">Community Results</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">

View File

@ -58,11 +58,11 @@ describe('Refstack controllers', function () {
});
});
describe('CapabilitiesController', function () {
describe('GuidelinesController', function () {
var ctrl;
beforeEach(inject(function ($controller) {
ctrl = $controller('CapabilitiesController', {});
ctrl = $controller('GuidelinesController', {});
}));
it('should set default states', function () {
@ -91,17 +91,17 @@ describe('Refstack controllers', function () {
};
$httpBackend.expectGET(fakeApiUrl +
'/capabilities').respond(['2015.03.json', '2015.04.json']);
'/guidelines').respond(['2015.03.json', '2015.04.json']);
// Should call request with latest version.
$httpBackend.expectGET(fakeApiUrl +
'/capabilities/2015.04.json').respond(fakeCaps);
'/guidelines/2015.04.json').respond(fakeCaps);
$httpBackend.flush();
// The version list should be sorted latest first.
expect(ctrl.versionList).toEqual(['2015.04.json',
'2015.03.json']);
expect(ctrl.capabilities).toEqual(fakeCaps);
expect(ctrl.guidelines).toEqual(fakeCaps);
// The guideline status should be approved.
expect(ctrl.capabilities.status).toEqual('approved');
expect(ctrl.guidelines.status).toEqual('approved');
var expectedTargetCaps = {
'cap_id_1': 'required',
'cap_id_2': 'advisory',
@ -158,8 +158,8 @@ describe('Refstack controllers', function () {
function () {
ctrl.targetCapabilities = {'cap-1': 'required',
'cap-2': 'advisory'};
ctrl.capabilities = {
'schema': 1.4,
ctrl.guidelines = {
'schema': '1.4',
'capabilities' : {
'cap-1': {
'tests': {
@ -175,8 +175,8 @@ describe('Refstack controllers', function () {
}
};
expect(ctrl.getTestList()).toEqual(['test_1', 'test_2']);
ctrl.capabilities = {
'schema': 1.2,
ctrl.guidelines = {
'schema': '1.2',
'capabilities' : {
'cap-1': {
'tests': ['test_1', 'test_2']
@ -186,6 +186,7 @@ describe('Refstack controllers', function () {
}
}
};
expect(ctrl.getTestList()).toEqual(['test_1', 'test_2']);
});
it('should have a method to open a modal for the relevant test list',
@ -351,7 +352,7 @@ describe('Refstack controllers', function () {
$httpBackend.expectGET(fakeApiUrl + '/results?page=1')
.respond(fakeResponse);
$httpBackend.expectGET(fakeApiUrl +
'/capabilities').respond(['2015.03.json', '2015.04.json']);
'/guidelines').respond(['2015.03.json', '2015.04.json']);
ctrl.getVersionList();
$httpBackend.flush();
// Expect the list to have the latest guideline first.
@ -394,30 +395,30 @@ describe('Refstack controllers', function () {
}));
it('should make all necessary API requests to get results ' +
'and capabilities',
'and guidelines',
function () {
$httpBackend.expectGET(fakeApiUrl +
'/results/1234').respond(fakeResultResponse);
$httpBackend.expectGET(fakeApiUrl +
'/capabilities').respond(['2015.03.json', '2015.04.json']);
'/guidelines').respond(['2015.03.json', '2015.04.json']);
// Should call request with latest version.
$httpBackend.expectGET(fakeApiUrl +
'/capabilities/2015.04.json').respond(fakeCapabilityResponse);
'/guidelines/2015.04.json').respond(fakeCapabilityResponse);
$httpBackend.flush();
expect(ctrl.resultsData).toEqual(fakeResultResponse);
// The version list should be sorted latest first.
expect(ctrl.versionList).toEqual(['2015.04.json',
'2015.03.json']);
expect(ctrl.capabilityData).toEqual(fakeCapabilityResponse);
expect(ctrl.guidelineData).toEqual(fakeCapabilityResponse);
// The guideline status should be approved.
expect(ctrl.capabilityData.status).toEqual('approved');
expect(ctrl.guidelineData.status).toEqual('approved');
expect(ctrl.schemaVersion).toEqual('1.2');
});
it('should have a method that creates an object containing each ' +
'relevant capability and its highest priority status',
function () {
ctrl.capabilityData = {
ctrl.guidelineData = {
'schema': '1.3',
'platform': {'required': ['compute', 'object']},
'components': {
@ -447,7 +448,7 @@ describe('Refstack controllers', function () {
'schema version 1.2',
function () {
ctrl.resultsData = fakeResultResponse;
ctrl.capabilityData = fakeCapabilityResponse;
ctrl.guidelineData = fakeCapabilityResponse;
ctrl.schemaVersion = '1.2';
ctrl.buildCapabilitiesObject();
var expectedCapsObject = {
@ -481,7 +482,7 @@ describe('Refstack controllers', function () {
'old_test_id_3',
'test_id_4']
};
ctrl.capabilityData = {
ctrl.guidelineData = {
'platform': {'required': ['compute']},
'schema': '1.4',
'components': {

View File

@ -1,86 +0,0 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Defcore capabilities controller."""
from oslo_config import cfg
from oslo_log import log
import pecan
from pecan import rest
import re
import requests
import requests_cache
CONF = cfg.CONF
LOG = log.getLogger(__name__)
# Cached requests will expire after 10 minutes.
requests_cache.install_cache(cache_name='github_cache',
backend='memory',
expire_after=600)
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:
regex = re.compile('^[0-9]{4}\.[0-9]{2}\.json$')
capability_files = []
for rfile in response.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)

View File

@ -0,0 +1,54 @@
# Copyright (c) 2015 Mirantis, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Defcore guidelines controller."""
from oslo_log import log
import pecan
from pecan import rest
from refstack.api import guidelines
LOG = log.getLogger(__name__)
class GuidelinesController(rest.RestController):
"""/v1/guidelines handler.
This acts as a proxy for retrieving guideline files
from the openstack/defcore Github repository.
"""
@pecan.expose('json')
def get(self):
"""Get a list of all available guidelines."""
g = guidelines.Guidelines()
version_list = g.get_guideline_list()
if version_list is None:
pecan.abort(500, 'The server was unable to get a list of '
'guidelines from the external source.')
else:
return version_list
@pecan.expose('json')
def get_one(self, file_name):
"""Handler for getting contents of specific guideline file."""
g = guidelines.Guidelines()
json = g.get_guideline_contents(file_name)
if json:
return json
else:
pecan.abort(500, 'The server was unable to get the JSON '
'content for the specified guideline file.')

View File

@ -16,7 +16,7 @@
"""Version 1 of the API."""
from refstack.api.controllers import auth
from refstack.api.controllers import capabilities
from refstack.api.controllers import guidelines
from refstack.api.controllers import results
from refstack.api.controllers import user
@ -25,6 +25,6 @@ class V1Controller(object):
"""Version 1 API controller root."""
results = results.ResultsController()
capabilities = capabilities.CapabilitiesController()
guidelines = guidelines.GuidelinesController()
auth = auth.AuthController()
profile = user.ProfileController()

103
refstack/api/guidelines.py Normal file
View File

@ -0,0 +1,103 @@
# Copyright (c) 2016 IBM, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Class for retrieving DefCore guideline information."""
from oslo_config import cfg
from oslo_log import log
import re
import requests
import requests_cache
CONF = cfg.CONF
LOG = log.getLogger(__name__)
# Cached requests will expire after 12 hours.
requests_cache.install_cache(cache_name='github_cache',
backend='memory',
expire_after=43200)
class Guidelines:
"""This class handles guideline/capability listing and retrieval."""
def __init__(self,
repo_url=None,
raw_url=None):
"""Initialize class with needed URLs.
The URL for the the guidelines repository is specified with 'repo_url'.
The URL for where raw files are served is specified with 'raw_url'.
These values will default to the values specified in the RefStack
config file.
"""
if repo_url:
self.repo_url = repo_url
else:
self.repo_url = CONF.api.github_api_capabilities_url
if raw_url:
self.raw_url = raw_url
else:
self.raw_url = CONF.api.github_raw_base_url
def get_guideline_list(self):
"""Return a list of a guideline files.
The repository url specificed in class instantiation is checked
for a list of JSON guideline files. A list of these is returned.
"""
try:
response = requests.get(self.repo_url)
LOG.debug("Response Status: %s / Used Requests Cache: %s" %
(response.status_code,
getattr(response, 'from_cache', False)))
if response.status_code == 200:
regex = re.compile('^[0-9]{4}\.[0-9]{2}\.json$')
capability_files = []
for rfile in response.json():
if rfile["type"] == "file" and regex.search(rfile["name"]):
capability_files.append(rfile["name"])
return capability_files
else:
LOG.warning('Guidelines repo URL (%s) returned non-success '
'HTTP code: %s' % (self.repo_url,
response.status_code))
return None
except requests.exceptions.RequestException as e:
LOG.warning('An error occurred trying to get repository contents '
'through %s: %s' % (self.repo_url, e))
return None
def get_guideline_contents(self, guideline_file):
"""Get JSON data from raw guidelines URL."""
file_url = ''.join((self.raw_url.rstrip('/'),
'/', guideline_file, ".json"))
try:
response = requests.get(file_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('Raw guideline URL (%s) returned non-success HTTP '
'code: %s' % (self.raw_url, response.status_code))
return None
except requests.exceptions.RequestException as e:
LOG.warning('An error occurred trying to get raw capability file '
'contents from %s: %s' % (self.raw_url, e))
return None

View File

@ -226,12 +226,12 @@ class TestResultsController(api.FunctionalTest):
self.assertEqual(r, filtering_results)
class TestCapabilitiesController(api.FunctionalTest):
"""Test case for CapabilitiesController."""
class TestGuidelinesController(api.FunctionalTest):
"""Test case for GuidelinesController."""
URL = '/v1/capabilities/'
URL = '/v1/guidelines/'
def test_get_capability_list(self):
def test_get_guideline_list(self):
@httmock.all_requests
def github_api_mock(url, request):
headers = {'content-type': 'application/json'}
@ -247,7 +247,7 @@ class TestCapabilitiesController(api.FunctionalTest):
expected_response = ['2015.03.json']
self.assertEqual(expected_response, actual_response)
def test_get_capability_file(self):
def test_get_guideline_file(self):
@httmock.all_requests
def github_mock(url, request):
content = {'foo': 'bar'}

View File

@ -16,34 +16,22 @@
"""Tests for API's controllers"""
import json
import sys
import httmock
import mock
from oslo_config import fixture as config_fixture
import requests
from six.moves.urllib import parse
import webob.exc
from refstack.api import constants as const
from refstack.api import exceptions as api_exc
from refstack.api.controllers import auth
from refstack.api.controllers import capabilities
from refstack.api.controllers import guidelines
from refstack.api.controllers import results
from refstack.api.controllers import validation
from refstack.api.controllers import user
from refstack.tests import unit as base
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 BaseControllerTestCase(base.RefstackBaseTestCase):
def setUp(self):
@ -289,76 +277,41 @@ class ResultsControllerTestCase(BaseControllerTestCase):
self.controller.delete, 'test_id')
class CapabilitiesControllerTestCase(BaseControllerTestCase):
class GuidelinesControllerTestCase(BaseControllerTestCase):
def setUp(self):
super(CapabilitiesControllerTestCase, self).setUp()
self.controller = capabilities.CapabilitiesController()
super(GuidelinesControllerTestCase, self).setUp()
self.controller = guidelines.GuidelinesController()
self.mock_abort.side_effect = None
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()
@mock.patch('refstack.api.guidelines.Guidelines.get_guideline_list')
def test_get_guidelines(self, mock_list):
"""Test when getting a list of all guideline files."""
mock_list.return_value = ['2015.03.json']
result = self.controller.get()
self.assertEqual(['2015.03.json'], result)
def test_get_capabilities_error_code(self):
"""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()
self.mock_abort.assert_called_with(404)
@mock.patch('requests.get')
def test_get_capabilities_exception(self, mock_requests_get):
"""Test when the GET request raises an exception."""
mock_requests_get.side_effect = requests.exceptions.RequestException()
@mock.patch('refstack.api.guidelines.Guidelines.get_guideline_list')
def test_get_guidelines_error(self, mock_list):
"""Test when there is a problem getting the guideline list and
nothing is returned."""
mock_list.return_value = None
self.controller.get()
self.mock_abort.assert_called_with(500)
self.mock_abort.assert_called_with(500, mock.ANY)
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')
@mock.patch('refstack.api.guidelines.Guidelines.get_guideline_contents')
def test_get_guideline_file(self, mock_get_contents):
"""Test when getting a specific guideline file"""
mock_get_contents.return_value = {'foo': 'bar'}
result = self.controller.get_one('2015.03')
self.assertEqual({'foo': 'bar'}, result)
def test_get_capability_file_error_code(self):
"""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')
self.mock_abort.assert_called_with(404)
@mock.patch('requests.get')
def test_get_capability_file_exception(self, mock_requests_get):
"""Test when the GET request raises an exception."""
mock_requests_get.side_effect = requests.exceptions.RequestException()
@mock.patch('refstack.api.guidelines.Guidelines.get_guideline_contents')
def test_get_guideline_file_error(self, mock_get_contents):
"""Test when there is a problem getting the guideline file contents."""
mock_get_contents.return_value = None
self.controller.get_one('2010.03')
self.mock_abort.assert_called_with(500)
self.mock_abort.assert_called_with(500, mock.ANY)
class BaseRestControllerWithValidationTestCase(BaseControllerTestCase):

View File

@ -0,0 +1,90 @@
# Copyright (c) 2016 IBM, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import httmock
import mock
from oslotest import base
import requests
from refstack.api import guidelines
class GuidelinesTestCase(base.BaseTestCase):
def setUp(self):
super(GuidelinesTestCase, self).setUp()
self.guidelines = guidelines.Guidelines()
def test_guidelines_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):
result = self.guidelines.get_guideline_list()
self.assertEqual(['2015.03.json'], result)
def test_get_guidelines_list_error_code(self):
"""Test when the HTTP status code isn't a 200 OK."""
@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):
result = self.guidelines.get_guideline_list()
self.assertIsNone(result)
@mock.patch('requests.get')
def test_get_guidelines_exception(self, mock_requests_get):
"""Test when the GET request raises an exception."""
mock_requests_get.side_effect = requests.exceptions.RequestException()
result = self.guidelines.get_guideline_list()
self.assertIsNone(result)
def test_get_capability_file(self):
"""Test when getting a specific guideline 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.guidelines.get_guideline_contents('2010.03.json')
self.assertEqual({'foo': 'bar'}, result)
def test_get_capability_file_error_code(self):
"""Test when the HTTP status code isn't a 200 OK."""
@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):
result = self.guidelines.get_guideline_contents('2010.03.json')
self.assertIsNone(result)
@mock.patch('requests.get')
def test_get_capability_file_exception(self, mock_requests_get):
"""Test when the GET request raises an exception."""
mock_requests_get.side_effect = requests.exceptions.RequestException()
result = self.guidelines.get_guideline_contents('2010.03.json')
self.assertIsNone(result)

View File

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