From d0b4c2268519527b5bb639a4c520725add1e2c91 Mon Sep 17 00:00:00 2001 From: Manish Tomar Date: Fri, 20 Jan 2017 20:46:11 -0800 Subject: [PATCH] Revert "Common function to read feed entries" --- config.example.json | 7 +- otter/cloud_client/__init__.py | 4 +- otter/cloud_client/clb.py | 55 +++++-- otter/cloud_client/cloudfeeds.py | 66 +------- otter/constants.py | 31 +--- otter/test/cloud_client/test_clb.py | 108 +++++++++--- otter/test/cloud_client/test_cloudfeeds.py | 183 +-------------------- otter/test/cloud_client/test_init.py | 2 + otter/test/tap/test_api.py | 3 +- otter/test/test_constants.py | 34 ++-- 10 files changed, 167 insertions(+), 326 deletions(-) diff --git a/config.example.json b/config.example.json index 8865b2d1d..79217f2d8 100644 --- a/config.example.json +++ b/config.example.json @@ -64,12 +64,7 @@ "cloudfeeds": { "service": "cloudFeeds", "tenant_id": "identity_admin_tenant", - "url": "https://cfurl.example.net/not/in/service/catalog" - }, - "terminator": { - "interval": 300, - "tenant_id": "identity_admin_tenant", - "cf_cap_url": "https://cfurl.example.net/not/in/service/catalog" + "url": "http://cfurl.net/not/in/service/catalog" }, "converger": { "build_timeout": 3600, diff --git a/otter/cloud_client/__init__.py b/otter/cloud_client/__init__.py index 36391ce92..ff19b4120 100644 --- a/otter/cloud_client/__init__.py +++ b/otter/cloud_client/__init__.py @@ -182,6 +182,8 @@ def concretize_service_request( log = service_request.log service_config = service_configs[service_request.service_type] + region = service_config['region'] + service_name = service_config['name'] def got_auth((token, catalog)): request_ = add_headers(otter_headers(token), request) @@ -191,8 +193,6 @@ def got_auth((token, catalog)): if 'url' in service_config: request_ = add_bind_root(service_config['url'], request_) else: - region = service_config['region'] - service_name = service_config['name'] request_ = add_bind_service( catalog, service_name, region, log, request_) request_ = add_error_handling( diff --git a/otter/cloud_client/clb.py b/otter/cloud_client/clb.py index 384500559..461fe0f79 100644 --- a/otter/cloud_client/clb.py +++ b/otter/cloud_client/clb.py @@ -1,18 +1,19 @@ from functools import partial from operator import itemgetter +from urlparse import parse_qs, urlparse import attr from characteristic import Attribute, attributes from effect import catch, raise_ +from effect.do import do, do_return import six from toolz.functoolz import identity from toolz.itertoolz import concat -from otter.cloud_client import cloudfeeds as cf from otter.cloud_client import ( log_success_response, match_errors, @@ -20,6 +21,7 @@ regex, service_request) from otter.constants import ServiceType +from otter.indexer import atom from otter.util.http import APIError, append_segments, try_json_with_keys from otter.util.pure_http import has_code @@ -368,27 +370,56 @@ def get_clbs(): success=lambda (response, body): body['loadBalancers']) -def get_clb_node_feed(lb_id, node_id): +def _node_feed_page(lb_id, node_id, params): """ - Get the atom feed associated with a CLB node. + Return page of CLB node feed :param int lb_id: Cloud Load balancer ID :param int node_id: Node ID of in loadbalancer node + :param dict params: Request query parameters - :returns: Effect of ``list`` of atom entry :class:`Element` - :rtype: ``Effect`` + :returns: Unparsed response body as string + :rtype: `str` """ - return cf.read_entries( - ServiceType.CLOUD_LOAD_BALANCERS, + return service_request( + ServiceType.CLOUD_LOAD_BALANCERS, 'GET', append_segments('loadbalancers', str(lb_id), 'nodes', '{}.atom'.format(node_id)), - {}, - cf.Direction.NEXT, - "request-get-clb-node-feed" - ).on(itemgetter(0)).on( + params=params, + json_response=False + ).on( error=only_json_api_errors( lambda c, b: _process_clb_api_error(c, b, lb_id)) - ) + ).on( + log_success_response('request-get-clb-node-feed', identity) + ).on(itemgetter(1)) + + +@do +def get_clb_node_feed(lb_id, node_id): + """ + Get the atom feed associated with a CLB node. + + :param int lb_id: Cloud Load balancer ID + :param int node_id: Node ID of in loadbalancer node + + :returns: Effect of ``list`` of atom entry :class:`Element` + :rtype: ``Effect`` + """ + all_entries = [] + params = {} + while True: + feed_str = yield _node_feed_page(lb_id, node_id, params) + feed = atom.parse(feed_str) + entries = atom.entries(feed) + if entries == []: + break + all_entries.extend(entries) + next_link = atom.next_link(feed) + if not next_link: + break + params = parse_qs(urlparse(next_link).query) + yield do_return(all_entries) def get_clb_health_monitor(lb_id): diff --git a/otter/cloud_client/cloudfeeds.py b/otter/cloud_client/cloudfeeds.py index 8b5a5e75b..37b512e81 100644 --- a/otter/cloud_client/cloudfeeds.py +++ b/otter/cloud_client/cloudfeeds.py @@ -2,17 +2,8 @@ Cloud feeds related APIs """ -from urlparse import parse_qs, urlparse - -from effect.do import do, do_return - -from toolz.functoolz import identity - -from twisted.python.constants import NamedConstant, Names - -from otter.cloud_client import log_success_response, service_request +from otter.cloud_client import service_request from otter.constants import ServiceType -from otter.indexer import atom from otter.util.http import append_segments from otter.util.pure_http import has_code @@ -31,58 +22,3 @@ def publish_autoscale_event(event, log=None): 'content-type': ['application/vnd.rackspace.atom+json']}, data=event, log=log, success_pred=has_code(201), json_response=False) - - -class Direction(Names): - """ - Which direction to follow the feeds? - """ - PREVIOUS = NamedConstant() - NEXT = NamedConstant() - - -@do -def read_entries(service_type, url, params, direction, follow_limit=100, - log_msg_type=None): - """ - Read all feed entries and follow in given direction until it is empty - - :param service_type: Service hosting the feed - :type service_type: A member of :class:`ServiceType` - :param str url: CF URL to append - :param dict params: HTTP parameters - :param direction: Where to continue fetching? - :type direction: A member of :class:`Direction` - :param int follow_limit: Maximum number of times to follow in given - direction - - :return: (``list`` of :obj:`Element`, last fetched params) tuple - """ - if direction == Direction.PREVIOUS: - direction_link = atom.previous_link - elif direction == Direction.NEXT: - direction_link = atom.next_link - else: - raise ValueError("Invalid direction") - - if log_msg_type is not None: - log_cb = log_success_response(log_msg_type, identity, False) - else: - log_cb = identity - - all_entries = [] - while follow_limit > 0: - resp, feed_str = yield service_request( - service_type, "GET", url, params=params, - json_response=False).on(log_cb) - feed = atom.parse(feed_str) - entries = atom.entries(feed) - if entries == []: - break - all_entries.extend(entries) - link = direction_link(feed) - if link is None: - break - params = parse_qs(urlparse(link).query) - follow_limit -= 1 - yield do_return((all_entries, params)) diff --git a/otter/constants.py b/otter/constants.py index 454346c41..614bc6f41 100644 --- a/otter/constants.py +++ b/otter/constants.py @@ -8,18 +8,12 @@ class ServiceType(Names): - """ - Constants representing Rackspace cloud services. - - Note: CLOUD_FEEDS_CAP represents customer access policy events which like - CLOUD_FEEDS is fixed URL but different from CLOUD_FEEDS. - """ + """Constants representing Rackspace cloud services.""" CLOUD_SERVERS = NamedConstant() CLOUD_LOAD_BALANCERS = NamedConstant() RACKCONNECT_V3 = NamedConstant() CLOUD_METRICS_INGEST = NamedConstant() CLOUD_FEEDS = NamedConstant() - CLOUD_FEEDS_CAP = NamedConstant() CLOUD_ORCHESTRATION = NamedConstant() @@ -34,23 +28,22 @@ def get_service_configs(config): :param dict config: Config from file containing service names that will be there in service catalog """ - region = config['region'] configs = { ServiceType.CLOUD_SERVERS: { 'name': config['cloudServersOpenStack'], - 'region': region, + 'region': config['region'], }, ServiceType.CLOUD_LOAD_BALANCERS: { 'name': config['cloudLoadBalancers'], - 'region': region, + 'region': config['region'], }, ServiceType.CLOUD_ORCHESTRATION: { 'name': config['cloudOrchestration'], - 'region': region, + 'region': config['region'], }, ServiceType.RACKCONNECT_V3: { 'name': config['rackconnect'], - 'region': region, + 'region': config['region'], } } @@ -59,18 +52,10 @@ def get_service_configs(config): configs[ServiceType.CLOUD_METRICS_INGEST] = { 'name': metrics['service'], 'region': metrics['region']} - # {"cloudfeeds": {"url": "https://url"}} config is for service which - # is used to push scaling group events: `otter.log.cloudfeeds`. cf = config.get('cloudfeeds') if cf is not None: - configs[ServiceType.CLOUD_FEEDS] = {'url': cf['url']} - - # "terminator" contains config to setup terminator service and - # cloud feed client to access customer access policy events. This and - # above cloudfeeds service have fixed URL as opposed to service being - # there in service catalog - term = config.get('terminator') - if term is not None: - configs[ServiceType.CLOUD_FEEDS_CAP] = {'url': term['cf_cap_url']} + configs[ServiceType.CLOUD_FEEDS] = { + 'name': cf['service'], 'region': config['region'], + 'url': cf['url']} return configs diff --git a/otter/test/cloud_client/test_clb.py b/otter/test/cloud_client/test_clb.py index 75dd63215..1a058a78f 100644 --- a/otter/test/cloud_client/test_clb.py +++ b/otter/test/cloud_client/test_clb.py @@ -3,8 +3,7 @@ import json from effect import sync_perform -from effect.testing import ( - EQFDispatcher, const, intent_func, noop, perform_sequence) +from effect.testing import EQFDispatcher, const, noop, perform_sequence import six @@ -30,6 +29,7 @@ get_clbs, remove_clb_nodes) from otter.constants import ServiceType +from otter.indexer import atom from otter.test.cloud_client.test_init import log_intent, service_request_eqf from otter.test.utils import ( StubResponse, @@ -431,33 +431,99 @@ def test_get_clb_health_mon_error(self): class GetCLBNodeFeedTests(SynchronousTestCase): - """ - Tests for :func:`get_clb_node_feed` - """ - def test_calls_read_entries(self): + entry = '{}' + feed_fmt = ( + '' + '{entries}') + + def svc_intent(self, params={}): + return service_request( + ServiceType.CLOUD_LOAD_BALANCERS, "GET", + "loadbalancers/12/nodes/13.atom", params=params, + json_response=False).intent + + def feed(self, next_link, summaries): + return self.feed_fmt.format( + next_link=next_link, + entries=''.join(self.entry.format(s) for s in summaries)) + + def test_empty(self): + """ + Does not goto next link when there are no entries and returns [] + """ + feedstr = self.feed("next-doesnt-matter", []) + seq = [ + (self.svc_intent(), const(stub_json_response(feedstr))), + (log_intent("request-get-clb-node-feed", feedstr), const(feedstr)) + ] + self.assertEqual( + perform_sequence(seq, get_clb_node_feed("12", "13")), []) + + def test_single_page(self): """ - Calls `cf.read_entries` with CLB servicetype and atom URL and returns - the feed part of the result + Collects entries and goes to next link if there are entries and returns + if next one is empty """ - from otter.cloud_client.clb import cf - self.patch(cf, "read_entries", intent_func("re")) - eff = get_clb_node_feed("12", "13") + feed1_str = self.feed("https://url?page=2", ["summary1", "summ2"]) + feed2_str = self.feed("next-link", []) seq = [ - (("re", ServiceType.CLOUD_LOAD_BALANCERS, - "loadbalancers/12/nodes/13.atom", {}, cf.Direction.NEXT, - "request-get-clb-node-feed"), - const((["feed1"], {"param": "2"}))) + (self.svc_intent(), const(stub_json_response(feed1_str))), + (log_intent("request-get-clb-node-feed", feed1_str), + const(feed1_str)), + (self.svc_intent({"page": ['2']}), + const(stub_json_response(feed2_str))), + (log_intent("request-get-clb-node-feed", feed2_str), + const(feed2_str)), ] - self.assertEqual(perform_sequence(seq, eff), ["feed1"]) + entries = perform_sequence(seq, get_clb_node_feed("12", "13")) + self.assertEqual( + [atom.summary(entry) for entry in entries], ["summary1", "summ2"]) + + def test_multiple_pages(self): + """ + Collects entries and goes to next link if there are entries and + continues until next link returns empty list + """ + feed1_str = self.feed("https://url?page=2", ["summ1", "summ2"]) + feed2_str = self.feed("https://url?page=3", ["summ3", "summ4"]) + feed3_str = self.feed("next-link", []) + seq = [ + (self.svc_intent(), const(stub_json_response(feed1_str))), + (log_intent("request-get-clb-node-feed", feed1_str), + const(feed1_str)), + (self.svc_intent({"page": ['2']}), + const(stub_json_response(feed2_str))), + (log_intent("request-get-clb-node-feed", feed2_str), + const(feed2_str)), + (self.svc_intent({"page": ['3']}), + const(stub_json_response(feed3_str))), + (log_intent("request-get-clb-node-feed", feed3_str), + const(feed3_str)), + ] + entries = perform_sequence(seq, get_clb_node_feed("12", "13")) + self.assertEqual( + [atom.summary(entry) for entry in entries], + ["summ1", "summ2", "summ3", "summ4"]) + + def test_no_next_link(self): + """ + Returns entries collected till now if there is no next link + """ + feedstr = ( + '' + 'summary') + seq = [ + (self.svc_intent(), const(stub_json_response(feedstr))), + (log_intent("request-get-clb-node-feed", feedstr), + const(feedstr)) + ] + entries = perform_sequence(seq, get_clb_node_feed("12", "13")) + self.assertEqual(atom.summary(entries[0]), "summary") def test_error_handling(self): """ Parses regular CLB errors and raises corresponding exceptions """ - svc_intent = service_request( - ServiceType.CLOUD_LOAD_BALANCERS, "GET", - "loadbalancers/12/nodes/13.atom", params={}, - json_response=False).intent assert_parses_common_clb_errors( - self, svc_intent, get_clb_node_feed("12", "13"), "12") + self, self.svc_intent(), get_clb_node_feed("12", "13"), "12") diff --git a/otter/test/cloud_client/test_cloudfeeds.py b/otter/test/cloud_client/test_cloudfeeds.py index d958c2c15..a42b70c6b 100644 --- a/otter/test/cloud_client/test_cloudfeeds.py +++ b/otter/test/cloud_client/test_cloudfeeds.py @@ -1,19 +1,15 @@ """Tests for otter.cloud_client.cloudfeeds""" -from functools import wraps - from effect import sync_perform -from effect.testing import ( - EQFDispatcher, base_dispatcher, const, noop, perform_sequence) +from effect.testing import EQFDispatcher from twisted.trial.unittest import SynchronousTestCase -from otter.cloud_client import cloudfeeds as cf from otter.cloud_client import service_request +from otter.cloud_client.cloudfeeds import publish_autoscale_event from otter.constants import ServiceType -from otter.indexer import atom -from otter.test.cloud_client.test_init import log_intent, service_request_eqf -from otter.test.utils import stub_json_response, stub_pure_response +from otter.test.cloud_client.test_init import service_request_eqf +from otter.test.utils import stub_pure_response from otter.util.pure_http import APIError, has_code @@ -26,7 +22,7 @@ def test_publish_autoscale_event(self): Publish an event to cloudfeeds. Successfully handle non-JSON data. """ _log = object() - eff = cf.publish_autoscale_event({'event': 'stuff'}, log=_log) + eff = publish_autoscale_event({'event': 'stuff'}, log=_log) expected = service_request( ServiceType.CLOUD_FEEDS, 'POST', 'autoscale/events', @@ -47,172 +43,3 @@ def test_publish_autoscale_event(self): expected.intent, service_request_eqf(stub_pure_response('', 202)))]) self.assertRaises(APIError, sync_perform, dispatcher, eff) - - -def both_links(method): - """ - Call given test method with "next" and "previous" rel - """ - @wraps(method) - def f(self): - method(self, "next") - method(self, "previous") - return f - - -class ReadEntriesTests(SynchronousTestCase): - - entry = '{}' - feed_fmt = ( - '' - '' - '{entries}') - url = "http://url" - directions = {"previous": cf.Direction.PREVIOUS, - "next": cf.Direction.NEXT} - service_type = ServiceType.CLOUD_FEEDS - - def svc_intent(self, params={}): - return service_request( - self.service_type, "GET", - self.url, params=params, json_response=False).intent - - def feed(self, rel, link, summaries): - return self.feed_fmt.format( - rel=rel, rel_link=link, - entries=''.join(self.entry.format(s) for s in summaries)) - - @both_links - def test_empty(self, rel): - """ - Does not go further when there are no entries and return [] - """ - feedstr = self.feed(rel, "link-doesnt-matter", []) - seq = [ - (self.svc_intent(), const(stub_json_response(feedstr))) - ] - entries_eff = cf.read_entries( - self.service_type, self.url, {}, self.directions[rel]) - self.assertEqual(perform_sequence(seq, entries_eff), ([], {})) - - @both_links - def test_single_page(self, rel): - """ - Collects entries and goes to next link if there are entries and returns - if next one is empty - """ - feed1str = self.feed(rel, "https://url?page=2", ["summary1", "summ2"]) - feed2str = self.feed(rel, "link", []) - seq = [ - (self.svc_intent({"a": "b"}), const(stub_json_response(feed1str))), - (self.svc_intent({"page": ['2']}), - const(stub_json_response(feed2str))) - ] - entries, params = perform_sequence( - seq, - cf.read_entries( - self.service_type, self.url, {"a": "b"}, self.directions[rel])) - self.assertEqual( - [atom.summary(entry) for entry in entries], - ["summary1", "summ2"]) - self.assertEqual(params, {"page": ["2"]}) - - @both_links - def test_multiple_pages(self, rel): - """ - Collects entries and goes to next link if there are entries and - continues until next link returns empty list - """ - feed1_str = self.feed(rel, "https://url?page=2", ["summ1", "summ2"]) - feed2_str = self.feed(rel, "https://url?page=3", ["summ3", "summ4"]) - feed3_str = self.feed(rel, "link", []) - seq = [ - (self.svc_intent(), const(stub_json_response(feed1_str))), - (self.svc_intent({"page": ['2']}), - const(stub_json_response(feed2_str))), - (self.svc_intent({"page": ['3']}), - const(stub_json_response(feed3_str))), - ] - entries, params = perform_sequence( - seq, - cf.read_entries( - self.service_type, self.url, {}, self.directions[rel])) - self.assertEqual( - [atom.summary(entry) for entry in entries], - ["summ1", "summ2", "summ3", "summ4"]) - self.assertEqual(params, {"page": ["3"]}) - - @both_links - def test_follow_limit(self, rel): - """ - Collects entries and keeping following rel link until `follow_limit` is - reached. - """ - feeds = [self.feed(rel, "https://url?page={}".format(i + 1), - ["summ{}".format(i + 1)]) - for i in range(5)] - seq = [ - (self.svc_intent(), const(stub_json_response(feeds[0]))), - (self.svc_intent({"page": ['1']}), - const(stub_json_response(feeds[1]))), - (self.svc_intent({"page": ['2']}), - const(stub_json_response(feeds[2]))), - ] - entries, params = perform_sequence( - seq, - cf.read_entries( - self.service_type, self.url, {}, self.directions[rel], 3)) - self.assertEqual( - [atom.summary(entry) for entry in entries], - ["summ1", "summ2", "summ3"]) - self.assertEqual(params, {"page": ["3"]}) - - @both_links - def test_log_responses(self, rel): - """ - Each request sent is logged if `log_msg_type` is given - """ - feed1_str = self.feed(rel, "https://url?page=2", ["summ1", "summ2"]) - feed2_str = self.feed(rel, "https://url?page=3", ["summ3", "summ4"]) - feed3_str = self.feed(rel, "link", []) - seq = [ - (self.svc_intent(), const(stub_json_response(feed1_str))), - (log_intent("nodemsg", feed1_str, False), noop), - (self.svc_intent({"page": ['2']}), - const(stub_json_response(feed2_str))), - (log_intent("nodemsg", feed2_str, False), noop), - (self.svc_intent({"page": ['3']}), - const(stub_json_response(feed3_str))), - (log_intent("nodemsg", feed3_str, False), noop) - ] - entries, params = perform_sequence( - seq, - cf.read_entries( - self.service_type, self.url, {}, self.directions[rel], - log_msg_type="nodemsg")) - - @both_links - def test_no_link(self, rel): - """ - Returns entries collected till now if there is no rel link - """ - feedstr = ( - '' - 'summary') - seq = [ - (self.svc_intent({"a": "b"}), const(stub_json_response(feedstr))) - ] - entries, params = perform_sequence( - seq, - cf.read_entries( - self.service_type, self.url, {"a": "b"}, self.directions[rel])) - self.assertEqual(atom.summary(entries[0]), "summary") - self.assertEqual(params, {"a": "b"}) - - def test_invalid_direction(self): - """ - Calling `read_entries` with invalid direction raises ValueError - """ - self.assertRaises( - ValueError, sync_perform, base_dispatcher, - cf.read_entries(self.service_type, self.url, {}, "bad")) diff --git a/otter/test/cloud_client/test_init.py b/otter/test/cloud_client/test_init.py index 7cbdbc40c..d2eb954d9 100644 --- a/otter/test/cloud_client/test_init.py +++ b/otter/test/cloud_client/test_init.py @@ -80,6 +80,8 @@ def make_service_configs(): 'name': 'cloudLoadBalancers', 'region': 'DFW'}, ServiceType.CLOUD_FEEDS: { + 'name': 'cloud_feeds', + 'region': 'DFW', 'url': 'special cloudfeeds url' }, ServiceType.CLOUD_ORCHESTRATION: { diff --git a/otter/test/tap/test_api.py b/otter/test/tap/test_api.py index 79cd52db0..a2c96bb1c 100644 --- a/otter/test/tap/test_api.py +++ b/otter/test/tap/test_api.py @@ -495,7 +495,8 @@ def test_cloudfeeds_setup(self): 'url': 'url'} makeService(conf) serv_confs = get_service_configs(conf) - serv_confs[ServiceType.CLOUD_FEEDS] = {'url': 'url'} + serv_confs[ServiceType.CLOUD_FEEDS] = { + 'name': 'cloudFeeds', 'region': 'ord', 'url': 'url'} self.assertEqual(len(get_fanout().subobservers), 1) cf_observer = get_fanout().subobservers[0] diff --git a/otter/test/test_constants.py b/otter/test/test_constants.py index 7f2736f86..35c87bc78 100644 --- a/otter/test/test_constants.py +++ b/otter/test/test_constants.py @@ -14,17 +14,14 @@ def setUp(self): """ Sample config """ - self.config = { - 'cloudServersOpenStack': 'nova', - 'cloudLoadBalancers': 'clb', - 'cloudOrchestration': 'orch', - 'rackconnect': 'rc', - 'region': 'DFW', - 'metrics': {'service': 'm', - 'region': 'IAD'}, - 'cloudfeeds': {'url': 'cf_url'}, - 'terminator': {'cf_cap_url': 'cap_url'} - } + self.config = {'cloudServersOpenStack': 'nova', + 'cloudLoadBalancers': 'clb', + 'cloudOrchestration': 'orch', + 'rackconnect': 'rc', + 'region': 'DFW', + 'metrics': {'service': 'm', + 'region': 'IAD'}, + 'cloudfeeds': {'service': 'cf', 'url': 'url'}} def test_takes_from_config(self): """ @@ -53,19 +50,20 @@ def test_takes_from_config(self): 'name': 'm', 'region': 'IAD', }, - ServiceType.CLOUD_FEEDS: {'url': 'cf_url'}, - ServiceType.CLOUD_FEEDS_CAP: {"url": "cap_url"}, + ServiceType.CLOUD_FEEDS: { + 'name': 'cf', + 'region': 'DFW', + 'url': 'url' + } }) def test_cloudfeeds_optional(self): """ - Does not return cloud feeds services if the config is not there + Does not return cloud feeds service if the config is not there """ del self.config['cloudfeeds'] - del self.config['terminator'] - confs = get_service_configs(self.config) - self.assertNotIn(ServiceType.CLOUD_FEEDS, confs) - self.assertNotIn(ServiceType.CLOUD_FEEDS_CAP, confs) + self.assertNotIn(ServiceType.CLOUD_FEEDS, + get_service_configs(self.config)) def test_metrics_optional(self): """