feat: addition of the BlueFlood Client
- add region mapping to every provider driver - add logic in analytics manager to call the provider, for which the domain is present - add asynchronous requests supports for blueflood and gather results REQUEST URL: GET /v1.0/services/{service_id}/analytics?domain={domain}&metricType=requestCount&startTime=2016-01-22T17:42:08&endTime=2016-01-24T17:42:08 RESPONSE: { "domain": "{domain}", "requestCount": { "India": [{}], "EMEA": [{ "1453420800000": 47, "1453507200000": 31, "1453593600000": 2 }], "APAC": [{ "1453593600000": 2 }], "North America": [{}], "South America": [{}], "Japan": [{ "1453593600000": 89 }] }, "flavor": "{flavor}", "provider": "{provider}" } Partially Implements: blueprint analytics Change-Id: I82a594c6ed1b2df93158af2b766dbbf2cd5440df
This commit is contained in:
parent
be02a04ce3
commit
0d9f84d616
|
@ -129,7 +129,8 @@ delay = 1
|
|||
|
||||
[driver:metrics:blueflood]
|
||||
blueflood_url = https://global.metrics.api.rackspacecloud.com/v2.0/{project_id}/views/
|
||||
|
||||
use_keystone_auth = True
|
||||
no_of_executors = 6
|
||||
|
||||
[drivers:provider]
|
||||
default_cache_ttl = 86400
|
||||
|
@ -179,6 +180,7 @@ group_id = "MY_GROUP_ID"
|
|||
property_id = "MY_PROPERTY_ID"
|
||||
# akamai_san_info_storage driver module (e.g. zookeeper, cassandra)
|
||||
san_info_storage_type = cassandra
|
||||
metrics_resolution = 86400
|
||||
|
||||
[drivers:provider:akamai:storage]
|
||||
# Zookeeper san_info_storage_type config options
|
||||
|
|
|
@ -29,6 +29,10 @@ class BadProviderDetail(Exception):
|
|||
"""Raised when attempted a non existent operation."""
|
||||
|
||||
|
||||
class ProviderNotFound(Exception):
|
||||
"""Raised when domain is not associated with a known Provider"""
|
||||
|
||||
|
||||
class ServiceNotFound(Exception):
|
||||
"""Raised when service is not found."""
|
||||
|
||||
|
@ -61,3 +65,8 @@ class ServicesOverLimit(Exception):
|
|||
class SharedShardsExhausted(Exception):
|
||||
|
||||
"""Raised when all shared ssl shards are occupied for a given domain."""
|
||||
|
||||
|
||||
class ServiceProviderDetailsNotFound(Exception):
|
||||
|
||||
"""Raised when provider details for a service is None."""
|
||||
|
|
|
@ -25,8 +25,21 @@ class AnalyticsController(controller.ManagerControllerBase):
|
|||
"""Home controller base class."""
|
||||
|
||||
def __init__(self, manager):
|
||||
self.manager = manager
|
||||
super(AnalyticsController, self).__init__(manager)
|
||||
|
||||
@property
|
||||
def storage_controller(self):
|
||||
return self.manager.storage.services_controller
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return self.manager.providers
|
||||
|
||||
@property
|
||||
def metrics_controller(self):
|
||||
return self.manager.metrics.services_controller
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
"""get analytics metrics by domain
|
||||
|
|
|
@ -12,33 +12,61 @@
|
|||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import json
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from poppy.common import errors
|
||||
from poppy.manager import base
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class AnalyticsController(base.AnalyticsController):
|
||||
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
# TODO(TheSriram): Insert call to metrics driver
|
||||
self.metrics_controller = self._driver.metrics.services_controller
|
||||
# NOTE(TheSriram): Returning Stubbed return value
|
||||
metrics_response = {
|
||||
"domain": "example.com",
|
||||
"StatusCodes_2XX": [
|
||||
{
|
||||
"US": {
|
||||
"1453136297": 24,
|
||||
"1453049897": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"EMEA": {
|
||||
"1453136297": 123,
|
||||
"1453049897": 11
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return json.dumps(metrics_response)
|
||||
storage_controller = self.storage_controller
|
||||
try:
|
||||
result = storage_controller.get_service_details_by_domain_name(
|
||||
domain_name=domain_name, project_id=project_id)
|
||||
except ValueError:
|
||||
msg = "Domain: {0} was not found for project_id: {1}".format(
|
||||
domain_name, project_id)
|
||||
LOG.warn(msg)
|
||||
raise errors.ServiceNotFound(msg)
|
||||
if not result:
|
||||
msg = "Domain: {0} was not found for project_id: {1}".format(
|
||||
domain_name, project_id)
|
||||
LOG.warn(msg)
|
||||
raise errors.ServiceNotFound(msg)
|
||||
|
||||
if not result.provider_details:
|
||||
msg = "Provider Details were None " \
|
||||
"for the service_id: {0} " \
|
||||
"corresponding to project_id: {1}".format(result.service_id,
|
||||
project_id)
|
||||
LOG.warn(msg)
|
||||
raise errors.ServiceProviderDetailsNotFound(msg)
|
||||
|
||||
provider_details_dict = result.provider_details
|
||||
|
||||
provider_for_domain = None
|
||||
|
||||
for provider, provider_details in provider_details_dict.items():
|
||||
if provider_details.get_domain_access_url(domain=domain_name):
|
||||
provider_for_domain = provider
|
||||
|
||||
if not provider_for_domain:
|
||||
msg = "Provider not found for Domain : {0}".format(domain_name)
|
||||
LOG.warn(msg)
|
||||
raise errors.ProviderNotFound(msg)
|
||||
|
||||
provider_obj = self.providers[provider_for_domain.lower()].obj
|
||||
provider_service_controller = provider_obj.service_controller
|
||||
extras['metrics_controller'] = self.metrics_controller
|
||||
metrics = provider_service_controller.get_metrics_by_domain(
|
||||
project_id, domain_name, provider_obj.regions, **extras)
|
||||
|
||||
metrics['provider'] = provider_for_domain.lower()
|
||||
metrics['flavor'] = result.flavor_id
|
||||
return metrics
|
||||
|
|
|
@ -28,7 +28,7 @@ class ServicesControllerBase(controller.MetricsControllerBase):
|
|||
def __init__(self, driver):
|
||||
super(ServicesControllerBase, self).__init__(driver)
|
||||
|
||||
def read(self, metric_name, from_timestamp, to_timestamp, resolution):
|
||||
def read(self, metric_names, from_timestamp, to_timestamp, resolution):
|
||||
"""read metrics from cache.
|
||||
|
||||
:raises NotImplementedError
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Cloud Metrics Cache driver for CDN"""
|
||||
"""BlueFlood Metrics driver for Poppy"""
|
||||
|
||||
from poppy.metrics.blueflood import driver
|
||||
|
||||
|
|
|
@ -21,8 +21,15 @@ from poppy.metrics.blueflood import controllers
|
|||
|
||||
BLUEFLOOD_OPTIONS = [
|
||||
cfg.StrOpt('blueflood_url',
|
||||
default='https://www.metrics.com',
|
||||
default='https://www.metrics.com/{project_id}/views',
|
||||
help='Metrics url for retrieving cached content'),
|
||||
cfg.BoolOpt('use_keystone_auth',
|
||||
default=True,
|
||||
help='Use Keystone Authentication?'),
|
||||
cfg.IntOpt('no_of_executors',
|
||||
default=6,
|
||||
help='Number of Executors for Parallel execution of requests'
|
||||
'to blueflood metrics backing store'),
|
||||
]
|
||||
|
||||
BLUEFLOOD_GROUP = 'drivers:metrics:blueflood'
|
||||
|
|
|
@ -14,7 +14,15 @@
|
|||
# limitations under the License.
|
||||
|
||||
|
||||
from oslo_context import context as context_utils
|
||||
from oslo_log import log
|
||||
|
||||
from poppy.metrics import base
|
||||
from poppy.metrics.blueflood.utils import client
|
||||
from poppy.metrics.blueflood.utils import errors
|
||||
from poppy.metrics.blueflood.utils import helper
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class ServicesController(base.ServicesController):
|
||||
|
@ -24,8 +32,78 @@ class ServicesController(base.ServicesController):
|
|||
|
||||
self.driver = driver
|
||||
|
||||
def read(self, metric_name, from_timestamp, to_timestamp, resolution):
|
||||
def _result_formatter(self, response):
|
||||
|
||||
resp_dict = dict()
|
||||
|
||||
if not response.ok:
|
||||
LOG.warn("BlueFlood Metrics Response status Code:{0} "
|
||||
"Response Text: {1} "
|
||||
"Request URL: {2}".format(response.status_code,
|
||||
response.text,
|
||||
response.url))
|
||||
return resp_dict
|
||||
else:
|
||||
|
||||
serialized_response = response.json()
|
||||
try:
|
||||
time_series = serialized_response['values']
|
||||
for timerange in time_series:
|
||||
resp_dict[timerange['timestamp']] = timerange['sum']
|
||||
except KeyError:
|
||||
msg = 'content from {0} not conforming ' \
|
||||
'to API contracts'.format(response.url)
|
||||
LOG.warn(msg)
|
||||
raise errors.BlueFloodApiSchemaError(msg)
|
||||
|
||||
return resp_dict
|
||||
|
||||
def read(self, metric_names, from_timestamp, to_timestamp, resolution):
|
||||
"""read metrics from metrics driver.
|
||||
|
||||
"""
|
||||
pass
|
||||
curr_resolution = \
|
||||
helper.resolution_converter_seconds_to_enum(resolution)
|
||||
context_dict = context_utils.get_current().to_dict()
|
||||
|
||||
project_id = context_dict['tenant']
|
||||
auth_token = None
|
||||
if self.driver.metrics_conf.use_keystone_auth:
|
||||
auth_token = context_dict['auth_token']
|
||||
|
||||
tenanted_blueflood_url = \
|
||||
self.driver.metrics_conf.blueflood_url.format(
|
||||
project_id=project_id
|
||||
)
|
||||
from_timestamp = int(helper.datetime_to_epoch(from_timestamp))
|
||||
to_timestamp = int(helper.datetime_to_epoch(to_timestamp))
|
||||
urls = []
|
||||
params = {
|
||||
'to': to_timestamp,
|
||||
'from': from_timestamp,
|
||||
'resolution': curr_resolution
|
||||
}
|
||||
for metric_name in metric_names:
|
||||
tenanted_blueflood_url_with_metric = helper.join_url(
|
||||
tenanted_blueflood_url, metric_name)
|
||||
urls.append(helper.set_qs_on_url(
|
||||
tenanted_blueflood_url_with_metric,
|
||||
**params))
|
||||
executors = self.driver.metrics_conf.no_of_executors
|
||||
blueflood_client = client.BlueFloodMetricsClient(token=auth_token,
|
||||
project_id=project_id,
|
||||
executors=executors)
|
||||
results = blueflood_client.async_requests(urls)
|
||||
reordered_metric_names = []
|
||||
for result in results:
|
||||
metric_name = helper.retrieve_last_relative_url(result.url)
|
||||
reordered_metric_names.append(metric_name)
|
||||
|
||||
formatted_results = []
|
||||
for metric_name, result in zip(reordered_metric_names, results):
|
||||
formatted_result = self._result_formatter(result)
|
||||
# NOTE(TheSriram): Tuple to pass the associated metric name, along
|
||||
# with the formatted result
|
||||
formatted_results.append((metric_name, formatted_result))
|
||||
|
||||
return formatted_results
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# 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.
|
||||
from concurrent import futures
|
||||
|
||||
from requests_futures.sessions import FuturesSession
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class BlueFloodMetricsClient(object):
|
||||
|
||||
def __init__(self, token, project_id, executors):
|
||||
self.token = token
|
||||
self.project_id = project_id
|
||||
self.session = FuturesSession(max_workers=executors)
|
||||
self.headers = {
|
||||
'X-Project-ID': self.project_id
|
||||
}
|
||||
if self.token:
|
||||
self.headers.update({
|
||||
'X-Auth-Token': self.token
|
||||
})
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def async_requests(self, urls):
|
||||
futures_results = []
|
||||
for url in urls:
|
||||
LOG.info("Request made to URL: {0}".format(url))
|
||||
futures_results.append(self.session.get(url))
|
||||
|
||||
responses = []
|
||||
|
||||
for future in futures.as_completed(fs=futures_results):
|
||||
resp = future.result()
|
||||
LOG.info("Request completed to URL: {0}".format(resp.url))
|
||||
responses.append((resp))
|
||||
|
||||
return responses
|
|
@ -0,0 +1,18 @@
|
|||
# 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.
|
||||
|
||||
|
||||
class BlueFloodApiSchemaError(Exception):
|
||||
pass
|
|
@ -0,0 +1,70 @@
|
|||
# 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.
|
||||
|
||||
import time
|
||||
|
||||
try: # pragma: no cover
|
||||
import six.moves.urllib.parse as parse
|
||||
except ImportError: # pragma: no cover
|
||||
import urllib.parse as parse
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def set_qs_on_url(url, **params):
|
||||
|
||||
url_parts = list(parse.urlparse(url))
|
||||
query = dict(parse.parse_qsl(url_parts[4]))
|
||||
query.update(params)
|
||||
|
||||
url_parts[4] = parse.urlencode(query)
|
||||
|
||||
return parse.urlunparse(url_parts)
|
||||
|
||||
|
||||
def retrieve_last_relative_url(url):
|
||||
url_parts = list(parse.urlparse(url))
|
||||
return url_parts[2].split('/')[-1:][0]
|
||||
|
||||
|
||||
def join_url(base_url, url):
|
||||
return parse.urljoin(base_url, url)
|
||||
|
||||
|
||||
def datetime_to_epoch(datetime_obj):
|
||||
return time.mktime(datetime_obj.timetuple()) * 1000
|
||||
|
||||
|
||||
def resolution_converter_seconds_to_enum(resolution_seconds):
|
||||
|
||||
resolution_resolver = {
|
||||
'0': 'FULL',
|
||||
'300': 'MIN5',
|
||||
'1200': 'MIN20',
|
||||
'3600': 'MIN60',
|
||||
'14400': 'MIN240',
|
||||
'86400': 'MIN1440'
|
||||
}
|
||||
try:
|
||||
return resolution_resolver[resolution_seconds]
|
||||
except KeyError:
|
||||
msg = 'Resolution of {0} does not translate ' \
|
||||
'into BlueFlood time windows, ' \
|
||||
'acceptable windows: {1}'.format(resolution_seconds,
|
||||
resolution_resolver.keys())
|
||||
LOG.error(msg)
|
||||
raise ValueError(msg)
|
|
@ -25,6 +25,7 @@ from stevedore import driver
|
|||
|
||||
from poppy.common import decorators
|
||||
from poppy.provider.akamai import controllers
|
||||
from poppy.provider.akamai import geo_zone_code_mapping
|
||||
from poppy.provider.akamai.mod_san_queue import zookeeper_queue
|
||||
from poppy.provider import base
|
||||
import uuid
|
||||
|
@ -103,6 +104,11 @@ AKAMAI_OPTIONS = [
|
|||
cfg.StrOpt(
|
||||
'group_id',
|
||||
help='Operator groupID'),
|
||||
|
||||
# Metrics related configs
|
||||
cfg.StrOpt('metrics_resolution',
|
||||
help='Resolution in seconds for retrieving metrics',
|
||||
default='86400')
|
||||
]
|
||||
|
||||
AKAMAI_GROUP = 'drivers:provider:akamai'
|
||||
|
@ -132,7 +138,7 @@ class CDNProvider(base.Driver):
|
|||
str(self.akamai_conf.ccu_api_base_url),
|
||||
'ccu/v2/queues/default'
|
||||
])
|
||||
|
||||
self.regions = geo_zone_code_mapping.REGIONS
|
||||
self.http_conf_number = self.akamai_conf.akamai_http_config_number
|
||||
self.https_shared_conf_number = (
|
||||
self.akamai_conf.akamai_https_shared_config_number)
|
||||
|
@ -186,6 +192,8 @@ class CDNProvider(base.Driver):
|
|||
self.mod_san_queue = (
|
||||
zookeeper_queue.ZookeeperModSanQueue(self._conf))
|
||||
|
||||
self.metrics_resolution = self.akamai_conf.metrics_resolution
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
def san_info_storage(self):
|
||||
storage_backend_type = 'poppy.provider.akamai.san_info_storage'
|
||||
|
|
|
@ -17,9 +17,10 @@ from oslo_log import log
|
|||
|
||||
from poppy.model.helpers import geo_zones
|
||||
|
||||
# to use log inside worker, we need to directly use logging
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
REGIONS = ['North America', 'South America', 'EMEA', 'Japan', 'India', 'APAC']
|
||||
|
||||
REGION_COUNTRY_MAPPING = {
|
||||
'North America': [
|
||||
'Antigua and Barbuda',
|
||||
|
|
|
@ -17,6 +17,11 @@ import datetime
|
|||
import json
|
||||
import traceback
|
||||
|
||||
try: # pragma: no cover
|
||||
import six.moves.urllib.parse as parse
|
||||
except ImportError: # pragma: no cover
|
||||
import urllib.parse as parse
|
||||
|
||||
from oslo_log import log
|
||||
|
||||
from poppy.common import decorators
|
||||
|
@ -1009,6 +1014,45 @@ class ServiceController(base.ServiceBase):
|
|||
id_list.append(dp_obj)
|
||||
return json.dumps(id_list)
|
||||
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
'''Use Akamai's report API to get the metrics by domain.'''
|
||||
return []
|
||||
def get_metrics_by_domain(self, project_id, domain_name, regions,
|
||||
**extras):
|
||||
"""Use Akamai's report API to get the metrics by domain."""
|
||||
|
||||
formatted_results = dict()
|
||||
metric_buckets = []
|
||||
metricType = extras['metricType']
|
||||
startTime = extras['startTime']
|
||||
endTime = extras['endTime']
|
||||
metrics_controller = extras['metrics_controller']
|
||||
resolution = self.driver.metrics_resolution
|
||||
if 'httpResponseCode' in metricType:
|
||||
http_series = metricType.split('_')[1]
|
||||
for region in regions:
|
||||
metric_buckets.append('_'.join(['requestCount', domain_name,
|
||||
region,
|
||||
http_series]))
|
||||
else:
|
||||
for region in regions:
|
||||
metric_buckets.append('_'.join([metricType, domain_name,
|
||||
region]))
|
||||
|
||||
metrics_results = metrics_controller.read(metric_names=metric_buckets,
|
||||
from_timestamp=startTime,
|
||||
to_timestamp=endTime,
|
||||
resolution=resolution)
|
||||
|
||||
formatted_results['domain'] = domain_name
|
||||
formatted_results[metricType] = dict()
|
||||
|
||||
for region in regions:
|
||||
formatted_results[metricType][region] = []
|
||||
for metric_name, metrics_response in metrics_results:
|
||||
unquoted_metric_name = parse.unquote(
|
||||
metric_name.split('_')[2]
|
||||
).lower()
|
||||
if region.lower() == unquoted_metric_name:
|
||||
formatted_results[metricType][region].append(
|
||||
metrics_response
|
||||
)
|
||||
|
||||
return formatted_results
|
||||
|
|
|
@ -77,10 +77,12 @@ class ServicesControllerBase(controller.ProviderControllerBase):
|
|||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
def get_metrics_by_domain(self, project_id, domain_name, region, **extras):
|
||||
"""get analytics metrics by domain from provider
|
||||
|
||||
:param service_name
|
||||
:param project_id
|
||||
:param domain_name
|
||||
:param regions
|
||||
:raises NotImplementedError
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -45,6 +45,7 @@ class CDNProvider(base.Driver):
|
|||
self.cloudfront_client = boto.connect_cloudfront(
|
||||
aws_access_key_id=self.cloudfront_conf.aws_access_key_id,
|
||||
aws_secret_access_key=self.cloudfront_conf.aws_secret_access_key)
|
||||
self.regions = []
|
||||
|
||||
def is_alive(self):
|
||||
"""is_alive.
|
||||
|
|
|
@ -99,7 +99,8 @@ class ServiceController(base.ServiceBase):
|
|||
def get_provider_service_id(self, service_obj):
|
||||
return service_obj.name
|
||||
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
def get_metrics_by_domain(self, project_id, domain_name, regions,
|
||||
**extras):
|
||||
'''Use CloudFronts's API to get the metrics by domain.'''
|
||||
return []
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ class CDNProvider(base.Driver):
|
|||
setattr(obj, fastly_scheme, self.fastly_conf.scheme)
|
||||
|
||||
self.fastly_client = fastly.connect(self.fastly_conf.apikey)
|
||||
self.regions = []
|
||||
|
||||
def is_alive(self):
|
||||
"""is_alive.
|
||||
|
|
|
@ -249,6 +249,7 @@ class ServiceController(base.ServiceBase):
|
|||
def get_provider_service_id(self, service_obj):
|
||||
return service_obj.service_id
|
||||
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
def get_metrics_by_domain(self, project_id, domain_name, regions,
|
||||
**extras):
|
||||
'''Use Fastly's API to get the metrics by domain.'''
|
||||
return []
|
||||
|
|
|
@ -36,7 +36,7 @@ MAXCDN_GROUP = 'drivers:provider:maxcdn'
|
|||
|
||||
|
||||
class CDNProvider(base.Driver):
|
||||
"""MaxCND Provider."""
|
||||
"""MaxCDN Provider."""
|
||||
|
||||
def __init__(self, conf):
|
||||
"""Init constructor."""
|
||||
|
@ -49,6 +49,7 @@ class CDNProvider(base.Driver):
|
|||
self.maxcdn_client = maxcdn.MaxCDN(self.maxcdn_conf.alias,
|
||||
self.maxcdn_conf.consumer_key,
|
||||
self.maxcdn_conf.consumer_secret)
|
||||
self.regions = []
|
||||
|
||||
def is_alive(self):
|
||||
"""is_alive.
|
||||
|
|
|
@ -137,7 +137,8 @@ class ServiceController(base.ServiceBase):
|
|||
def get_provider_service_id(self, service_obj):
|
||||
return self._map_service_name(service_obj.name)
|
||||
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
def get_metrics_by_domain(self, project_id, domain_name, region,
|
||||
**extras):
|
||||
'''Use MaxCDN's API to get the metrics by domain.'''
|
||||
return []
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ class CDNProvider(base.Driver):
|
|||
|
||||
def __init__(self, conf):
|
||||
super(CDNProvider, self).__init__(conf)
|
||||
self.regions = []
|
||||
|
||||
def is_alive(self):
|
||||
"""is_alive.
|
||||
|
|
|
@ -54,7 +54,8 @@ class ServiceController(base.ServiceBase):
|
|||
def get_provider_service_id(self, service_obj):
|
||||
return []
|
||||
|
||||
def get_metrics_by_domain(self, project_id, domain_name, **extras):
|
||||
def get_metrics_by_domain(self, project_id, domain_name, regions,
|
||||
**extras):
|
||||
return []
|
||||
|
||||
@decorators.lazy_property(write=False)
|
||||
|
|
|
@ -898,7 +898,7 @@ class ServicesController(base.ServicesController):
|
|||
results[provider_name] = provider_detail_obj
|
||||
return results
|
||||
|
||||
def get_service_details_by_domain_name(self, domain_name):
|
||||
def get_service_details_by_domain_name(self, domain_name, project_id=None):
|
||||
"""get_provider_details_by_domain_name.
|
||||
|
||||
:param domain_name
|
||||
|
@ -920,6 +920,11 @@ class ServicesController(base.ServicesController):
|
|||
details = None
|
||||
for r in complete_results:
|
||||
proj_id = r.get('project_id')
|
||||
if project_id and proj_id != project_id:
|
||||
raise ValueError("Domain: {0} not "
|
||||
"present under "
|
||||
"project_id: {1}".format(domain_name,
|
||||
project_id))
|
||||
service = r.get('service_id')
|
||||
details = self.get(proj_id, service)
|
||||
return details
|
||||
|
|
|
@ -170,7 +170,8 @@ class ServicesController(base.ServicesController):
|
|||
if key in self.certs:
|
||||
self.certs[key].cert_details = cert_details
|
||||
|
||||
def get_service_details_by_domain_name(self, domain_name):
|
||||
def get_service_details_by_domain_name(self, domain_name,
|
||||
project_id=None):
|
||||
for service_id in self.created_services:
|
||||
service_dict_in_cache = self.created_services[service_id]
|
||||
if domain_name in [d['domain']
|
||||
|
|
|
@ -117,14 +117,18 @@ class ServicesAnalyticsController(base.Controller, hooks.HookController):
|
|||
domain = call_args.pop('domain')
|
||||
analytics_controller = \
|
||||
self._driver.manager.analytics_controller
|
||||
|
||||
res = analytics_controller.get_metrics_by_domain(
|
||||
self.project_id,
|
||||
domain,
|
||||
**call_args
|
||||
)
|
||||
|
||||
return pecan.Response(res, 200)
|
||||
try:
|
||||
res = analytics_controller.get_metrics_by_domain(
|
||||
self.project_id,
|
||||
domain,
|
||||
**call_args
|
||||
)
|
||||
except errors.ServiceNotFound:
|
||||
return pecan.Response(status=404)
|
||||
except Exception:
|
||||
return pecan.Response(status=500)
|
||||
else:
|
||||
return pecan.Response(json_body=res, status=200)
|
||||
|
||||
|
||||
class ServicesController(base.Controller, hooks.HookController):
|
||||
|
|
|
@ -493,7 +493,8 @@ def is_valid_analytics_request(request):
|
|||
"%Y-%m-%dT%H:%M:%S"))
|
||||
endTime = request.GET.get('endTime',
|
||||
default_end_time.strftime("%Y-%m-%dT%H:%M:%S"))
|
||||
# Default metric type will be all metrics
|
||||
|
||||
# NOTE(TheSriram): metricType is a required entity
|
||||
metricType = request.GET.get('metricType', None)
|
||||
|
||||
if not is_valid_domain_name(domain):
|
||||
|
@ -515,11 +516,16 @@ def is_valid_analytics_request(request):
|
|||
raise exceptions.ValidationFailed('startTime cannot be later than'
|
||||
' endTime')
|
||||
|
||||
# Leave these 3 metric types for now.
|
||||
# NOTE(TheSriram): The metrics listed below are the currently supported
|
||||
# metric types
|
||||
valid_metric_types = [
|
||||
'requestCount',
|
||||
'bandwithOut',
|
||||
'httpResponseCode'
|
||||
'bandwidthOut',
|
||||
'httpResponseCode_1XX',
|
||||
'httpResponseCode_2XX',
|
||||
'httpResponseCode_3XX',
|
||||
'httpResponseCode_4XX',
|
||||
'httpResponseCode_5XX'
|
||||
]
|
||||
if metricType not in valid_metric_types:
|
||||
raise exceptions.ValidationFailed('Must provide an metric name....'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
requests-futures
|
|
@ -14,11 +14,16 @@
|
|||
# limitations under the License.
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import mock
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
import six
|
||||
|
||||
from poppy.common import errors
|
||||
from poppy.manager.default.analytics import AnalyticsController
|
||||
|
||||
from tests.functional.transport.pecan import base
|
||||
|
||||
if six.PY2: # pragma: no cover
|
||||
|
@ -37,36 +42,47 @@ class TestServicesAnalytics(base.FunctionalTest):
|
|||
self.service_id = str(uuid.uuid1())
|
||||
self.endTime = datetime.datetime.now()
|
||||
self.startTime = self.endTime - datetime.timedelta(hours=3)
|
||||
self.start_time = datetime.datetime.strftime(
|
||||
self.startTime,
|
||||
"%Y-%m-%dT%H:%M:%S")
|
||||
self.end_time = datetime.datetime.strftime(
|
||||
self.endTime,
|
||||
"%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
def test_services_analytics_happy_path_with_default_timewindow(self):
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
})
|
||||
with mock.patch.object(AnalyticsController,
|
||||
'get_metrics_by_domain') as mock_get:
|
||||
mock_get.return_value = json.dumps({})
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_services_analytics_happy_path(self):
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
'startTime': datetime.datetime.strftime(
|
||||
self.startTime, "%Y-%m-%dT%H:%M:%S"),
|
||||
'endTime': datetime.datetime.strftime(
|
||||
self.endTime, "%Y-%m-%dT%H:%M:%S")
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
})
|
||||
with mock.patch.object(AnalyticsController,
|
||||
'get_metrics_by_domain') as mock_get:
|
||||
mock_get.return_value = json.dumps({})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
'startTime': self.start_time,
|
||||
'endTime': self.end_time
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ddt.file_data("data_services_analytics_bad_input.json")
|
||||
def test_services_analytics_negative(self, get_params):
|
||||
|
@ -79,3 +95,61 @@ class TestServicesAnalytics(base.FunctionalTest):
|
|||
expect_errors=True)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_services_analytics_exceptions_no_service(self):
|
||||
with mock.patch.object(AnalyticsController,
|
||||
'get_metrics_by_domain') as mock_get:
|
||||
|
||||
mock_get.side_effect = errors.ServiceNotFound
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
'startTime': self.start_time,
|
||||
'endTime': self.end_time
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_services_analytics_exceptions_provider_details(self):
|
||||
with mock.patch.object(AnalyticsController,
|
||||
'get_metrics_by_domain') as mock_get:
|
||||
mock_get.side_effect = errors.ServiceProviderDetailsNotFound
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
'startTime': self.start_time,
|
||||
'endTime': self.end_time
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
def test_services_analytics_negative_exceptions_no_provider(self):
|
||||
with mock.patch.object(AnalyticsController,
|
||||
'get_metrics_by_domain') as mock_get:
|
||||
mock_get.side_effect = errors.ProviderNotFound
|
||||
response = self.app.get('/v1.0/services/%s/analytics' %
|
||||
self.service_id,
|
||||
params=urllib.urlencode({
|
||||
'domain': 'abc.com',
|
||||
'metricType': 'requestCount',
|
||||
'startTime': self.start_time,
|
||||
'endTime': self.end_time
|
||||
}),
|
||||
headers={
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
|
|
@ -7,6 +7,7 @@ mock
|
|||
nose
|
||||
openstack.nose_plugin
|
||||
oslotest>=1.9.0
|
||||
requests-mock
|
||||
testrepository
|
||||
testtools
|
||||
python-heatclient
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
# 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.
|
||||
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from poppy.common import errors
|
||||
from poppy.manager.default import driver
|
||||
from poppy.manager.default import services
|
||||
from tests.unit import base
|
||||
|
||||
|
||||
class StorageResult(object):
|
||||
|
||||
def __init__(self, provider_details=None, flavor_id=None):
|
||||
self.provider_details = provider_details
|
||||
self.service_id = str(uuid.uuid4())
|
||||
self.flavor_id = flavor_id
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class DefaultManagerServiceTests(base.TestCase):
|
||||
|
||||
@mock.patch('poppy.notification.base.driver.NotificationDriverBase')
|
||||
@mock.patch('poppy.dns.base.driver.DNSDriverBase')
|
||||
@mock.patch('poppy.storage.base.driver.StorageDriverBase')
|
||||
@mock.patch('poppy.distributed_task.base.driver.DistributedTaskDriverBase')
|
||||
@mock.patch('poppy.metrics.base.driver.MetricsDriverBase')
|
||||
def setUp(self, mock_metrics, mock_distributed_task,
|
||||
mock_storage, mock_dns, mock_notification):
|
||||
# NOTE(TheSriram): the mock.patch decorator applies mocks
|
||||
# in the reverse order of the arguments present
|
||||
super(DefaultManagerServiceTests, self).setUp()
|
||||
|
||||
# create mocked config and driver
|
||||
conf = cfg.ConfigOpts()
|
||||
|
||||
_DRIVER_DNS_OPTIONS = [
|
||||
cfg.IntOpt(
|
||||
'retries',
|
||||
default=5,
|
||||
help='Total number of Retries after '
|
||||
'Exponentially Backing Off'),
|
||||
cfg.IntOpt(
|
||||
'min_backoff_range',
|
||||
default=20,
|
||||
help='Minimum Number of seconds to sleep between retries'),
|
||||
cfg.IntOpt(
|
||||
'max_backoff_range',
|
||||
default=30,
|
||||
help='Maximum Number of seconds to sleep between retries'),
|
||||
]
|
||||
|
||||
_PROVIDER_OPTIONS = [
|
||||
cfg.IntOpt(
|
||||
'default_cache_ttl',
|
||||
default=86400,
|
||||
help='Default ttl to be set, when no caching '
|
||||
'rules are specified'),
|
||||
]
|
||||
_MAX_SERVICE_OPTIONS = [
|
||||
cfg.IntOpt('max_services_per_project', default=20,
|
||||
help='Default max service per project_id')
|
||||
]
|
||||
_DRIVER_DNS_GROUP = 'driver:dns'
|
||||
_PROVIDER_GROUP = 'drivers:provider'
|
||||
_MAX_SERVICE_GROUP = 'drivers:storage'
|
||||
|
||||
conf.register_opts(_PROVIDER_OPTIONS, group=_PROVIDER_GROUP)
|
||||
conf.register_opts(_DRIVER_DNS_OPTIONS, group=_DRIVER_DNS_GROUP)
|
||||
conf.register_opts(_MAX_SERVICE_OPTIONS, group=_MAX_SERVICE_GROUP)
|
||||
self.max_services_per_project = \
|
||||
conf[_MAX_SERVICE_GROUP].max_services_per_project
|
||||
|
||||
provider_mock = mock.Mock()
|
||||
provider_mock.obj = mock.Mock()
|
||||
provider_mock.obj.service_controller = mock.Mock()
|
||||
provider_mock.obj.service_controller.get_metrics_by_domain = \
|
||||
mock.Mock(return_value=dict())
|
||||
|
||||
# mock a stevedore provider extension
|
||||
def get_provider_by_name(name):
|
||||
name_p_name_mapping = {
|
||||
'mock_provider': provider_mock,
|
||||
'maxcdn': 'MaxCDN',
|
||||
'cloudfront': 'CloudFront',
|
||||
'fastly': 'Fastly',
|
||||
'mock': 'Mock',
|
||||
'Provider': 'Provider'
|
||||
}
|
||||
if name == 'mock_provider':
|
||||
return name_p_name_mapping[name]
|
||||
else:
|
||||
return mock.Mock(obj=mock.Mock(provider_name=(
|
||||
name_p_name_mapping[name])))
|
||||
|
||||
mock_providers = mock.MagicMock()
|
||||
mock_providers.__getitem__.side_effect = get_provider_by_name
|
||||
|
||||
manager_driver = driver.DefaultManagerDriver(conf,
|
||||
mock_storage,
|
||||
mock_providers,
|
||||
mock_dns,
|
||||
mock_distributed_task,
|
||||
mock_notification,
|
||||
mock_metrics)
|
||||
|
||||
# stubbed driver
|
||||
|
||||
self.sc = services.DefaultServicesController(manager_driver)
|
||||
self.manager = manager_driver
|
||||
self.project_id = str(uuid.uuid4())
|
||||
self.domain_name = str(uuid.uuid4())
|
||||
|
||||
def test_analytics_get_metrics_by_domain_provider_details_None(self):
|
||||
analytics_controller = \
|
||||
self.manager.analytics_controller
|
||||
extras = {}
|
||||
storage_controller = \
|
||||
self.manager.analytics_controller._driver.storage
|
||||
|
||||
services_controller = storage_controller.services_controller
|
||||
services_controller.get_service_details_by_domain_name = \
|
||||
mock.Mock(return_value=StorageResult())
|
||||
self.assertRaises(errors.ServiceProviderDetailsNotFound,
|
||||
analytics_controller.get_metrics_by_domain,
|
||||
self.project_id, self.domain_name, **extras)
|
||||
|
||||
def test_analytics_get_metrics_by_domain_service_not_found(self):
|
||||
analytics_controller = \
|
||||
self.manager.analytics_controller
|
||||
extras = {}
|
||||
storage_controller = \
|
||||
self.manager.analytics_controller._driver.storage
|
||||
|
||||
services_controller = storage_controller.services_controller
|
||||
services_controller.get_service_details_by_domain_name = \
|
||||
mock.Mock(return_value=None)
|
||||
self.assertRaises(errors.ServiceNotFound,
|
||||
analytics_controller.get_metrics_by_domain,
|
||||
self.project_id, self.domain_name, **extras)
|
||||
|
||||
def test_analytics_get_metrics_by_domain_provider_not_found(self):
|
||||
analytics_controller = \
|
||||
self.manager.analytics_controller
|
||||
extras = {}
|
||||
storage_controller = \
|
||||
self.manager.analytics_controller._driver.storage
|
||||
|
||||
actual_provider_details = mock.Mock()
|
||||
actual_provider_details.get_domain_access_url = \
|
||||
mock.Mock(return_value=None)
|
||||
provider_details_dict = {
|
||||
'Provider': actual_provider_details
|
||||
}
|
||||
services_controller = storage_controller.services_controller
|
||||
services_controller.get_service_details_by_domain_name = \
|
||||
mock.Mock(return_value=StorageResult(
|
||||
provider_details=provider_details_dict))
|
||||
self.assertRaises(errors.ProviderNotFound,
|
||||
analytics_controller.get_metrics_by_domain,
|
||||
self.project_id, self.domain_name, **extras)
|
||||
|
||||
def test_analytics_get_metrics_by_domain_happy_path(self):
|
||||
|
||||
analytics_controller = \
|
||||
self.manager.analytics_controller
|
||||
extras = {}
|
||||
storage_controller = \
|
||||
self.manager.analytics_controller._driver.storage
|
||||
|
||||
actual_provider_details = mock.Mock()
|
||||
actual_provider_details.get_domain_access_url = \
|
||||
mock.Mock(return_value=self.domain_name)
|
||||
provider_details_dict = {
|
||||
'Mock_Provider': actual_provider_details
|
||||
}
|
||||
services_controller = storage_controller.services_controller
|
||||
services_controller.get_service_details_by_domain_name = \
|
||||
mock.Mock(return_value=StorageResult(
|
||||
provider_details=provider_details_dict,
|
||||
flavor_id='mock_flavor'))
|
||||
|
||||
results = analytics_controller.get_metrics_by_domain(self.project_id,
|
||||
self.domain_name,
|
||||
**extras)
|
||||
|
||||
self.assertEqual(results['provider'], 'mock_provider')
|
||||
self.assertEqual(results['flavor'], 'mock_flavor')
|
|
@ -0,0 +1,66 @@
|
|||
# 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.
|
||||
|
||||
"""Unittests for BlueFlood client"""
|
||||
|
||||
|
||||
import uuid
|
||||
|
||||
from poppy.metrics.blueflood.utils import client
|
||||
|
||||
from tests.unit import base
|
||||
|
||||
import requests_mock
|
||||
|
||||
|
||||
class TestBlueFloodClient(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBlueFloodClient, self).setUp()
|
||||
self.project_id = uuid.uuid4()
|
||||
self.token = uuid.uuid4()
|
||||
self.executors = 5
|
||||
self.headers = {
|
||||
'X-Project-ID': self.project_id,
|
||||
'X-Auth-Token': self.token
|
||||
}
|
||||
self.bf_client = client.BlueFloodMetricsClient(
|
||||
project_id=self.project_id,
|
||||
token=self.token,
|
||||
executors=self.executors
|
||||
)
|
||||
|
||||
def test_client_init(self):
|
||||
|
||||
self.assertEqual(self.project_id, self.bf_client.project_id)
|
||||
self.assertEqual(self.token, self.bf_client.token)
|
||||
self.assertEqual(
|
||||
sorted(self.headers.items()),
|
||||
sorted(self.bf_client.headers.items()))
|
||||
|
||||
def test_client_async_results(self):
|
||||
urls = ["http://blueflood.com/{0}/views/{1}".format(
|
||||
self.project_id, i) for i in range(10)]
|
||||
with requests_mock.mock() as req_mock:
|
||||
for url in urls:
|
||||
req_mock.get(url, text='Success')
|
||||
results = self.bf_client.async_requests(urls)
|
||||
re_ordered_urls = []
|
||||
for result in results:
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertEqual(result.text, 'Success')
|
||||
re_ordered_urls.append(result.url)
|
||||
|
||||
self.assertEqual(sorted(urls), sorted(re_ordered_urls))
|
|
@ -15,16 +15,36 @@
|
|||
|
||||
"""Unittests for BlueFlood metrics service_controller."""
|
||||
|
||||
import datetime
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_context import context as context_utils
|
||||
|
||||
from poppy.metrics.blueflood import driver
|
||||
from poppy.metrics.blueflood.utils import client
|
||||
from poppy.metrics.blueflood.utils import errors
|
||||
from tests.unit import base
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis import strategies
|
||||
|
||||
class Response(object):
|
||||
|
||||
def __init__(self, ok, url, text, json_dict):
|
||||
self.ok = ok
|
||||
self.url = url
|
||||
self.text = text
|
||||
self.json_dict = json_dict
|
||||
|
||||
def json(self):
|
||||
return self.json_dict
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestBlueFloodServiceController(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -34,21 +54,87 @@ class TestBlueFloodServiceController(base.TestCase):
|
|||
self.metrics_driver = (
|
||||
driver.BlueFloodMetricsDriver(self.conf))
|
||||
|
||||
@given(strategies.text(), strategies.integers(),
|
||||
strategies.integers(), strategies.integers())
|
||||
def test_read(self, metric_name, from_timestamp, to_timestamp, resolution):
|
||||
self.metrics_driver.services_controller.__class__.read = \
|
||||
mock.Mock(return_value='success')
|
||||
@ddt.data('requestCount', 'bandwidthOut', 'httpResponseCode_1XX',
|
||||
'httpResponseCode_2XX', 'httpResponseCode_3XX',
|
||||
'httpResponseCode_4XX', 'httpResponseCode_5XX')
|
||||
def test_read(self, metric_name):
|
||||
project_id = str(uuid.uuid4())
|
||||
auth_token = str(uuid.uuid4())
|
||||
domain_name = 'www.' + str(uuid.uuid4()) + '.com'
|
||||
to_timestamp = datetime.datetime.utcnow()
|
||||
from_timestamp = \
|
||||
(datetime.datetime.utcnow() - datetime.timedelta(days=1))
|
||||
context_utils.get_current = mock.Mock()
|
||||
context_utils.get_current().to_dict = \
|
||||
mock.Mock(return_value={'tenant': project_id,
|
||||
'auth_token': auth_token})
|
||||
with mock.patch.object(client.BlueFloodMetricsClient,
|
||||
'async_requests',
|
||||
auto_spec=True) as mock_async:
|
||||
timestamp1 = str(int(time.time()))
|
||||
timestamp2 = str(int(time.time()) + 100)
|
||||
timestamp3 = str(int(time.time()) + 200)
|
||||
json_dict = {
|
||||
'values': [
|
||||
{
|
||||
'timestamp': timestamp1,
|
||||
'sum': 45
|
||||
},
|
||||
{
|
||||
'timestamp': timestamp2,
|
||||
'sum': 34
|
||||
},
|
||||
{
|
||||
'timestamp': timestamp3,
|
||||
'sum': 11
|
||||
},
|
||||
]
|
||||
}
|
||||
metric_names = []
|
||||
regions = ['Mock_region{0}'.format(i) for i in range(6)]
|
||||
for region in regions:
|
||||
metric_names.append('_'.join([metric_name, domain_name,
|
||||
region]))
|
||||
mock_async_responses = []
|
||||
for metric_name in metric_names:
|
||||
url = 'https://www.metrics.com/{0}/{1}'.format(
|
||||
project_id, metric_name)
|
||||
res = Response(ok=True,
|
||||
url=url,
|
||||
text='success',
|
||||
json_dict=json_dict)
|
||||
mock_async_responses.append(res)
|
||||
|
||||
self.metrics_driver.services_controller.read(
|
||||
metric_name=metric_name,
|
||||
from_timestamp=from_timestamp,
|
||||
to_timestamp=to_timestamp,
|
||||
resolution=resolution
|
||||
)
|
||||
self.metrics_driver.services_controller.read.assert_called_once_with(
|
||||
metric_name=metric_name,
|
||||
from_timestamp=from_timestamp,
|
||||
to_timestamp=to_timestamp,
|
||||
resolution=resolution
|
||||
)
|
||||
# NOTE(TheSriram): shuffle the order of responses
|
||||
random.shuffle(mock_async_responses)
|
||||
mock_async.return_value = mock_async_responses
|
||||
|
||||
results = self.metrics_driver.services_controller.read(
|
||||
metric_names=metric_names,
|
||||
from_timestamp=from_timestamp,
|
||||
to_timestamp=to_timestamp,
|
||||
resolution='86400'
|
||||
)
|
||||
|
||||
for result in results:
|
||||
metric_name, response = result
|
||||
self.assertIn(metric_name, metric_names)
|
||||
metric_names.remove(metric_name)
|
||||
self.assertEqual(response[timestamp1], 45)
|
||||
self.assertEqual(response[timestamp2], 34)
|
||||
self.assertEqual(response[timestamp3], 11)
|
||||
|
||||
def test_format_results_exception(self):
|
||||
json_dict = {
|
||||
'this is a error': [
|
||||
{
|
||||
'errorcode': 400,
|
||||
}
|
||||
]
|
||||
}
|
||||
resp = Response(ok=True,
|
||||
url='https://www.metrics.com',
|
||||
text='success',
|
||||
json_dict=json_dict)
|
||||
formatter = self.metrics_driver.services_controller._result_formatter
|
||||
self.assertRaises(errors.BlueFloodApiSchemaError, formatter, resp)
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
# 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.
|
||||
|
||||
"""Unittests for BlueFlood utils"""
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
from poppy.metrics.blueflood.utils import helper
|
||||
|
||||
from tests.unit import base
|
||||
|
||||
|
||||
class TestBlueFloodUtils(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBlueFloodUtils, self).setUp()
|
||||
self.url = 'https://www.metrics.com'
|
||||
|
||||
def _almostequal(self, entity1, entity2, delta=1):
|
||||
if abs(entity1-entity2) <= delta:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def test_helper_set_qs_on_url(self):
|
||||
|
||||
params = {
|
||||
'metricType': 'requestCount',
|
||||
'domain': 'poppy.org'
|
||||
}
|
||||
|
||||
url_with_qs_set = helper.set_qs_on_url(self.url, **params)
|
||||
|
||||
self.assertIn('metricType=requestCount', url_with_qs_set)
|
||||
self.assertIn('domain=poppy.org', url_with_qs_set)
|
||||
|
||||
def test_helper_join_url(self):
|
||||
relative_url = 'requestCount'
|
||||
expected_url = self.url + '/' + relative_url
|
||||
self.assertEqual(helper.join_url(self.url, relative_url),
|
||||
expected_url)
|
||||
|
||||
def test_retrieve_last_relative_url(self):
|
||||
relative_url = 'requestCount'
|
||||
non_relative_url = self.url + '/' + relative_url
|
||||
self.assertEqual(helper.retrieve_last_relative_url(non_relative_url),
|
||||
relative_url)
|
||||
|
||||
def test_datetime_to_epoch(self):
|
||||
datetime_obj = datetime.datetime.today()
|
||||
expected = helper.datetime_to_epoch(datetime_obj=datetime_obj)
|
||||
observed = int(time.time()) * 1000
|
||||
equality = self._almostequal(expected, observed)
|
||||
self.assertEqual(equality, True)
|
||||
|
||||
def test_resolution_converter_seconds_to_enum_happy(self):
|
||||
seconds_series = ['0', '300', '1200', '3600', '14400', '86400']
|
||||
for seconds in seconds_series:
|
||||
helper.resolution_converter_seconds_to_enum(seconds)
|
||||
|
||||
def test_resolution_converter_seconds_to_enum_exception(self):
|
||||
self.assertRaises(ValueError,
|
||||
helper.resolution_converter_seconds_to_enum,
|
||||
'12345')
|
|
@ -111,7 +111,8 @@ class MockStorageController(mock.Mock):
|
|||
}
|
||||
)
|
||||
|
||||
def get_service_details_by_domain_name(self, domain_name):
|
||||
def get_service_details_by_domain_name(self, domain_name,
|
||||
project_id=None):
|
||||
r = service.Service(
|
||||
str(uuid.uuid4()),
|
||||
str(uuid.uuid4()),
|
||||
|
|
|
@ -97,7 +97,13 @@ AKAMAI_OPTIONS = [
|
|||
help='Operator groupID'),
|
||||
cfg.StrOpt(
|
||||
'property_id',
|
||||
help='Operator propertyID')
|
||||
help='Operator propertyID'),
|
||||
|
||||
|
||||
# Metrics related configs
|
||||
cfg.IntOpt('metrics_resolution',
|
||||
help='Resolution in seconds for retrieving metrics',
|
||||
default=86400)
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import ddt
|
||||
|
@ -26,6 +27,7 @@ from poppy.model.helpers import origin
|
|||
from poppy.model.helpers import restriction
|
||||
from poppy.model.helpers import rule
|
||||
from poppy.model.service import Service
|
||||
from poppy.provider.akamai import geo_zone_code_mapping
|
||||
from poppy.provider.akamai import services
|
||||
from poppy.transport.pecan.models.request import service
|
||||
from poppy.transport.pecan.models.request import ssl_certificate
|
||||
|
@ -48,6 +50,8 @@ class TestServices(base.TestCase):
|
|||
self.driver.akamai_https_access_url_suffix = str(uuid.uuid1())
|
||||
self.san_cert_cnames = [str(x) for x in range(7)]
|
||||
self.driver.san_cert_cnames = self.san_cert_cnames
|
||||
self.driver.regions = geo_zone_code_mapping.REGIONS
|
||||
self.driver.metrics_resolution = 86400
|
||||
self.controller = services.ServiceController(self.driver)
|
||||
service_id = str(uuid.uuid4())
|
||||
domains_old = domain.Domain(domain='cdn.poppy.org')
|
||||
|
@ -611,3 +615,104 @@ class TestServices(base.TestCase):
|
|||
controller.sps_api_base_url.format(spsId=lastSpsId))
|
||||
self.assertFalse(controller.sps_api_client.post.called)
|
||||
return
|
||||
|
||||
def test_regions(self):
|
||||
controller = services.ServiceController(self.driver)
|
||||
self.assertEqual(controller.driver.regions,
|
||||
geo_zone_code_mapping.REGIONS)
|
||||
|
||||
@ddt.data('requestCount', 'bandwidthOut', 'httpResponseCode_1XX',
|
||||
'httpResponseCode_2XX', 'httpResponseCode_3XX',
|
||||
'httpResponseCode_4XX', 'httpResponseCode_5XX')
|
||||
def test_get_metrics_by_domain_metrics_controller(self, metrictype):
|
||||
controller = services.ServiceController(self.driver)
|
||||
project_id = str(uuid.uuid4())
|
||||
domain_name = 'www.' + str(uuid.uuid4()) + '.com'
|
||||
regions = controller.driver.regions
|
||||
end_time = datetime.datetime.utcnow()
|
||||
start_time = (datetime.datetime.utcnow() - datetime.timedelta(days=1))
|
||||
startTime = start_time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
endTime = end_time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
metrics_controller = mock.Mock()
|
||||
# NOTE(TheSriram): We mock a empty return value, to just test
|
||||
# what the call args were for the metrics_controller
|
||||
metrics_controller.read = mock.Mock(return_value=[])
|
||||
|
||||
extras = {
|
||||
'metricType': metrictype,
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
'metrics_controller': metrics_controller
|
||||
}
|
||||
|
||||
controller.get_metrics_by_domain(project_id, domain_name,
|
||||
regions, **extras)
|
||||
|
||||
call_args = metrics_controller.read.call_args[1]
|
||||
self.assertEqual(call_args['resolution'],
|
||||
self.driver.metrics_resolution)
|
||||
self.assertEqual(call_args['to_timestamp'], endTime)
|
||||
self.assertEqual(call_args['from_timestamp'], startTime)
|
||||
metric_names = call_args['metric_names']
|
||||
for metric_name in metric_names:
|
||||
metric_split = metric_name.split('_')
|
||||
if len(metric_split) == 3:
|
||||
self.assertEqual(metric_split[0], metrictype)
|
||||
self.assertEqual(metric_split[1], domain_name)
|
||||
self.assertIn(metric_split[2], regions)
|
||||
else:
|
||||
self.assertEqual(metric_split[0], 'requestCount')
|
||||
self.assertEqual(metric_split[1], domain_name)
|
||||
self.assertIn(metric_split[2], regions)
|
||||
self.assertIn(metric_split[3], metrictype.split('_')[1])
|
||||
|
||||
@ddt.data('requestCount', 'bandwidthOut', 'httpResponseCode_1XX',
|
||||
'httpResponseCode_2XX', 'httpResponseCode_3XX',
|
||||
'httpResponseCode_4XX', 'httpResponseCode_5XX')
|
||||
def test_get_metrics_by_domain_metrics_controller_return(self, metrictype):
|
||||
controller = services.ServiceController(self.driver)
|
||||
project_id = str(uuid.uuid4())
|
||||
domain_name = 'www.' + str(uuid.uuid4()) + '.com'
|
||||
regions = controller.driver.regions
|
||||
end_time = datetime.datetime.utcnow()
|
||||
start_time = (datetime.datetime.utcnow() - datetime.timedelta(days=1))
|
||||
startTime = start_time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
endTime = end_time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
metrics_controller = mock.Mock()
|
||||
metric_buckets = []
|
||||
|
||||
if 'httpResponseCode' in metrictype:
|
||||
http_series = metrictype.split('_')[1]
|
||||
for region in regions:
|
||||
metric_buckets.append('_'.join(['requestCount', domain_name,
|
||||
region,
|
||||
http_series]))
|
||||
else:
|
||||
for region in regions:
|
||||
metric_buckets.append('_'.join([metrictype, domain_name,
|
||||
region]))
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
value = 55
|
||||
metrics_response = [(metric_bucket, {timestamp: value})
|
||||
for metric_bucket in metric_buckets]
|
||||
metrics_controller.read = mock.Mock(return_value=metrics_response)
|
||||
extras = {
|
||||
'metricType': metrictype,
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
'metrics_controller': metrics_controller
|
||||
}
|
||||
|
||||
formatted_results = controller.get_metrics_by_domain(project_id,
|
||||
domain_name,
|
||||
regions,
|
||||
**extras)
|
||||
|
||||
self.assertEqual(formatted_results['domain'], domain_name)
|
||||
self.assertEqual(sorted(formatted_results[metrictype].keys()),
|
||||
sorted(regions))
|
||||
for timestamp_counter in formatted_results[metrictype].values():
|
||||
self.assertEqual(timestamp_counter[0][timestamp], value)
|
||||
|
|
|
@ -36,6 +36,7 @@ class TestServices(base.TestCase):
|
|||
self.provider_service_id = uuid.uuid1()
|
||||
self.mock_get_client = mock_get_client
|
||||
self.driver = MockDriver()
|
||||
self.driver.regions = []
|
||||
self.controller = services.ServiceController(self.driver)
|
||||
|
||||
def test_get(self):
|
||||
|
@ -131,3 +132,6 @@ class TestServices(base.TestCase):
|
|||
# TODO(tonytan4ever/obulpathi): fill in once correct
|
||||
# current_customer logic is done
|
||||
self.assertTrue(self.controller.current_customer is None)
|
||||
|
||||
def test_regions(self):
|
||||
self.assertEqual(self.controller.driver.regions, [])
|
||||
|
|
|
@ -38,6 +38,7 @@ class TestServices(base.TestCase):
|
|||
super(TestServices, self).setUp()
|
||||
self.driver = mock_driver()
|
||||
self.driver.provider_name = 'Fastly'
|
||||
self.driver.regions = []
|
||||
self.mock_service = mock_service
|
||||
self.mock_version = mock_version
|
||||
|
||||
|
@ -445,3 +446,9 @@ class TestProviderValidation(base.TestCase):
|
|||
resp = self.controller.get('magic-service')
|
||||
|
||||
self.assertIn('error', resp[self.driver.provider_name])
|
||||
|
||||
def test_regions(self):
|
||||
driver = mock.Mock()
|
||||
driver.regions = []
|
||||
controller = services.ServiceController(driver)
|
||||
self.assertEqual(controller.driver.regions, [])
|
||||
|
|
|
@ -302,3 +302,12 @@ class TestServices(base.TestCase):
|
|||
controller.current_customer
|
||||
except RuntimeError as e:
|
||||
self.assertTrue(str(e) == "Get maxcdn current customer failed...")
|
||||
|
||||
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider.client')
|
||||
@mock.patch('poppy.provider.maxcdn.driver.CDNProvider')
|
||||
def test_regions(self, mock_controllerclient, mock_driver):
|
||||
driver = mock_driver()
|
||||
driver.regions = []
|
||||
driver.attach_mock(mock_controllerclient, 'client')
|
||||
controller = services.ServiceController(driver)
|
||||
self.assertEqual(controller.driver.regions, [])
|
||||
|
|
|
@ -31,6 +31,7 @@ class MockProviderServicesTest(base.TestCase):
|
|||
def setUp(self, mock_driver):
|
||||
super(MockProviderServicesTest, self).setUp()
|
||||
self.driver = mock_driver()
|
||||
self.driver.regions = []
|
||||
self.test_provider_service_id = uuid.uuid1()
|
||||
self.sc = services.ServiceController(self.driver)
|
||||
|
||||
|
@ -66,3 +67,6 @@ class MockProviderServicesTest(base.TestCase):
|
|||
|
||||
def test_current_customer(self):
|
||||
self.assertTrue(self.sc.current_customer is None)
|
||||
|
||||
def test_regions(self):
|
||||
self.assertEqual(self.sc._driver.regions, [])
|
||||
|
|
Loading…
Reference in New Issue