From 126b2c543617866e9e1ea45ef9c5770ce5f5dda9 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 16:11:37 -0500 Subject: [PATCH] Add an API example base and functional test base Add examples/common.py, which is a basic common setup that mimics OSC's configuration options and logging without the rest of the CLI. Also add the functional test tooling for examples to prevent bit rot. Co-Authored-By: Dean Troyer Change-Id: Ie92b675eafd93482ddc9a8ce0b0588e23ed50c35 --- examples/common.py | 264 ++++++++++++++++++++++++++++++ functional/common/test.py | 6 + functional/tests/test_examples.py | 22 +++ 3 files changed, 292 insertions(+) create mode 100755 examples/common.py create mode 100644 functional/tests/test_examples.py diff --git a/examples/common.py b/examples/common.py new file mode 100755 index 0000000000..ad3a5e492e --- /dev/null +++ b/examples/common.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# common.py - Common bits for API examples + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +API Examples + +This is a collection of common functions used by the example scripts. +It may also be run directly as a script to do basic testing of itself. + +common.object_parser() provides the common set of command-line arguments +used in the library CLIs for setting up authentication. This should make +playing with the example scripts against a running OpenStack simpler. + +common.configure_logging() provides the same basic logging control as +the OSC shell. + +common.make_session() does the minimal loading of a Keystone authentication +plugin and creates a Keystone client Session. + +""" + +import argparse +import logging +import os +import sys +import traceback + +from keystoneclient import session as ksc_session + +from openstackclient.api import auth + + +CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' +DEFAULT_VERBOSE_LEVEL = 1 +USER_AGENT = 'osc-examples' + +PARSER_DESCRIPTION = 'A demonstration framework' + +DEFAULT_IDENTITY_API_VERSION = '2.0' + +_logger = logging.getLogger(__name__) + +# --debug sets this True +dump_stack_trace = False + + +# Generally useful stuff often found in a utils module + +def env(*vars, **kwargs): + """Search for the first defined of possibly many env vars + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +# Common Example functions + +def base_parser(parser): + """Set up some of the common CLI options + + These are the basic options that match the library CLIs so + command-line/environment setups for those also work with these + demonstration programs. + + """ + + # Global arguments + parser.add_argument( + '--os-url', + metavar='', + default=env('OS_URL'), + help='Defaults to env[OS_URL]', + ) + parser.add_argument( + '--os-region-name', + metavar='', + default=env('OS_REGION_NAME'), + help='Authentication region name (Env: OS_REGION_NAME)', + ) + parser.add_argument( + '--os-cacert', + metavar='', + default=env('OS_CACERT'), + help='CA certificate bundle file (Env: OS_CACERT)', + ) + verify_group = parser.add_mutually_exclusive_group() + verify_group.add_argument( + '--verify', + action='store_true', + help='Verify server certificate (default)', + ) + verify_group.add_argument( + '--insecure', + action='store_true', + help='Disable server certificate verification', + ) + parser.add_argument( + '--timing', + default=False, + action='store_true', + help="Print API call timing info", + ) + parser.add_argument( + '-v', '--verbose', + action='count', + dest='verbose_level', + default=1, + help='Increase verbosity of output. Can be repeated.', + ) + parser.add_argument( + '--debug', + default=False, + action='store_true', + help='show tracebacks on errors', + ) + parser.add_argument( + 'rest', + nargs='*', + help='the rest of the args', + ) + return parser + + +def configure_logging(opts): + """Typical app logging setup + + Based on OSC/cliff + + """ + + global dump_stack_trace + + root_logger = logging.getLogger('') + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + requests_log.setLevel(logging.ERROR) + + # Other modules we don't want DEBUG output for so + # don't reset them below + iso8601_log = logging.getLogger("iso8601") + iso8601_log.setLevel(logging.ERROR) + + # Always send higher-level messages to the console via stderr + console = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter(CONSOLE_MESSAGE_FORMAT) + console.setFormatter(formatter) + root_logger.addHandler(console) + + # Set logging to the requested level + dump_stack_trace = False + if opts.verbose_level == 0: + # --quiet + root_logger.setLevel(logging.ERROR) + elif opts.verbose_level == 1: + # This is the default case, no --debug, --verbose or --quiet + root_logger.setLevel(logging.WARNING) + elif opts.verbose_level == 2: + # One --verbose + root_logger.setLevel(logging.INFO) + elif opts.verbose_level >= 3: + # Two or more --verbose + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + if opts.debug: + # --debug forces traceback + dump_stack_trace = True + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + return + + +def make_session(opts, **kwargs): + """Create our base session using simple auth from ksc plugins + + The arguments required in opts varies depending on the auth plugin + that is used. This example assumes Identity v2 will be used + and selects token auth if both os_url and os_token have been + provided, otherwise it uses password. + + :param Namespace opts: + A parser options Namespace containing the authentication + options to be used + :param dict kwargs: + Additional options passed directly to Session constructor + """ + + # If no auth type is named by the user, select one based on + # the supplied options + auth_plugin_name = auth.select_auth_plugin(opts) + + (auth_plugin, auth_params) = auth.build_auth_params( + auth_plugin_name, + opts, + ) + auth_p = auth_plugin.load_from_options(**auth_params) + + session = ksc_session.Session( + auth=auth_p, + **kwargs + ) + + return session + + +# Top-level functions + +def run(opts): + """Default run command""" + + # Do some basic testing here + sys.stdout.write("Default run command\n") + sys.stdout.write("Verbose level: %s\n" % opts.verbose_level) + sys.stdout.write("Debug: %s\n" % opts.debug) + sys.stdout.write("dump_stack_trace: %s\n" % dump_stack_trace) + + +def setup(): + """Parse command line and configure logging""" + opts = base_parser( + auth.build_auth_plugins_option_parser( + argparse.ArgumentParser(description='Object API Example') + ) + ).parse_args() + configure_logging(opts) + return opts + + +def main(opts, run): + try: + return run(opts) + except Exception as e: + if dump_stack_trace: + _logger.error(traceback.format_exc(e)) + else: + _logger.error('Exception raised: ' + str(e)) + return 1 + + +if __name__ == "__main__": + opts = setup() + sys.exit(main(opts, run)) diff --git a/functional/common/test.py b/functional/common/test.py index c1bb0b101a..464844fad1 100644 --- a/functional/common/test.py +++ b/functional/common/test.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import re import shlex import subprocess @@ -19,6 +20,11 @@ import six from functional.common import exceptions +COMMON_DIR = os.path.dirname(os.path.abspath(__file__)) +FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..')) +ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..')) +EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples') + def execute(cmd, action, flags='', params='', fail_ok=False, merge_stderr=False): diff --git a/functional/tests/test_examples.py b/functional/tests/test_examples.py new file mode 100644 index 0000000000..fdaa26b8d4 --- /dev/null +++ b/functional/tests/test_examples.py @@ -0,0 +1,22 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from functional.common import test + + +class ExampleTests(test.TestCase): + """Functional tests for running examples.""" + + def test_common(self): + # NOTE(stevemar): If an examples has a non-zero return + # code, then execute will raise an error by default. + test.execute('python', test.EXAMPLE_DIR + '/common.py --debug')