diff options
-rw-r--r-- | ceilometerclient/client.py | 203 | ||||
-rw-r--r-- | ceilometerclient/shell.py | 245 | ||||
-rw-r--r-- | ceilometerclient/tests/test_client.py | 2 | ||||
-rw-r--r-- | ceilometerclient/tests/test_shell.py | 68 | ||||
-rw-r--r-- | ceilometerclient/tests/v2/test_alarms.py | 40 | ||||
-rw-r--r-- | ceilometerclient/tests/v2/test_events.py | 45 | ||||
-rw-r--r-- | ceilometerclient/tests/v2/test_resources.py | 11 | ||||
-rw-r--r-- | ceilometerclient/tests/v2/test_shell.py | 50 | ||||
-rw-r--r-- | ceilometerclient/v2/alarms.py | 16 | ||||
-rw-r--r-- | ceilometerclient/v2/events.py | 5 | ||||
-rw-r--r-- | ceilometerclient/v2/resources.py | 5 | ||||
-rw-r--r-- | ceilometerclient/v2/shell.py | 6 |
12 files changed, 516 insertions, 180 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 9d87b2c..98f3851 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 @@ -249,6 +309,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: @@ -274,10 +342,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 " @@ -323,6 +398,18 @@ class CeilometerShell(object): class HelpFormatter(argparse.HelpFormatter): + INDENT_BEFORE_ARGUMENTS = 6 + MAX_WIDTH_ARGUMENTS = 32 + + def add_arguments(self, actions): + for action in filter(lambda x: not x.option_strings, actions): + for choice in action.choices: + length = len(choice) + self.INDENT_BEFORE_ARGUMENTS + if(length > self._max_help_position and + length <= self.MAX_WIDTH_ARGUMENTS): + self._max_help_position = length + super(HelpFormatter, self).add_arguments(actions) + def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) 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) diff --git a/ceilometerclient/tests/v2/test_alarms.py b/ceilometerclient/tests/v2/test_alarms.py index 13d04d1..a0e13ac 100644 --- a/ceilometerclient/tests/v2/test_alarms.py +++ b/ceilometerclient/tests/v2/test_alarms.py @@ -201,6 +201,10 @@ fixtures = { {}, UPDATED_ALARM, ), + 'DELETE': ( + {}, + None, + ), }, '/v2/alarms/alarm-id/state': { @@ -341,7 +345,41 @@ class AlarmManagerTest(testtools.TestCase): ('DELETE', '/v2/alarms/victim-id', {}, None), ] self.assertEqual(self.api.calls, expect) - self.assertTrue(deleted is None) + self.assertIsNone(deleted) + + def test_get_from_alarm_class(self): + alarm = self.mgr.get(alarm_id='alarm-id') + self.assertTrue(alarm) + alarm.get() + expect = [ + ('GET', '/v2/alarms/alarm-id', {}, None), + ('GET', '/v2/alarms/alarm-id', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('alarm-id', alarm.alarm_id) + self.assertEqual(alarm.threshold_rule, alarm.rule) + + def test_get_state_from_alarm_class(self): + alarm = self.mgr.get(alarm_id='alarm-id') + self.assertTrue(alarm) + state = alarm.get_state() + expect = [ + ('GET', '/v2/alarms/alarm-id', {}, None), + ('GET', '/v2/alarms/alarm-id/state', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('alarm', state) + + def test_delete_from_alarm_class(self): + alarm = self.mgr.get(alarm_id='alarm-id') + self.assertTrue(alarm) + deleted = alarm.delete() + expect = [ + ('GET', '/v2/alarms/alarm-id', {}, None), + ('DELETE', '/v2/alarms/alarm-id', {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(deleted) def _do_test_get_history(self, q, url): history = self.mgr.get_history(q=q, alarm_id='alarm-id') diff --git a/ceilometerclient/tests/v2/test_events.py b/ceilometerclient/tests/v2/test_events.py index 96cf7f5..4935d8b 100644 --- a/ceilometerclient/tests/v2/test_events.py +++ b/ceilometerclient/tests/v2/test_events.py @@ -22,22 +22,22 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', - 'traits': {'trait_A': 'abc', - 'message_id': '1'}, + 'traits': {'trait_A': 'abc'}, }, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', - 'traits': {'trait_A': 'def', - 'message_id': '2'}, + 'traits': {'trait_A': 'def'}, }, { + 'message_id': '3', 'event_type': 'Bar', 'generated': '1970-01-01T00:00:00', - 'traits': {'trait_B': 'bartrait', - 'message_id': '3'}, + 'traits': {'trait_B': 'bartrait'}, }, ] ), @@ -48,18 +48,18 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'abc', - 'hostname': 'localhost', - 'message_id': '1'}, + 'hostname': 'localhost'}, }, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'def', - 'hostname': 'localhost', - 'message_id': '2'}, + 'hostname': 'localhost'}, } ] ), @@ -70,18 +70,18 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'abc', - 'hostname': 'foreignhost', - 'message_id': '1'}, + 'hostname': 'foreignhost'}, }, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'def', - 'hostname': 'foreignhost', - 'message_id': '2'}, + 'hostname': 'foreignhost'}, } ] ), @@ -93,12 +93,12 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Bar', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'abc', 'hostname': 'localhost', - 'num_cpus': '5', - 'message_id': '1'}, + 'num_cpus': '5'}, }, ] ), @@ -109,10 +109,10 @@ fixtures = { 'GET': ( {}, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'def', - 'message_id': '2', 'intTrait': '42'}, } ), @@ -186,3 +186,14 @@ class EventManagerTest(utils.BaseTestCase): ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(events), 1) + + def test_get_from_event_class(self): + event = self.mgr.get(2) + self.assertTrue(event) + event.get() + expect = [ + ('GET', '/v2/events/2', {}, None), + ('GET', '/v2/events/2', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('Foo', event.event_type) diff --git a/ceilometerclient/tests/v2/test_resources.py b/ceilometerclient/tests/v2/test_resources.py index 13a29fe..d62ce40 100644 --- a/ceilometerclient/tests/v2/test_resources.py +++ b/ceilometerclient/tests/v2/test_resources.py @@ -104,3 +104,14 @@ class ResourceManagerTest(utils.BaseTestCase): self.assertEqual(self.api.calls, expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'a') + + def test_get_from_resource_class(self): + resource = self.mgr.get(resource_id='a') + self.assertTrue(resource) + resource.get() + expect = [ + ('GET', '/v2/resources/a', {}, None), + ('GET', '/v2/resources/a', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('a', resource.resource_id) diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 819e938..96ab38f 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -178,6 +178,20 @@ class ShellAlarmCommandTest(utils.BaseTestCase): "type": "threshold", "name": "cpu_high"} + THRESHOLD_ALARM_CLI_ARGS = [ + '--name', 'cpu_high', + '--description', 'instance running hot', + '--meter-name', 'cpu_util', + '--threshold', '70.0', + '--comparison-operator', 'gt', + '--statistic', 'avg', + '--period', '600', + '--evaluation-periods', '3', + '--alarm-action', 'log://', + '--alarm-action', 'http://example.com/alarm/state', + '--query', 'resource_id=INSTANCE_ID' + ] + def setUp(self): super(ShellAlarmCommandTest, self).setUp() self.cc = mock.Mock() @@ -201,7 +215,7 @@ class ShellAlarmCommandTest(utils.BaseTestCase): if repeat_actions is not None: self.assertEqual(repeat_actions, kwargs.get('repeat_actions')) else: - self.assertFalse('repeat_actions' in kwargs) + self.assertNotIn('repeat_actions', kwargs) def test_alarm_update_repeat_actions_untouched(self): method = ceilometer_shell.do_alarm_update @@ -241,31 +255,33 @@ class ShellAlarmCommandTest(utils.BaseTestCase): @mock.patch('sys.stdout', new=six.StringIO()) def test_alarm_threshold_create_args(self): + argv = ['alarm-threshold-create'] + self.THRESHOLD_ALARM_CLI_ARGS + self._test_alarm_threshold_action_args('create', argv) + + def test_alarm_threshold_update_args(self): + argv = ['alarm-threshold-update', + '--alarm_id', 'x'] + self.THRESHOLD_ALARM_CLI_ARGS + self._test_alarm_threshold_action_args('update', argv) + + @mock.patch('sys.stdout', new=six.StringIO()) + def _test_alarm_threshold_action_args(self, action, argv): shell = base_shell.CeilometerShell() - argv = ['alarm-threshold-create', - '--name', 'cpu_high', - '--description', 'instance running hot', - '--meter-name', 'cpu_util', - '--threshold', '70.0', - '--comparison-operator', 'gt', - '--statistic', 'avg', - '--period', '600', - '--evaluation-periods', '3', - '--alarm-action', 'log://', - '--alarm-action', 'http://example.com/alarm/state', - '--query', 'resource_id=INSTANCE_ID'] _, args = shell.parse_args(argv) alarm = alarms.Alarm(mock.Mock(), self.ALARM) - self.cc.alarms.create.return_value = alarm + getattr(self.cc.alarms, action).return_value = alarm - ceilometer_shell.do_alarm_threshold_create(self.cc, args) - _, kwargs = self.cc.alarms.create.call_args + func = getattr(ceilometer_shell, 'do_alarm_threshold_' + action) + func(self.cc, args) + _, kwargs = getattr(self.cc.alarms, action).call_args + self._check_alarm_threshold_args(kwargs) + + def _check_alarm_threshold_args(self, kwargs): self.assertEqual('cpu_high', kwargs.get('name')) self.assertEqual('instance running hot', kwargs.get('description')) actions = ['log://', 'http://example.com/alarm/state'] self.assertEqual(actions, kwargs.get('alarm_actions')) - self.assertTrue('threshold_rule' in kwargs) + self.assertIn('threshold_rule', kwargs) rule = kwargs['threshold_rule'] self.assertEqual('cpu_util', rule.get('meter_name')) self.assertEqual(70.0, rule.get('threshold')) diff --git a/ceilometerclient/v2/alarms.py b/ceilometerclient/v2/alarms.py index 50fa1e4..7c46fb2 100644 --- a/ceilometerclient/v2/alarms.py +++ b/ceilometerclient/v2/alarms.py @@ -19,6 +19,7 @@ import warnings from ceilometerclient.common import base from ceilometerclient.common import utils +from ceilometerclient import exc from ceilometerclient.v2 import options @@ -48,8 +49,16 @@ class Alarm(base.Resource): # that look like the Alarm storage object if k == 'rule': k = '%s_rule' % self.type + if k == 'id': + return self.alarm_id return super(Alarm, self).__getattr__(k) + def delete(self): + return self.manager.delete(self.alarm_id) + + def get_state(self): + return self.manager.get_state(self.alarm_id) + class AlarmChange(base.Resource): def __repr__(self): @@ -74,6 +83,13 @@ class AlarmManager(base.Manager): return self._list(self._path(alarm_id), expect_single=True)[0] except IndexError: return None + except exc.HTTPNotFound: + # When we try to get deleted alarm HTTPNotFound occurs + # or when alarm doesn't exists this exception don't must + # go deeper because cleanUp() (method which remove all + # created things like instance, alarm, etc.) at scenario + # tests doesn't know how to process it + return None @classmethod def _compat_legacy_alarm_kwargs(cls, kwargs, create=False): diff --git a/ceilometerclient/v2/events.py b/ceilometerclient/v2/events.py index bf96d4c..32380d6 100644 --- a/ceilometerclient/v2/events.py +++ b/ceilometerclient/v2/events.py @@ -20,6 +20,11 @@ class Event(base.Resource): def __repr__(self): return "<Event %s>" % self._info + def __getattr__(self, k): + if k == 'id': + return self.message_id + return super(Event, self).__getattr__(k) + class EventManager(base.Manager): resource_class = Event diff --git a/ceilometerclient/v2/resources.py b/ceilometerclient/v2/resources.py index 897033e..99e6fd5 100644 --- a/ceilometerclient/v2/resources.py +++ b/ceilometerclient/v2/resources.py @@ -21,6 +21,11 @@ class Resource(base.Resource): def __repr__(self): return "<Resource %s>" % self._info + def __getattr__(self, k): + if k == 'id': + return self.resource_id + return super(Resource, self).__getattr__(k) + class ResourceManager(base.Manager): resource_class = Resource diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 9be9b02..b13ab20 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -322,9 +322,8 @@ def _display_alarm(alarm): help='ID of the alarm to show.') def do_alarm_show(cc, args={}): '''Show an alarm.''' - try: - alarm = cc.alarms.get(args.alarm_id) - except exc.HTTPNotFound: + alarm = cc.alarms.get(args.alarm_id) + if alarm is None: raise exc.CommandError('Alarm not found: %s' % args.alarm_id) else: _display_alarm(alarm) @@ -545,6 +544,7 @@ def do_alarm_update(cc, args={}): dest='threshold_rule/threshold', help='Threshold to evaluate against.') @utils.arg('-q', '--query', metavar='<QUERY>', + dest='threshold_rule/query', help='key[op]data_type::value; list. data_type is optional, ' 'but if supplied must be string, integer, float, or boolean.') @utils.arg('--repeat-actions', dest='repeat_actions', |