diff options
-rw-r--r-- | ceilometerclient/client.py | 203 | ||||
-rw-r--r-- | ceilometerclient/shell.py | 233 | ||||
-rw-r--r-- | ceilometerclient/tests/test_client.py | 2 | ||||
-rw-r--r-- | ceilometerclient/tests/test_shell.py | 68 |
4 files changed, 364 insertions, 142 deletions
diff --git a/ceilometerclient/client.py b/ceilometerclient/client.py index 7bfe1a6..b66e547 100644 --- a/ceilometerclient/client.py +++ b/ceilometerclient/client.py @@ -10,38 +10,133 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient.v2_0 import client as ksclient +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import discover +from keystoneclient import session import six from ceilometerclient.common import utils - - -def _get_ksclient(**kwargs): - """Get an endpoint and auth token from Keystone. - - :param kwargs: keyword args containing credentials: - * username: name of user - * password: user's password - * auth_url: endpoint to authenticate against - * cacert: path of CA TLS certificate - * insecure: allow insecure SSL (no cert verification) - * tenant_{name|id}: name or ID of tenant - """ - return ksclient.Client(username=kwargs.get('username'), - password=kwargs.get('password'), - tenant_id=kwargs.get('tenant_id'), - tenant_name=kwargs.get('tenant_name'), - auth_url=kwargs.get('auth_url'), - region_name=kwargs.get('region_name'), - cacert=kwargs.get('cacert'), - insecure=kwargs.get('insecure')) - - -def _get_endpoint(client, **kwargs): - """Get an endpoint using the provided keystone client.""" - return client.service_catalog.url_for( - service_type=kwargs.get('service_type') or 'metering', - endpoint_type=kwargs.get('endpoint_type') or 'publicURL') +from ceilometerclient import exc + + +def _get_keystone_session(**kwargs): + # TODO(fabgia): the heavy lifting here should be really done by Keystone. + # Unfortunately Keystone does not support a richer method to perform + # discovery and return a single viable URL. A bug against Keystone has + # been filed: https://bugs.launchpad.net/pyhton-keystoneclient/+bug/1330677 + + # first create a Keystone session + cacert = kwargs.pop('cacert', None) + cert = kwargs.pop('cert', None) + key = kwargs.pop('key', None) + insecure = kwargs.pop('insecure', False) + auth_url = kwargs.pop('auth_url', None) + project_id = kwargs.pop('project_id', None) + project_name = kwargs.pop('project_name', None) + + if insecure: + verify = False + else: + verify = cacert or True + + if cert and key: + # passing cert and key together is deprecated in favour of the + # requests lib form of having the cert and key as a tuple + cert = (cert, key) + + # create the keystone client session + ks_session = session.Session(verify=verify, cert=cert) + + try: + # discover the supported keystone versions using the auth endpoint url + ks_discover = discover.Discover(session=ks_session, auth_url=auth_url) + # Determine which authentication plugin to use. + v2_auth_url = ks_discover.url_for('2.0') + v3_auth_url = ks_discover.url_for('3.0') + except Exception: + raise exc.CommandError('Unable to determine the Keystone version ' + 'to authenticate with using the given ' + 'auth_url: %s' % auth_url) + + username = kwargs.pop('username', None) + user_id = kwargs.pop('user_id', None) + user_domain_name = kwargs.pop('user_domain_name', None) + user_domain_id = kwargs.pop('user_domain_id', None) + project_domain_name = kwargs.pop('project_domain_name', None) + project_domain_id = kwargs.pop('project_domain_id', None) + auth = None + + if v3_auth_url and v2_auth_url: + # the auth_url does not have the versions specified + # e.g. http://no.where:5000 + # Keystone will return both v2 and v3 as viable options + # but we need to decide based on the arguments passed + # what version is callable + if (user_domain_name or user_domain_id or project_domain_name or + project_domain_id): + # domain is supported only in v3 + auth = v3_auth.Password( + v3_auth_url, + username=username, + user_id=user_id, + user_domain_name=user_domain_name, + user_domain_id=user_domain_id, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id, + **kwargs) + else: + # no domain, then use v2 + auth = v2_auth.Password( + v2_auth_url, + username, + kwargs.pop('password', None), + tenant_id=project_id, + tenant_name=project_name) + elif v3_auth_url: + # the auth_url as v3 specified + # e.g. http://no.where:5000/v3 + # Keystone will return only v3 as viable option + auth = v3_auth.Password( + v3_auth_url, + username=username, + user_id=user_id, + user_domain_name=user_domain_name, + user_domain_id=user_domain_id, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id, + **kwargs) + elif v2_auth_url: + # the auth_url as v2 specified + # e.g. http://no.where:5000/v2.0 + # Keystone will return only v2 as viable option + auth = v2_auth.Password( + v2_auth_url, + username, + kwargs.pop('password', None), + tenant_id=project_id, + tenant_name=project_name) + else: + raise exc.CommandError('Unable to determine the Keystone version ' + 'to authenticate with using the given ' + 'auth_url.') + + ks_session.auth = auth + return ks_session + + +def _get_endpoint(ks_session, **kwargs): + """Get an endpoint using the provided keystone session.""" + + # set service specific endpoint types + endpoint_type = kwargs.get('endpoint_type') or 'publicURL' + service_type = kwargs.get('service_type') or 'metering' + + endpoint = ks_session.get_endpoint(service_type=service_type, + endpoint_type=endpoint_type, + region_name=kwargs.get('region_name')) + + return endpoint def get_client(api_version, **kwargs): @@ -55,10 +150,19 @@ def get_client(api_version, **kwargs): or: * os_username: name of user * os_password: user's password + * os_user_id: user's id + * os_user_domain_id: the domain id of the user + * os_user_domain_name: the domain name of the user + * os_project_id: the user project id + * os_tenant_id: V2 alternative to os_project_id + * os_project_name: the user project name + * os_tenant_name: V2 alternative to os_project_name + * os_project_domain_name: domain name for the user project + * os_project_domain_id: domain id for the user project * os_auth_url: endpoint to authenticate against - * os_cacert: path of CA TLS certificate + * os_cert|os_cacert: path of CA TLS certificate + * os_key: SSL private key * insecure: allow insecure SSL (no cert verification) - * os_tenant_{name|id}: name or ID of tenant """ token = kwargs.get('os_auth_token') if token and not six.callable(token): @@ -66,36 +170,41 @@ def get_client(api_version, **kwargs): if token and kwargs.get('ceilometer_url'): endpoint = kwargs.get('ceilometer_url') - elif (kwargs.get('os_username') and - kwargs.get('os_password') and - kwargs.get('os_auth_url') and - (kwargs.get('os_tenant_id') or kwargs.get('os_tenant_name'))): - + else: + project_id = kwargs.get('os_project_id') or kwargs.get('os_tenant_id') + project_name = (kwargs.get('os_project_name') or + kwargs.get('os_tenant_name')) ks_kwargs = { 'username': kwargs.get('os_username'), 'password': kwargs.get('os_password'), - 'tenant_id': kwargs.get('os_tenant_id'), - 'tenant_name': kwargs.get('os_tenant_name'), + 'user_id': kwargs.get('os_user_id'), + 'user_domain_id': kwargs.get('os_user_domain_id'), + 'user_domain_name': kwargs.get('os_user_domain_name'), + 'project_id': project_id, + 'project_name': project_name, + 'project_domain_name': kwargs.get('os_project_domain_name'), + 'project_domain_id': kwargs.get('os_project_domain_id'), 'auth_url': kwargs.get('os_auth_url'), - 'region_name': kwargs.get('os_region_name'), - 'service_type': kwargs.get('os_service_type'), - 'endpoint_type': kwargs.get('os_endpoint_type'), 'cacert': kwargs.get('os_cacert'), - 'insecure': kwargs.get('insecure'), + 'cert': kwargs.get('os_cert'), + 'key': kwargs.get('os_key'), + 'insecure': kwargs.get('insecure') } - _ksclient = _get_ksclient(**ks_kwargs) - token = token or (lambda: _ksclient.auth_token) + + # retrieve session + ks_session = _get_keystone_session(**ks_kwargs) + token = token or (lambda: ks_session.get_token()) endpoint = kwargs.get('ceilometer_url') or \ - _get_endpoint(_ksclient, **ks_kwargs) + _get_endpoint(ks_session, **ks_kwargs) cli_kwargs = { 'token': token, 'insecure': kwargs.get('insecure'), 'timeout': kwargs.get('timeout'), 'cacert': kwargs.get('os_cacert'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), + 'cert_file': kwargs.get('os_cert'), + 'key_file': kwargs.get('os_key') } return Client(api_version, endpoint, **cli_kwargs) diff --git a/ceilometerclient/shell.py b/ceilometerclient/shell.py index 6e26f31..46a460c 100644 --- a/ceilometerclient/shell.py +++ b/ceilometerclient/shell.py @@ -32,36 +32,11 @@ from ceilometerclient.openstack.common import strutils class CeilometerShell(object): - def get_base_parser(self): - parser = argparse.ArgumentParser( - prog='ceilometer', - description=__doc__.strip(), - epilog='See "ceilometer help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=HelpFormatter, - ) - - # Global arguments - parser.add_argument('-h', '--help', - action='store_true', - help=argparse.SUPPRESS, - ) - - parser.add_argument('--version', - action='version', - version=ceilometerclient.__version__) - - parser.add_argument('-d', '--debug', - default=bool(cliutils.env('CEILOMETERCLIENT_DEBUG') - ), - action='store_true', - help='Defaults to env[CEILOMETERCLIENT_DEBUG].') - - parser.add_argument('-v', '--verbose', - default=False, action="store_true", - help="Print more verbose output.") - + def _append_identity_args(self, parser): + # FIXME(fabgia): identity related parameters should be passed by the + # Keystone client itself to avoid constant update in all the services + # clients. When this fix is merged this method can be made obsolete. + # Bug: https://bugs.launchpad.net/python-keystoneclient/+bug/1332337 parser.add_argument('-k', '--insecure', default=False, action='store_true', @@ -72,32 +47,7 @@ class CeilometerShell(object): "authorities. This option should be used with " "caution.") - parser.add_argument('--cert-file', - help='Path of certificate file to use in SSL ' - 'connection. This file can optionally be prepended' - ' with the private key.') - - parser.add_argument('--key-file', - help='Path of client key to use in SSL connection.' - ' This option is not necessary if your key is ' - 'prepended to your cert file.') - - parser.add_argument('--os-cacert', - metavar='<ca-certificate-file>', - dest='os_cacert', - default=cliutils.env('OS_CACERT'), - help='Path of CA TLS certificate(s) used to verify' - 'the remote server\'s certificate. Without this ' - 'option ceilometer looks for the default system ' - 'CA certificates.') - parser.add_argument('--ca-file', - dest='os_cacert', - help='DEPRECATED! Use --os-cacert.') - - parser.add_argument('--timeout', - default=600, - help='Number of seconds to wait for a response.') - + # User related options parser.add_argument('--os-username', default=cliutils.env('OS_USERNAME'), help='Defaults to env[OS_USERNAME].') @@ -105,6 +55,10 @@ class CeilometerShell(object): parser.add_argument('--os_username', help=argparse.SUPPRESS) + parser.add_argument('--os-user-id', + default=cliutils.env('OS_USER_ID'), + help='Defaults to env[OS_USER_ID].') + parser.add_argument('--os-password', default=cliutils.env('OS_PASSWORD'), help='Defaults to env[OS_PASSWORD].') @@ -112,9 +66,43 @@ class CeilometerShell(object): parser.add_argument('--os_password', help=argparse.SUPPRESS) + # Domain related options + parser.add_argument('--os-user-domain-id', + default=cliutils.env('OS_USER_DOMAIN_ID'), + help='Defaults to env[OS_USER_DOMAIN_ID].') + + parser.add_argument('--os-user-domain-name', + default=cliutils.env('OS_USER_DOMAIN_NAME'), + help='Defaults to env[OS_USER_DOMAIN_NAME].') + + parser.add_argument('--os-project-domain-id', + default=cliutils.env('OS_PROJECT_DOMAIN_ID'), + help='Defaults to env[OS_PROJECT_DOMAIN_ID].') + + parser.add_argument('--os-project-domain-name', + default=cliutils.env('OS_PROJECT_DOMAIN_NAME'), + help='Defaults to env[OS_PROJECT_DOMAIN_NAME].') + + # Project V3 or Tenant V2 related options + parser.add_argument('--os-project-id', + default=cliutils.env('OS_PROJECT_ID'), + help='Another way to specify tenant ID. ' + 'This option is mutually exclusive with ' + ' --os-tenant-id. ' + 'Defaults to env[OS_PROJECT_ID].') + + parser.add_argument('--os-project-name', + default=cliutils.env('OS_PROJECT_NAME'), + help='Another way to specify tenant name. ' + 'This option is mutually exclusive with ' + ' --os-tenant-name. ' + 'Defaults to env[OS_PROJECT_NAME].') + parser.add_argument('--os-tenant-id', default=cliutils.env('OS_TENANT_ID'), - help='Defaults to env[OS_TENANT_ID].') + help='This option is mutually exclusive with ' + ' --os-project-id. ' + 'Defaults to env[OS_PROJECT_ID].') parser.add_argument('--os_tenant_id', help=argparse.SUPPRESS) @@ -126,6 +114,7 @@ class CeilometerShell(object): parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) + # Auth related options parser.add_argument('--os-auth-url', default=cliutils.env('OS_AUTH_URL'), help='Defaults to env[OS_AUTH_URL].') @@ -133,6 +122,47 @@ class CeilometerShell(object): parser.add_argument('--os_auth_url', help=argparse.SUPPRESS) + parser.add_argument('--os-auth-token', + default=cliutils.env('OS_AUTH_TOKEN'), + help='Defaults to env[OS_AUTH_TOKEN].') + + parser.add_argument('--os_auth_token', + help=argparse.SUPPRESS) + + parser.add_argument('--os-cacert', + metavar='<ca-certificate-file>', + dest='os_cacert', + default=cliutils.env('OS_CACERT'), + help='Path of CA TLS certificate(s) used to verify' + 'the remote server\'s certificate. Without this ' + 'option ceilometer looks for the default system ' + 'CA certificates.') + + parser.add_argument('--os-cert', + help='Path of certificate file to use in SSL ' + 'connection. This file can optionally be ' + 'prepended with the private key.') + + parser.add_argument('--os-key', + help='Path of client key to use in SSL ' + 'connection. This option is not necessary ' + 'if your key is prepended to your cert file.') + + # Service Catalog related options + parser.add_argument('--os-service-type', + default=cliutils.env('OS_SERVICE_TYPE'), + help='Defaults to env[OS_SERVICE_TYPE].') + + parser.add_argument('--os_service_type', + help=argparse.SUPPRESS) + + parser.add_argument('--os-endpoint-type', + default=cliutils.env('OS_ENDPOINT_TYPE'), + help='Defaults to env[OS_ENDPOINT_TYPE].') + + parser.add_argument('--os_endpoint_type', + help=argparse.SUPPRESS) + parser.add_argument('--os-region-name', default=cliutils.env('OS_REGION_NAME'), help='Defaults to env[OS_REGION_NAME].') @@ -140,12 +170,52 @@ class CeilometerShell(object): parser.add_argument('--os_region_name', help=argparse.SUPPRESS) - parser.add_argument('--os-auth-token', - default=cliutils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN].') + # Deprecated options + parser.add_argument('--ca-file', + dest='os_cacert', + help='DEPRECATED! Use --os-cacert.') - parser.add_argument('--os_auth_token', - help=argparse.SUPPRESS) + parser.add_argument('--cert-file', + dest='os_cert', + help='DEPRECATED! Use --os-cert.') + + parser.add_argument('--key-file', + dest='os_key', + help='DEPRECATED! Use --os-key.') + + def get_base_parser(self): + parser = argparse.ArgumentParser( + prog='ceilometer', + description=__doc__.strip(), + epilog='See "ceilometer help COMMAND" ' + 'for help on a specific command.', + add_help=False, + formatter_class=HelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', '--help', + action='store_true', + help=argparse.SUPPRESS, + ) + + parser.add_argument('--version', + action='version', + version=ceilometerclient.__version__) + + parser.add_argument('-d', '--debug', + default=bool(cliutils.env('CEILOMETERCLIENT_DEBUG') + ), + action='store_true', + help='Defaults to env[CEILOMETERCLIENT_DEBUG].') + + parser.add_argument('-v', '--verbose', + default=False, action="store_true", + help="Print more verbose output.") + + parser.add_argument('--timeout', + default=600, + help='Number of seconds to wait for a response.') parser.add_argument('--ceilometer-url', default=cliutils.env('CEILOMETER_URL'), @@ -163,19 +233,9 @@ class CeilometerShell(object): parser.add_argument('--ceilometer_api_version', help=argparse.SUPPRESS) - parser.add_argument('--os-service-type', - default=cliutils.env('OS_SERVICE_TYPE'), - help='Defaults to env[OS_SERVICE_TYPE].') - - parser.add_argument('--os_service_type', - help=argparse.SUPPRESS) - - parser.add_argument('--os-endpoint-type', - default=cliutils.env('OS_ENDPOINT_TYPE'), - help='Defaults to env[OS_ENDPOINT_TYPE].') - - parser.add_argument('--os_endpoint_type', - help=argparse.SUPPRESS) + # FIXME(fabgia): identity related parameters should be passed by the + # Keystone client itself. + self._append_identity_args(parser) return parser @@ -247,6 +307,14 @@ class CeilometerShell(object): # Return parsed args return api_version, subcommand_parser.parse_args(argv) + def no_project_and_domain_set(self, args): + if not (args.os_project_id or (args.os_project_name and + (args.os_user_domain_name or args.os_user_domain_id)) or + (args.os_tenant_id or args.os_tenant_name)): + return True + else: + return False + def main(self, argv): parsed = self.parse_args(argv) if parsed == 0: @@ -272,10 +340,17 @@ class CeilometerShell(object): "either --os-password or via " "env[OS_PASSWORD]") - if not (args.os_tenant_id or args.os_tenant_name): - raise exc.CommandError("You must provide a tenant_id via " - "either --os-tenant-id or via " - "env[OS_TENANT_ID]") + if self.no_project_and_domain_set(args): + # steer users towards Keystone V3 API + raise exc.CommandError("You must provide a project_id via " + "either --os-project-id or via " + "env[OS_PROJECT_ID] and " + "a domain_name via either " + "--os-user-domain-name or via " + "env[OS_USER_DOMAIN_NAME] or " + "a domain_id via either " + "--os-user-domain-id or via " + "env[OS_USER_DOMAIN_ID]") if not args.os_auth_url: raise exc.CommandError("You must provide an auth url via " diff --git a/ceilometerclient/tests/test_client.py b/ceilometerclient/tests/test_client.py index de46c9a..14aaca6 100644 --- a/ceilometerclient/tests/test_client.py +++ b/ceilometerclient/tests/test_client.py @@ -20,7 +20,7 @@ from ceilometerclient.v2 import client as v2client FAKE_ENV = {'os_username': 'username', 'os_password': 'password', 'os_tenant_name': 'tenant_name', - 'os_auth_url': 'http://no.where', + 'os_auth_url': 'http://no.where:5000/', 'os_auth_token': '1234', 'ceilometer_url': 'http://no.where'} diff --git a/ceilometerclient/tests/test_shell.py b/ceilometerclient/tests/test_shell.py index be6e115..5f0ffba 100644 --- a/ceilometerclient/tests/test_shell.py +++ b/ceilometerclient/tests/test_shell.py @@ -14,7 +14,7 @@ import re import sys import fixtures -from keystoneclient.v2_0 import client as ksclient +from keystoneclient import session as ks_session import mock import six from testtools import matchers @@ -24,25 +24,31 @@ from ceilometerclient import shell as ceilometer_shell from ceilometerclient.tests import utils from ceilometerclient.v1 import client as v1client -FAKE_ENV = {'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where'} +FAKE_V2_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://localhost:5000/v2.0'} + +FAKE_V3_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_USER_DOMAIN_NAME': 'domain_name', + 'OS_PROJECT_ID': '1234567890', + 'OS_AUTH_URL': 'http://localhost:5000/v3'} class ShellTest(utils.BaseTestCase): re_options = re.DOTALL | re.MULTILINE # Patch os.environ to avoid required auth info. - def make_env(self, exclude=None): - env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude) + def make_env(self, env_version, exclude=None): + env = dict((k, v) for k, v in env_version.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): super(ShellTest, self).setUp() @mock.patch('sys.stdout', new=six.StringIO()) - @mock.patch.object(ksclient, 'Client') + @mock.patch.object(ks_session, 'Session') @mock.patch.object(v1client.http.HTTPClient, 'json_request') @mock.patch.object(v1client.http.HTTPClient, 'raw_request') def shell(self, argstr, mock_ksclient, mock_json, mock_raw): @@ -85,28 +91,60 @@ class ShellTest(utils.BaseTestCase): self.assertThat(help_text, matchers.MatchesRegex(r, self.re_options)) + +class ShellKeystoneV2Test(ShellTest): + + def test_auth_param(self): + self.make_env(FAKE_V2_ENV, exclude='OS_USERNAME') + self.test_help() + + @mock.patch.object(ks_session, 'Session') + def test_debug_switch_raises_error(self, mock_ksclient): + mock_ksclient.side_effect = exc.HTTPUnauthorized + self.make_env(FAKE_V2_ENV) + args = ['--debug', 'event-list'] + self.assertRaises(exc.HTTPUnauthorized, ceilometer_shell.main, args) + + @mock.patch.object(ks_session, 'Session') + def test_dash_d_switch_raises_error(self, mock_ksclient): + mock_ksclient.side_effect = exc.CommandError("FAIL") + self.make_env(FAKE_V2_ENV) + args = ['-d', 'event-list'] + self.assertRaises(exc.CommandError, ceilometer_shell.main, args) + + @mock.patch('sys.stderr') + @mock.patch.object(ks_session, 'Session') + def test_no_debug_switch_no_raises_errors(self, mock_ksclient, __): + mock_ksclient.side_effect = exc.HTTPUnauthorized("FAIL") + self.make_env(FAKE_V2_ENV) + args = ['event-list'] + self.assertRaises(SystemExit, ceilometer_shell.main, args) + + +class ShellKeystoneV3Test(ShellTest): + def test_auth_param(self): - self.make_env(exclude='OS_USERNAME') + self.make_env(FAKE_V3_ENV, exclude='OS_USER_DOMAIN_NAME') self.test_help() - @mock.patch.object(ksclient, 'Client') + @mock.patch.object(ks_session, 'Session') def test_debug_switch_raises_error(self, mock_ksclient): mock_ksclient.side_effect = exc.HTTPUnauthorized - self.make_env() + self.make_env(FAKE_V3_ENV) args = ['--debug', 'event-list'] self.assertRaises(exc.HTTPUnauthorized, ceilometer_shell.main, args) - @mock.patch.object(ksclient, 'Client') + @mock.patch.object(ks_session, 'Session') def test_dash_d_switch_raises_error(self, mock_ksclient): mock_ksclient.side_effect = exc.CommandError("FAIL") - self.make_env() + self.make_env(FAKE_V3_ENV) args = ['-d', 'event-list'] self.assertRaises(exc.CommandError, ceilometer_shell.main, args) @mock.patch('sys.stderr') - @mock.patch.object(ksclient, 'Client') + @mock.patch.object(ks_session, 'Session') def test_no_debug_switch_no_raises_errors(self, mock_ksclient, __): mock_ksclient.side_effect = exc.HTTPUnauthorized("FAIL") - self.make_env() + self.make_env(FAKE_V3_ENV) args = ['event-list'] self.assertRaises(SystemExit, ceilometer_shell.main, args) |