feat: allow querying for services and project_ids based on status

The allowed query params for status are:
        'deploy_in_progress',
        'deployed',
        'update_in_progress',
        'delete_in_progress',
        'failed'

REQUEST:

GET /v1.0/admin/services?status=failed

RESPONSE:

[
  {
    "service_id": "7b68485a-1ba1-464a-93b6-9f673d637531",
    "project_id": "000"
  }
]

200 OK

Change-Id: I37b1fbf0b1339f827b707c68266fadf63fcd61fa
This commit is contained in:
Sriram Madapusi Vasudevan
2016-02-09 14:48:08 -05:00
parent e945786247
commit f230bd89b6
12 changed files with 379 additions and 146 deletions

View File

@@ -93,6 +93,13 @@ class DefaultServicesController(base.ServicesController):
domain_name))
return service_details
def get_services_by_status(self, status):
services_project_ids = \
self.storage_controller.get_services_by_status(status)
return services_project_ids
def _append_defaults(self, service_json, operation='create'):
# default origin rule
for origin in service_json.get('origins', []):

View File

@@ -0,0 +1,13 @@
CREATE TABLE service_status (
service_id UUID,
project_id VARCHAR,
status VARCHAR,
PRIMARY KEY (service_id));
CREATE INDEX status_index on service_status (status);
--//@UNDO
DROP TABLE service_status;
DROP INDEX status_index;

View File

@@ -249,6 +249,28 @@ CQL_UPDATE_CERT_DETAILS = '''
IF cert_type = %(cert_type)s AND flavor_id = %(flavor_id)s
'''
CQL_SET_SERVICE_STATUS = '''
INSERT INTO service_status (service_id,
project_id,
status
)
VALUES (%(service_id)s,
%(project_id)s,
%(status)s)
'''
CQL_GET_SERVICE_STATUS = '''
SELECT project_id,
service_id
FROM service_status
WHERE status = %(status)s
'''
CQL_DELETE_SERVICE_STATUS = '''
DELETE FROM service_status
WHERE service_id = %(service_id)s
'''
class ServicesController(base.ServicesController):
@@ -412,6 +434,42 @@ class ServicesController(base.ServicesController):
"for project_id: {1}".format(count, project_id))
return count
def get_services_by_status(self, status):
LOG.info("Fetching service_ids and "
"project_ids with status: {0}".format(status))
args = {
'status': status
}
stmt = query.SimpleStatement(
CQL_GET_SERVICE_STATUS,
consistency_level=self._driver.consistency_level)
resultset = self.session.execute(stmt, args)
complete_results = list(resultset)
for result in complete_results:
result['service_id'] = str(result['service_id'])
return complete_results
def delete_services_status(self, project_id, service_id):
LOG.info("Deleting service_id: {0} "
"with project_id: {1} from service_status "
"column family".format(service_id, project_id))
args = {
'service_id': uuid.UUID(str(service_id))
}
stmt = query.SimpleStatement(
CQL_DELETE_SERVICE_STATUS,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
def get_service_limit(self, project_id):
"""get_service_limit
@@ -706,7 +764,9 @@ class ServicesController(base.ServicesController):
pds = {provider:
json.dumps(service_obj.provider_details[provider].to_dict())
for provider in service_obj.provider_details}
status = None
for provider in service_obj.provider_details:
status = service_obj.provider_details[provider].status
log_delivery = json.dumps(service_obj.log_delivery.to_dict())
# fetch current domains
args = {
@@ -777,6 +837,17 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
status_args = {
'service_id': uuid.UUID(str(service_id)),
'project_id': project_id,
'status': status
}
stmt = query.SimpleStatement(
CQL_SET_SERVICE_STATUS,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, status_args)
def update_state(self, project_id, service_id, state):
"""update_state
@@ -822,6 +893,8 @@ class ServicesController(base.ServicesController):
pds = result.get('provider_details', {}) or {}
pds = {key: value for key, value in pds.items()}
self.delete_services_status(project_id, service_id)
if self._driver.archive_on_delete:
archive_args = {
'project_id': result.get('project_id'),
@@ -965,7 +1038,9 @@ class ServicesController(base.ServicesController):
:param service_id
:param provider_details
"""
provider_detail_dict = {}
status = None
for provider_name in sorted(provider_details.keys()):
the_provider_detail_dict = collections.OrderedDict()
the_provider_detail_dict["id"] = (
@@ -974,6 +1049,7 @@ class ServicesController(base.ServicesController):
provider_details[provider_name].access_urls)
the_provider_detail_dict["status"] = (
provider_details[provider_name].status)
status = the_provider_detail_dict["status"]
the_provider_detail_dict["name"] = (
provider_details[provider_name].name)
the_provider_detail_dict["domains_certificate_status"] = (
@@ -1001,6 +1077,17 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
args = {
'project_id': project_id,
'service_id': uuid.UUID(str(service_id)),
'status': status
}
stmt = query.SimpleStatement(
CQL_SET_SERVICE_STATUS,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
"""update_cert_info.

View File

@@ -318,11 +318,34 @@ class ServiceStatusController(base.Controller, hooks.HookController):
class AdminServiceController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
def __init__(self, driver):
super(AdminServiceController, self).__init__(driver)
self.__class__.action = OperatorServiceActionController(driver)
self.__class__.status = ServiceStatusController(driver)
@pecan.expose('json')
@pecan.expose('json')
@decorators.validate(
request=rule.Rule(
helpers.is_valid_service_status(),
helpers.abort_with_message,
stoplight_helpers.pecan_getter)
)
def get(self):
services_controller = self._driver.manager.services_controller
call_args = getattr(pecan.request.context,
"call_args")
status = call_args.pop('status')
service_projectids = services_controller.get_services_by_status(
status)
return pecan.Response(json_body=service_projectids,
status=200)
class DomainController(base.Controller, hooks.HookController):

View File

@@ -541,6 +541,31 @@ def is_valid_analytics_request(request):
}
@decorators.validation_function
def is_valid_service_status(request):
status = request.GET.get('status', "")
# NOTE(TheSriram): The statuses listed below are the currently
# supported statuses
VALID_STATUSES = [
u'deploy_in_progress',
u'deployed',
u'update_in_progress',
u'delete_in_progress',
u'failed']
if status not in VALID_STATUSES:
raise exceptions.ValidationFailed('Unknown status type {0} present, '
'Valid status types '
'are: {1}'.format(status,
VALID_STATUSES))
# Update context so the decorated function can get all this parameters
request.context.call_args = {
'status': status
}
def abort_with_message(error_info):
pecan.abort(400, detail=util.help_escape(
getattr(error_info, "message", "")),

View File

@@ -16,7 +16,6 @@
# limitations under the License.
import json
import uuid
import ddt
@@ -34,56 +33,8 @@ class TestServiceLimits(base.TestBase):
'Test Operator Functions is disabled in configuration')
self.flavor_id = self.test_flavor
self.caching_list = [
{
u"name": u"default",
u"ttl": 3600,
u"rules": [{
u"name": "default",
u"request_url": "/*"
}]
},
{
u"name": u"home",
u"ttl": 1200,
u"rules": [{
u"name": u"index",
u"request_url": u"/index.htm"
}]
}
]
self.service_list = []
def _service_limit_create_test_service(self, resp_code=False):
service_name = str(uuid.uuid1())
domain_list = [{"domain": self.generate_random_string(
prefix='www.api-test-domain') + '.com'}]
origin_list = [{"origin": self.generate_random_string(
prefix='api-test-origin') + '.com', "port": 80, "ssl": False,
"hostheadertype": "custom", "hostheadervalue":
"www.customweb.com"}]
self.log_delivery = {"enabled": False}
resp = self.service_limit_user_client.create_service(
service_name=service_name,
domain_list=domain_list,
origin_list=origin_list,
caching_list=self.caching_list,
flavor_id=self.flavor_id,
log_delivery=self.log_delivery)
if resp_code:
return resp
self.assertEqual(resp.status_code, 202)
service_url = resp.headers["location"]
return service_url
@ddt.data(-1, -10000000000, 'invalid', '学校', '', None)
def test_service_limit_parameters_invalid(self, limit):
@@ -101,10 +52,13 @@ class TestServiceLimits(base.TestBase):
self.assertEqual(resp.status_code, 201)
self.service_list = [self._service_limit_create_test_service()
for _ in range(limit)]
self.service_list = [self._service_limit_create_test_service(
client=self.service_limit_user_client)
for _ in range(limit)]
resp = self._service_limit_create_test_service(resp_code=True)
resp = self._service_limit_create_test_service(
client=self.service_limit_user_client,
resp_code=True)
self.assertEqual(resp.status_code, 403)
resp = self.operator_client.get_admin_service_limit(

View File

@@ -18,6 +18,7 @@
import ddt
from tests.api import base
from tests.api.utils.schema import admin
@ddt.ddt
@@ -29,100 +30,45 @@ class TestServiceStatus(base.TestBase):
if self.test_config.run_operator_tests is False:
self.skipTest(
'Test Operator Functions is disabled in configuration')
self.service_name = self.generate_random_string(prefix='API-Test-')
self.flavor_id = self.test_flavor
self.service_urls = []
domain = self.generate_random_string(
prefix='www.api-test-domain') + '.com'
self.domain_list = [
{"domain": domain}
]
@ddt.data((1, u'deployed'), (1, u'failed'),
(3, u'deployed'), (3, u'failed'),
(5, u'deployed'), (5, u'failed'))
def test_set_services(self, services_status):
no_of_services, status = services_status
self.service_urls = \
[self._service_limit_create_test_service(client=self.client)
for _ in range(no_of_services)]
origin = self.generate_random_string(
prefix='api-test-origin') + u'.com'
self.origin_list = [
{
u"origin": origin,
u"port": 80,
u"ssl": False,
u"rules": [{
u"name": u"default",
u"request_url": u"/*"
}]
}
]
self.caching_list = [
{
u"name": u"default",
u"ttl": 3600,
u"rules": [{
u"name": "default",
u"request_url": "/*"
}]
},
{
u"name": u"home",
u"ttl": 1200,
u"rules": [{
u"name": u"index",
u"request_url": u"/index.htm"
}]
}
]
self.restrictions_list = [
{
u"name": u"website only",
u"rules": [
{
u"name": domain,
u"referrer": domain,
u"request_url": "/*"
}
]
}
]
resp = self.setup_service(
service_name=self.service_name,
domain_list=self.domain_list,
origin_list=self.origin_list,
caching_list=self.caching_list,
restrictions_list=self.restrictions_list,
flavor_id=self.flavor_id)
self.assertEqual(resp.status_code, 202)
self.assertEqual(resp.text, '')
self.service_url = resp.headers['location']
self.client.wait_for_service_status(
location=self.service_url,
status='deployed',
abort_on_status='failed',
retry_interval=self.test_config.status_check_retry_interval,
retry_timeout=self.test_config.status_check_retry_timeout)
@ddt.data(u'deployed', u'failed')
def test_set_service(self, status):
service_id = self.service_url.rsplit('/')[-1:][0]
service_ids = [url.rsplit('/')[-1:][0] for url in self.service_urls]
project_id = self.user_project_id
set_service_resp = self.operator_client.set_service_status(
project_id=project_id,
service_id=service_id,
status=status)
for service_id, service_url in zip(service_ids, self.service_urls):
set_service_resp = self.operator_client.set_service_status(
project_id=project_id,
service_id=service_id,
status=status)
self.assertEqual(set_service_resp.status_code, 201)
self.assertEqual(set_service_resp.status_code, 201)
service_resp = self.client.get_service(self.service_url)
resp_body = service_resp.json()
resp_status = resp_body['status']
self.assertEqual(resp_status, status)
service_resp = self.client.get_service(service_url)
resp_body = service_resp.json()
resp_status = resp_body['status']
self.assertEqual(resp_status, status)
get_service_resp = self.operator_client.get_by_service_status(
status=status)
self.assertSchema(get_service_resp.json(),
admin.get_service_project_status)
self.assertIn(service_id, get_service_resp.content)
self.assertIn(project_id, get_service_resp.content)
def tearDown(self):
self.client.delete_service(location=self.service_url)
if self.test_config.generate_flavors:
self.client.delete_flavor(flavor_id=self.flavor_id)
super(TestServiceStatus, self).tearDown()
for service_url in self.service_urls:
self.client.delete_service(location=service_url)
if self.test_config.generate_flavors:
self.client.delete_flavor(flavor_id=self.flavor_id)
super(TestServiceStatus, self).tearDown()

View File

@@ -178,6 +178,58 @@ class TestBase(fixtures.BaseTestFixture):
return resp
def _service_limit_create_test_service(self, client, resp_code=False):
service_name = str(uuid.uuid1())
domain_list = [{"domain": self.generate_random_string(
prefix='www.api-test-domain') + '.com'}]
origin_list = [{"origin": self.generate_random_string(
prefix='api-test-origin') + '.com', "port": 80, "ssl": False,
"hostheadertype": "custom", "hostheadervalue":
"www.customweb.com"}]
caching_list = [
{
u"name": u"default",
u"ttl": 3600,
u"rules": [{
u"name": "default",
u"request_url": "/*"
}]
},
{
u"name": u"home",
u"ttl": 1200,
u"rules": [{
u"name": u"index",
u"request_url": u"/index.htm"
}]
}
]
log_delivery = {"enabled": False}
resp = client.create_service(
service_name=service_name,
domain_list=domain_list,
origin_list=origin_list,
caching_list=caching_list,
flavor_id=self.flavor_id,
log_delivery=log_delivery)
if resp_code:
return resp
self.assertEqual(resp.status_code, 202)
service_url = resp.headers["location"]
client.wait_for_service_status(
location=service_url,
status='DEPLOYED',
abort_on_status='FAILED',
retry_interval=self.test_config.status_check_retry_interval,
retry_timeout=self.test_config.status_check_retry_timeout)
return service_url
def assert_patch_service_details(self, actual_response, expected_response):
self.assertEqual(actual_response['name'],
expected_response['name'])

View File

@@ -274,6 +274,19 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
return self.request('POST', url, request_entity=request_object,
requestslib_kwargs=requestslib_kwargs)
def get_by_service_status(self, status,
requestslib_kwargs=None):
"""GET Services by Status
:return: Response Object containing response code 200
GET
/admin/services?status
"""
url = '{0}/admin/services?status={1}'.format(self.url, status)
return self.request('GET', url, requestslib_kwargs=requestslib_kwargs)
def admin_migrate_domain(self, project_id, service_id, domain, new_cert,
requestslib_kwargs=None):
"""Update SAN domain

View File

@@ -0,0 +1,34 @@
# Copyright (c) 2016 Rackspace, Inc.
#
# 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.
# Response Schema Definition for Get Service ids and Project id by status
from tests.api.utils.schema.services import project_id
from tests.api.utils.schema.services import service_id
get_service_project_status = {
'type': 'array',
'items': [
{
'type': 'object',
'properties': {
'service_id': service_id,
'project_id': project_id,
},
'required': ['service_id', 'project_id'],
'additionalProperties': False
}
]
}

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2015 Rackspace, Inc.
#
# 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 uuid
import ddt
from hypothesis import given
from hypothesis import strategies
import mock
import six
from poppy.manager.default.services import DefaultServicesController
from tests.functional.transport.pecan import base
@ddt.ddt
class TestGetServiceStatus(base.FunctionalTest):
@given(strategies.text())
def test_get_service_status_invalid_queryparam(self, status):
# invalid status field
try:
# NOTE(TheSriram): Py3k Hack
if six.PY3 and type(status) == str:
status = status.encode('utf-8')
url = '/v1.0/admin/services?status={0}'.format(status)
else:
url = '/v1.0/admin/services?status=%s' \
% status.decode('utf-8')
except (UnicodeDecodeError, UnicodeEncodeError):
pass
else:
response = self.app.get(url,
headers={'Content-Type':
'application/json',
'X-Project-ID':
str(uuid.uuid4())},
expect_errors=True)
self.assertEqual(response.status_code, 400)
@ddt.data('deploy_in_progress', 'deployed', 'update_in_progress',
'delete_in_progress', 'failed')
def test_get_service_status_valid_queryparam(self, status):
# valid status
with mock.patch.object(DefaultServicesController,
'get_services_by_status'):
response = self.app.get('/v1.0/admin/services'
'?status={0}'.format(status),
headers={'Content-Type':
'application/json',
'X-Project-ID':
str(uuid.uuid4())})
self.assertEqual(response.status_code, 200)

View File

@@ -231,6 +231,7 @@ class CassandraStorageServiceTests(base.TestCase):
# this is for update_provider_details unittest code coverage
arg_provider_details_dict = {}
status = None
for provider_name in provider_details_dict:
the_provider_detail_dict = collections.OrderedDict()
the_provider_detail_dict["id"] = (
@@ -239,6 +240,7 @@ class CassandraStorageServiceTests(base.TestCase):
provider_details_dict[provider_name].access_urls)
the_provider_detail_dict["status"] = (
provider_details_dict[provider_name].status)
status = the_provider_detail_dict["status"]
the_provider_detail_dict["name"] = (
provider_details_dict[provider_name].name)
the_provider_detail_dict["domains_certificate_status"] = (
@@ -251,17 +253,25 @@ class CassandraStorageServiceTests(base.TestCase):
arg_provider_details_dict[provider_name] = json.dumps(
the_provider_detail_dict)
call_args = {
provider_details_args = {
'project_id': self.project_id,
'service_id': self.service_id,
'provider_details': arg_provider_details_dict
}
status_args = {
'status': status,
'project_id': self.project_id,
'service_id': self.service_id
}
# This is to verify mock has been called with the correct arguments
def assert_mock_execute_args(*args):
self.assertEqual(args[0].query_string,
services.CQL_UPDATE_PROVIDER_DETAILS)
self.assertEqual(args[1], call_args)
if args[0].query_string == services.CQL_UPDATE_PROVIDER_DETAILS:
self.assertEqual(args[1], provider_details_args)
elif args[0].query_string == services.CQL_SET_SERVICE_STATUS:
self.assertEqual(args[1], status_args)
mock_execute.execute.side_effect = assert_mock_execute_args
self.sc.update_provider_details(