Add amphora statistics to the admin API

This patch adds an admin API for getting per-amphora statistics.

Change-Id: Ib57b2136dbb41067d6b8949ee42f946f109616e7
This commit is contained in:
Michael Johnson 2018-07-23 13:54:24 -07:00
parent 9bb4c5c1c4
commit 66298f9a48
15 changed files with 337 additions and 4 deletions

View File

@ -143,6 +143,13 @@ amphora-role:
in: body
required: true
type: string
amphora-stats:
description: |
A list of amphora statistics objects, one per listener.
in: body
min_version: 2.3
required: true
type: array
amphora-status:
description: |
The status of the amphora. One of: ``BOOTING``, ``ALLOCATED``, ``READY``,

View File

@ -79,7 +79,7 @@ Response Example
:language: javascript
Show Amphora details
===========================
====================
.. rest_method:: GET /v2/octavia/amphorae/{amphora_id}
@ -153,6 +153,68 @@ Response Example
.. literalinclude:: examples/amphora-show-response.json
:language: javascript
Show Amphora Statistics
=======================
.. rest_method:: GET /v2/octavia/amphorae/{amphora_id}/stats
Show the statistics for an amphora.
If you are not an administrative user, the service returns the HTTP
``Forbidden (403)`` response code.
Use the ``fields`` query parameter to control which fields are
returned in the response body.
**New in version 2.3**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 404
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- amphora_id: path-amphora-id
- fields: fields
Curl Example
------------
.. literalinclude:: examples/amphora-show-stats-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- active_connections: active_connections
- amphora_stats: amphora-stats
- bytes_in: bytes_in
- bytes_out: bytes_out
- id: amphora-id
- listener_id: listener-id
- loadbalancer_id: loadbalancer-id
- request_errors: request_errors
- total_connections: total_connections
Response Example
----------------
.. literalinclude:: examples/amphora-show-stats-response.json
:language: javascript
Failover Amphora
================

View File

@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2/octavia/amphorae/63d8349e-c4d7-4156-bc94-29260607b04f/stats

View File

@ -0,0 +1,24 @@
{
"amphora_stats": [
{
"active_connections": 48629,
"bytes_in": 65671420,
"bytes_out": 774771186,
"id": "63d8349e-c4d7-4156-bc94-29260607b04f",
"listener_id": "bbe44114-cda2-4fe0-b192-d9e24ce661db",
"loadbalancer_id": "65b5a7c3-1437-4909-84cf-cec9f7e371ea",
"request_errors": 0,
"total_connections": 26189172
},
{
"active_connections": 0,
"bytes_in": 5,
"bytes_out": 100,
"id": "63d8349e-c4d7-4156-bc94-29260607b04f",
"listener_id": "af45a658-4eeb-4ce9-8b7e-16b0e5676f87",
"loadbalancer_id": "65b5a7c3-1437-4909-84cf-cec9f7e371ea",
"request_errors": 0,
"total_connections": 1
}
]
}

View File

@ -77,6 +77,8 @@ class RootController(rest.RestController):
'2018-04-20T00:00:00Z', host_url)
self._add_a_version(versions, 'v2.2', 'v2', 'SUPPORTED',
'2018-07-31T00:00:00Z', host_url)
self._add_a_version(versions, 'v2.3', 'v2', 'CURRENT',
self._add_a_version(versions, 'v2.3', 'v2', 'SUPPORTED',
'2018-12-18T00:00:00Z', host_url)
self._add_a_version(versions, 'v2.4', 'v2', 'CURRENT',
'2018-12-19T00:00:00Z', host_url)
return {'versions': versions}

View File

@ -24,6 +24,7 @@ from wsmeext import pecan as wsme_pecan
from octavia.api.v2.controllers import base
from octavia.api.v2.types import amphora as amp_types
from octavia.common import constants
from octavia.common import exceptions
CONF = cfg.CONF
@ -84,6 +85,8 @@ class AmphoraController(base.BaseController):
remainder = remainder[1:]
if controller == 'failover':
return FailoverController(amp_id=amphora_id), remainder
if controller == 'stats':
return AmphoraStatsController(amp_id=amphora_id), remainder
return None
@ -131,3 +134,30 @@ class FailoverController(base.BaseController):
self.repositories.load_balancer.update(
context.session, db_amp.load_balancer.id,
provisioning_status=constants.ERROR)
class AmphoraStatsController(base.BaseController):
RBAC_TYPE = constants.RBAC_AMPHORA
def __init__(self, amp_id):
super(AmphoraStatsController, self).__init__()
self.amp_id = amp_id
@wsme_pecan.wsexpose(amp_types.StatisticsRootResponse, wtypes.text,
status_code=200)
def get(self):
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_STATS)
stats = self.repositories.get_amphora_stats(context.session,
self.amp_id)
if stats == []:
raise exceptions.NotFound(resource='Amphora stats for',
id=self.amp_id)
wsme_stats = []
for stat in stats:
wsme_stats.append(amp_types.AmphoraStatisticsResponse(**stat))
return amp_types.StatisticsRootResponse(amphora_stats=wsme_stats)

View File

@ -60,3 +60,19 @@ class AmphoraRootResponse(types.BaseType):
class AmphoraeRootResponse(types.BaseType):
amphorae = wtypes.wsattr([AmphoraResponse])
amphorae_links = wtypes.wsattr([types.PageType])
class AmphoraStatisticsResponse(BaseAmphoraType):
"""Defines which attributes are to show on stats response."""
active_connections = wtypes.wsattr(wtypes.IntegerType())
bytes_in = wtypes.wsattr(wtypes.IntegerType())
bytes_out = wtypes.wsattr(wtypes.IntegerType())
id = wtypes.wsattr(wtypes.UuidType())
listener_id = wtypes.wsattr(wtypes.UuidType())
loadbalancer_id = wtypes.wsattr(wtypes.UuidType())
request_errors = wtypes.wsattr(wtypes.IntegerType())
total_connections = wtypes.wsattr(wtypes.IntegerType())
class StatisticsRootResponse(types.BaseType):
amphora_stats = wtypes.wsattr([AmphoraStatisticsResponse])

View File

@ -258,6 +258,11 @@ REQ_CONN_TIMEOUT = 'req_conn_timeout'
REQ_READ_TIMEOUT = 'req_read_timeout'
CONN_MAX_RETRIES = 'conn_max_retries'
CONN_RETRY_INTERVAL = 'conn_retry_interval'
ACTIVE_CONNECTIONS = 'active_connections'
BYTES_IN = 'bytes_in'
BYTES_OUT = 'bytes_out'
REQUEST_ERRORS = 'request_errors'
TOTAL_CONNECTIONS = 'total_connections'
CERT_ROTATE_AMPHORA_FLOW = 'octavia-cert-rotate-amphora-flow'
CREATE_AMPHORA_FLOW = 'octavia-create-amphora-flow'

View File

@ -677,6 +677,34 @@ class Repositories(object):
session.expire_all()
return self.load_balancer.get(session, id=lb_dm.id)
def get_amphora_stats(self, session, amp_id):
"""Gets the statistics for all listeners on an amphora.
:param session: A Sql Alchemy database session.
:param amp_id: The amphora ID to query.
:returns: An amphora stats dictionary
"""
with session.begin(subtransactions=True):
columns = (models.ListenerStatistics.__table__.columns +
[models.Amphora.load_balancer_id])
amp_records = (
session.query(*columns)
.filter(models.ListenerStatistics.amphora_id == amp_id)
.filter(models.ListenerStatistics.amphora_id ==
models.Amphora.id).all())
amp_stats = []
for amp in amp_records:
amp_stat = {consts.LOADBALANCER_ID: amp.load_balancer_id,
consts.LISTENER_ID: amp.listener_id,
'id': amp.amphora_id,
consts.ACTIVE_CONNECTIONS: amp.active_connections,
consts.BYTES_IN: amp.bytes_in,
consts.BYTES_OUT: amp.bytes_out,
consts.REQUEST_ERRORS: amp.request_errors,
consts.TOTAL_CONNECTIONS: amp.total_connections}
amp_stats.append(amp_stat)
return amp_stats
class LoadBalancerRepository(BaseRepository):
model_class = models.LoadBalancer

View File

@ -38,6 +38,13 @@ rules = [
[{'method': 'PUT',
'path': '/v2/octavia/amphorae/{amphora_id}/failover'}]
),
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_AMPHORA,
action=constants.RBAC_GET_STATS),
constants.RULE_API_ADMIN,
"Show Amphora statistics",
[{'method': 'GET', 'path': '/v2/octavia/amphorae/{amphora_id}/stats'}]
),
]

View File

@ -46,12 +46,13 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
versions = self._get_versions_with_config(
api_v1_enabled=True, api_v2_enabled=True)
version_ids = tuple(v.get('id') for v in versions)
self.assertEqual(5, len(version_ids))
self.assertEqual(6, len(version_ids))
self.assertIn('v1', version_ids)
self.assertIn('v2.0', version_ids)
self.assertIn('v2.1', version_ids)
self.assertIn('v2.2', version_ids)
self.assertIn('v2.3', version_ids)
self.assertIn('v2.4', version_ids)
# Each version should have a 'self' 'href' to the API version URL
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
@ -71,11 +72,12 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
def test_api_v1_disabled(self):
versions = self._get_versions_with_config(
api_v1_enabled=False, api_v2_enabled=True)
self.assertEqual(4, len(versions))
self.assertEqual(5, len(versions))
self.assertEqual('v2.0', versions[0].get('id'))
self.assertEqual('v2.1', versions[1].get('id'))
self.assertEqual('v2.2', versions[2].get('id'))
self.assertEqual('v2.3', versions[3].get('id'))
self.assertEqual('v2.4', versions[4].get('id'))
def test_api_v2_disabled(self):
versions = self._get_versions_with_config(

View File

@ -68,6 +68,7 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
AMPHORAE_PATH = '/octavia/amphorae'
AMPHORA_PATH = AMPHORAE_PATH + '/{amphora_id}'
AMPHORA_FAILOVER_PATH = AMPHORA_PATH + '/failover'
AMPHORA_STATS_PATH = AMPHORA_PATH + '/stats'
PROVIDERS_PATH = '/lbaas/providers'

View File

@ -29,6 +29,7 @@ class TestAmphora(base.BaseAPITest):
root_tag = 'amphora'
root_tag_list = 'amphorae'
root_tag_links = 'amphorae_links'
root_tag_stats = 'amphora_stats'
def setUp(self):
super(TestAmphora, self).setUp()
@ -61,6 +62,34 @@ class TestAmphora(base.BaseAPITest):
self.amp = self.amphora_repo.create(self.session, **self.amp_args)
self.amp_id = self.amp.id
self.amp_args['id'] = self.amp_id
self.listener1_id = uuidutils.generate_uuid()
self.create_listener_stats_dynamic(self.listener1_id, self.amp_id,
bytes_in=1, bytes_out=2,
active_connections=3,
total_connections=4,
request_errors=5)
self.listener2_id = uuidutils.generate_uuid()
self.create_listener_stats_dynamic(self.listener2_id, self.amp_id,
bytes_in=6, bytes_out=7,
active_connections=8,
total_connections=9,
request_errors=10)
self.listener1_amp_stats = {'active_connections': 3,
'bytes_in': 1, 'bytes_out': 2,
'id': self.amp_id,
'listener_id': self.listener1_id,
'loadbalancer_id': self.lb_id,
'request_errors': 5,
'total_connections': 4}
self.listener2_amp_stats = {'active_connections': 8,
'bytes_in': 6, 'bytes_out': 7,
'id': self.amp_id,
'listener_id': self.listener2_id,
'loadbalancer_id': self.lb_id,
'request_errors': 10,
'total_connections': 9}
self.ref_amp_stats = [self.listener1_amp_stats,
self.listener2_amp_stats]
def _create_additional_amp(self):
amp_args = {
@ -398,3 +427,77 @@ class TestAmphora(base.BaseAPITest):
response = self.get(self.AMPHORAE_PATH).json.get(self.root_tag_list)
self.assertIsInstance(response, list)
self.assertEqual(0, len(response))
def test_get_stats_authorized(self):
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
with mock.patch.object(octavia.common.context.Context, 'project_id',
self.project_id):
override_credentials = {
'service_user_id': None,
'user_domain_id': None,
'is_admin_project': True,
'service_project_domain_id': None,
'service_project_id': None,
'roles': ['load-balancer_member'],
'user_id': None,
'is_admin': True,
'service_user_domain_id': None,
'project_domain_id': None,
'service_roles': [],
'project_id': self.project_id}
with mock.patch(
"oslo_context.context.RequestContext.to_policy_values",
return_value=override_credentials):
response = self.get(self.AMPHORA_STATS_PATH.format(
amphora_id=self.amp_id)).json.get(self.root_tag_stats)
# Reset api auth setting
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.ref_amp_stats, response)
def test_get_stats_not_authorized(self):
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
with mock.patch.object(octavia.common.context.Context, 'project_id',
uuidutils.generate_uuid()):
response = self.get(self.AMPHORA_STATS_PATH.format(
amphora_id=self.amp_id), status=403)
# Reset api auth setting
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, response.json)
def test_get_stats_bad_amp_id(self):
self.get(self.AMPHORA_STATS_PATH.format(
amphora_id='bogus_id'), status=404)
def test_get_stats_no_listeners(self):
self.lb2 = self.create_load_balancer(
uuidutils.generate_uuid()).get('loadbalancer')
self.lb2_id = self.lb2.get('id')
self.set_lb_status(self.lb2_id)
self.amp2_args = {
'load_balancer_id': self.lb2_id,
'compute_id': uuidutils.generate_uuid(),
'lb_network_ip': '192.168.1.20',
'vrrp_ip': '192.168.1.5',
'ha_ip': '192.168.1.100',
'vrrp_port_id': uuidutils.generate_uuid(),
'ha_port_id': uuidutils.generate_uuid(),
'cert_expiration': datetime.datetime.now(),
'cert_busy': False,
'role': constants.ROLE_STANDALONE,
'status': constants.AMPHORA_ALLOCATED,
'vrrp_interface': 'eth1',
'vrrp_id': 1,
'vrrp_priority': 100,
'cached_zone': None,
'created_at': datetime.datetime.now(),
'updated_at': datetime.datetime.now(),
'image_id': uuidutils.generate_uuid(),
}
self.amp2 = self.amphora_repo.create(self.session, **self.amp2_args)
self.amp2_id = self.amp2.id
self.get(self.AMPHORA_STATS_PATH.format(
amphora_id=self.amp2_id), status=404)

View File

@ -82,6 +82,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
FAKE_UUID_1 = uuidutils.generate_uuid()
FAKE_UUID_2 = uuidutils.generate_uuid()
FAKE_UUID_3 = uuidutils.generate_uuid()
FAKE_IP = '192.0.2.44'
def setUp(self):
super(AllRepositoriesTest, self).setUp()
@ -96,6 +98,11 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
enabled=True, provisioning_status=constants.ACTIVE,
operating_status=constants.ONLINE,
load_balancer_id=self.load_balancer.id)
self.amphora = self.repos.amphora.create(
self.session, id=uuidutils.generate_uuid(),
load_balancer_id=self.load_balancer.id,
compute_id=self.FAKE_UUID_3, status=constants.ACTIVE,
vrrp_ip=self.FAKE_IP, lb_network_ip=self.FAKE_IP)
def test_all_repos_has_correct_repos(self):
repo_attr_names = ('load_balancer', 'vip', 'health_monitor',
@ -1837,6 +1844,40 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
self.session, project_id=project_id).in_use_member)
conf.config(group='api_settings', auth_strategy=constants.TESTING)
def test_get_amphora_stats(self):
listener2_id = uuidutils.generate_uuid()
self.repos.listener_stats.create(
self.session, listener_id=self.listener.id,
amphora_id=self.amphora.id, bytes_in=1, bytes_out=2,
active_connections=3, total_connections=4, request_errors=5)
self.repos.listener_stats.create(
self.session, listener_id=listener2_id,
amphora_id=self.amphora.id, bytes_in=6, bytes_out=7,
active_connections=8, total_connections=9, request_errors=10)
amp_stats = self.repos.get_amphora_stats(self.session, self.amphora.id)
self.assertEqual(2, len(amp_stats))
for stats in amp_stats:
if stats['listener_id'] == self.listener.id:
self.assertEqual(self.load_balancer.id,
stats['loadbalancer_id'])
self.assertEqual(self.listener.id, stats['listener_id'])
self.assertEqual(self.amphora.id, stats['id'])
self.assertEqual(1, stats['bytes_in'])
self.assertEqual(2, stats['bytes_out'])
self.assertEqual(3, stats['active_connections'])
self.assertEqual(4, stats['total_connections'])
self.assertEqual(5, stats['request_errors'])
else:
self.assertEqual(self.load_balancer.id,
stats['loadbalancer_id'])
self.assertEqual(listener2_id, stats['listener_id'])
self.assertEqual(self.amphora.id, stats['id'])
self.assertEqual(6, stats['bytes_in'])
self.assertEqual(7, stats['bytes_out'])
self.assertEqual(8, stats['active_connections'])
self.assertEqual(9, stats['total_connections'])
self.assertEqual(10, stats['request_errors'])
class PoolRepositoryTest(BaseRepositoryTest):

View File

@ -0,0 +1,4 @@
---
features:
- |
Adds an administrator API to access per-amphora statistics.