Skip to content

Commit 126b2c5

Browse files
Steve MartinelliDean Troyer
andcommitted
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 <dtroyer@gmail.com> Change-Id: Ie92b675eafd93482ddc9a8ce0b0588e23ed50c35
1 parent c55fdb6 commit 126b2c5

3 files changed

Lines changed: 292 additions & 0 deletions

File tree

examples/common.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python
2+
# common.py - Common bits for API examples
3+
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
"""
17+
API Examples
18+
19+
This is a collection of common functions used by the example scripts.
20+
It may also be run directly as a script to do basic testing of itself.
21+
22+
common.object_parser() provides the common set of command-line arguments
23+
used in the library CLIs for setting up authentication. This should make
24+
playing with the example scripts against a running OpenStack simpler.
25+
26+
common.configure_logging() provides the same basic logging control as
27+
the OSC shell.
28+
29+
common.make_session() does the minimal loading of a Keystone authentication
30+
plugin and creates a Keystone client Session.
31+
32+
"""
33+
34+
import argparse
35+
import logging
36+
import os
37+
import sys
38+
import traceback
39+
40+
from keystoneclient import session as ksc_session
41+
42+
from openstackclient.api import auth
43+
44+
45+
CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
46+
DEFAULT_VERBOSE_LEVEL = 1
47+
USER_AGENT = 'osc-examples'
48+
49+
PARSER_DESCRIPTION = 'A demonstration framework'
50+
51+
DEFAULT_IDENTITY_API_VERSION = '2.0'
52+
53+
_logger = logging.getLogger(__name__)
54+
55+
# --debug sets this True
56+
dump_stack_trace = False
57+
58+
59+
# Generally useful stuff often found in a utils module
60+
61+
def env(*vars, **kwargs):
62+
"""Search for the first defined of possibly many env vars
63+
64+
Returns the first environment variable defined in vars, or
65+
returns the default defined in kwargs.
66+
67+
"""
68+
for v in vars:
69+
value = os.environ.get(v, None)
70+
if value:
71+
return value
72+
return kwargs.get('default', '')
73+
74+
75+
# Common Example functions
76+
77+
def base_parser(parser):
78+
"""Set up some of the common CLI options
79+
80+
These are the basic options that match the library CLIs so
81+
command-line/environment setups for those also work with these
82+
demonstration programs.
83+
84+
"""
85+
86+
# Global arguments
87+
parser.add_argument(
88+
'--os-url',
89+
metavar='<url>',
90+
default=env('OS_URL'),
91+
help='Defaults to env[OS_URL]',
92+
)
93+
parser.add_argument(
94+
'--os-region-name',
95+
metavar='<auth-region-name>',
96+
default=env('OS_REGION_NAME'),
97+
help='Authentication region name (Env: OS_REGION_NAME)',
98+
)
99+
parser.add_argument(
100+
'--os-cacert',
101+
metavar='<ca-bundle-file>',
102+
default=env('OS_CACERT'),
103+
help='CA certificate bundle file (Env: OS_CACERT)',
104+
)
105+
verify_group = parser.add_mutually_exclusive_group()
106+
verify_group.add_argument(
107+
'--verify',
108+
action='store_true',
109+
help='Verify server certificate (default)',
110+
)
111+
verify_group.add_argument(
112+
'--insecure',
113+
action='store_true',
114+
help='Disable server certificate verification',
115+
)
116+
parser.add_argument(
117+
'--timing',
118+
default=False,
119+
action='store_true',
120+
help="Print API call timing info",
121+
)
122+
parser.add_argument(
123+
'-v', '--verbose',
124+
action='count',
125+
dest='verbose_level',
126+
default=1,
127+
help='Increase verbosity of output. Can be repeated.',
128+
)
129+
parser.add_argument(
130+
'--debug',
131+
default=False,
132+
action='store_true',
133+
help='show tracebacks on errors',
134+
)
135+
parser.add_argument(
136+
'rest',
137+
nargs='*',
138+
help='the rest of the args',
139+
)
140+
return parser
141+
142+
143+
def configure_logging(opts):
144+
"""Typical app logging setup
145+
146+
Based on OSC/cliff
147+
148+
"""
149+
150+
global dump_stack_trace
151+
152+
root_logger = logging.getLogger('')
153+
154+
# Requests logs some stuff at INFO that we don't want
155+
# unless we have DEBUG
156+
requests_log = logging.getLogger("requests")
157+
requests_log.setLevel(logging.ERROR)
158+
159+
# Other modules we don't want DEBUG output for so
160+
# don't reset them below
161+
iso8601_log = logging.getLogger("iso8601")
162+
iso8601_log.setLevel(logging.ERROR)
163+
164+
# Always send higher-level messages to the console via stderr
165+
console = logging.StreamHandler(sys.stderr)
166+
formatter = logging.Formatter(CONSOLE_MESSAGE_FORMAT)
167+
console.setFormatter(formatter)
168+
root_logger.addHandler(console)
169+
170+
# Set logging to the requested level
171+
dump_stack_trace = False
172+
if opts.verbose_level == 0:
173+
# --quiet
174+
root_logger.setLevel(logging.ERROR)
175+
elif opts.verbose_level == 1:
176+
# This is the default case, no --debug, --verbose or --quiet
177+
root_logger.setLevel(logging.WARNING)
178+
elif opts.verbose_level == 2:
179+
# One --verbose
180+
root_logger.setLevel(logging.INFO)
181+
elif opts.verbose_level >= 3:
182+
# Two or more --verbose
183+
root_logger.setLevel(logging.DEBUG)
184+
requests_log.setLevel(logging.DEBUG)
185+
186+
if opts.debug:
187+
# --debug forces traceback
188+
dump_stack_trace = True
189+
root_logger.setLevel(logging.DEBUG)
190+
requests_log.setLevel(logging.DEBUG)
191+
192+
return
193+
194+
195+
def make_session(opts, **kwargs):
196+
"""Create our base session using simple auth from ksc plugins
197+
198+
The arguments required in opts varies depending on the auth plugin
199+
that is used. This example assumes Identity v2 will be used
200+
and selects token auth if both os_url and os_token have been
201+
provided, otherwise it uses password.
202+
203+
:param Namespace opts:
204+
A parser options Namespace containing the authentication
205+
options to be used
206+
:param dict kwargs:
207+
Additional options passed directly to Session constructor
208+
"""
209+
210+
# If no auth type is named by the user, select one based on
211+
# the supplied options
212+
auth_plugin_name = auth.select_auth_plugin(opts)
213+
214+
(auth_plugin, auth_params) = auth.build_auth_params(
215+
auth_plugin_name,
216+
opts,
217+
)
218+
auth_p = auth_plugin.load_from_options(**auth_params)
219+
220+
session = ksc_session.Session(
221+
auth=auth_p,
222+
**kwargs
223+
)
224+
225+
return session
226+
227+
228+
# Top-level functions
229+
230+
def run(opts):
231+
"""Default run command"""
232+
233+
# Do some basic testing here
234+
sys.stdout.write("Default run command\n")
235+
sys.stdout.write("Verbose level: %s\n" % opts.verbose_level)
236+
sys.stdout.write("Debug: %s\n" % opts.debug)
237+
sys.stdout.write("dump_stack_trace: %s\n" % dump_stack_trace)
238+
239+
240+
def setup():
241+
"""Parse command line and configure logging"""
242+
opts = base_parser(
243+
auth.build_auth_plugins_option_parser(
244+
argparse.ArgumentParser(description='Object API Example')
245+
)
246+
).parse_args()
247+
configure_logging(opts)
248+
return opts
249+
250+
251+
def main(opts, run):
252+
try:
253+
return run(opts)
254+
except Exception as e:
255+
if dump_stack_trace:
256+
_logger.error(traceback.format_exc(e))
257+
else:
258+
_logger.error('Exception raised: ' + str(e))
259+
return 1
260+
261+
262+
if __name__ == "__main__":
263+
opts = setup()
264+
sys.exit(main(opts, run))

functional/common/test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
import os
1314
import re
1415
import shlex
1516
import subprocess
@@ -19,6 +20,11 @@
1920

2021
from functional.common import exceptions
2122

23+
COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
24+
FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..'))
25+
ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..'))
26+
EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples')
27+
2228

2329
def execute(cmd, action, flags='', params='', fail_ok=False,
2430
merge_stderr=False):

functional/tests/test_examples.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from functional.common import test
14+
15+
16+
class ExampleTests(test.TestCase):
17+
"""Functional tests for running examples."""
18+
19+
def test_common(self):
20+
# NOTE(stevemar): If an examples has a non-zero return
21+
# code, then execute will raise an error by default.
22+
test.execute('python', test.EXAMPLE_DIR + '/common.py --debug')

0 commit comments

Comments
 (0)