diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..085b7b9 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = gbpclient +omit = gbpclient/openstack/*,gbpclient/tests/* + +[report] +ignore-errors = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bbb8b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*.pyc +*.DS_Store +*.egg +*.sw? +AUTHORS +ChangeLog +build/* +build-stamp +cover/* +doc/build/ +doc/source/api/ +python_group_based_policy_client.egg-info/* +gbp/vcsversion.py +gbpclient/versioninfo +run_tests.err.log +run_tests.log +.autogenerated +.coverage +.testrepository/ +.tox/ +.venv/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..1e24c6f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,39 @@ +# The format of this file isn't really documented; just use --generate-rcfile +[MASTER] +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +ignore=test + +[Messages Control] +# NOTE(justinsb): We might want to have a 2nd strict pylintrc in future +# C0111: Don't require docstrings on every method +# W0511: TODOs in code comments are fine. +# W0142: *args and **kwargs are fine. +# W0622: Redefining id is fine. +disable=C0111,W0511,W0142,W0622 + +[Basic] +# Variable names can be 1 to 31 characters long, with lowercase and underscores +variable-rgx=[a-z_][a-z0-9_]{0,30}$ + +# Argument names can be 2 to 31 characters long, with lowercase and underscores +argument-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Method names should be at least 3 characters long +# and be lowecased with underscores +method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ + +# Don't require docstrings on tests. +no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ + +[Design] +max-public-methods=100 +min-public-methods=0 +max-args=6 + +[Variables] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +# _ is used by our localization +additional-builtins=_ diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..d152a5a --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..22da404 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps documented at: + + https://wiki.openstack.org/wiki/How_To_Contribute#If_you.27re_a_developer + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + https://wiki.openstack.org/GerritWorkflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-group-based-policy-client diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c0f014e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include tox.ini +include LICENSE README.rst HACKING.rst +include AUTHORS +include ChangeLog +include tools/* +recursive-include tests * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..018ae84 --- /dev/null +++ b/README.rst @@ -0,0 +1 @@ +This is the client API library for Group Based Policy. diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..c463060 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# + +project = 'python-group-based-policy-client' + +# -- General configuration --------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'oslosphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +copyright = u'OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output --------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme = 'nature' + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, +# documentclass [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..adb9642 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,41 @@ +Python bindings to the Group Based Policy API +============================================= + +In order to use the python group-based-policy- client directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: + + >>> import logging + >>> from gbpclient.gbp import client + >>> logging.basicConfig(level=logging.DEBUG) + >>> gbp = client.Client('2.0', endpoint_url=OS_URL, token=OS_TOKEN) + >>> gbp.format = 'json' + >>> ptg = {'name': 'my_ptg'} + >>> gbp.create_policy_target_group({'policy_target_group':ptg}) + >>> policy_target_groups = gbp.list_policy_target_groups(name='my_ptg') + >>> print policy_target_groups + >>> ptg_id = policy_target_groups['policy_target_groups'][0]['id'] + >>> gbp.delete_policy_target_group(ptg_id) + + +Command-line Tool +================= +In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-tenant-name``, and ``--os-auth-url``) or set them in environment variables:: + + export OS_USERNAME=user + export OS_PASSWORD=pass + export OS_TENANT_NAME=tenant + export OS_AUTH_URL=http://auth.example.com:5000/v2.0 + +The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-url`` and ``--os-auth-token``. You can alternatively set these environment variables:: + + export OS_URL=http://neutron.example.org:9696/ + export OS_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 + +If neutron server does not require authentication, besides these two arguments or environment variables (We can use any value as token.), we need manually supply ``--os-auth-strategy`` or set the environment variable:: + + export OS_AUTH_STRATEGY=noauth + +Once you've configured your authentication parameters, you can run ``gbp -h`` to see a complete listing of available commands. + +Release Notes +============= + diff --git a/gbp_test.sh b/gbp_test.sh new file mode 100755 index 0000000..4adfe37 --- /dev/null +++ b/gbp_test.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -x +function die() { + local exitcode=$? + set +o xtrace + echo $@ + cleanup + exit $exitcode +} + +ptg_name=myptg1 +## TODO Sumit: Test for other resources as well after renaming +function cleanup() { + echo Removing test ptg... + gbp endpointgroup-delete ptg_name +} + +noauth_tenant_id=me +if [ "$1" == "noauth" ]; then + NOAUTH="--tenant_id $noauth_tenant_id" +else + NOAUTH= +fi + +echo "NOTE: User should be admin in order to perform all operations." +sleep 3 + +FORMAT=" --request-format xml" + +# test the CRUD of network +ptg=$ptg_name +gbp endpointgroup-create $FORMAT $NOAUTH $ptg || die "fail to create ptg $ptg" +temp=`gbp endpointgroup-list $FORMAT -- --name $ptg --fields id | wc -l` +echo $temp +if [ $temp -ne 5 ]; then + die "PTGs with name $ptg is not unique or found" +fi +ptg_id=`gbp gbp-list -- --name $ptg --fields id | tail -n 2 | head -n 1 | cut -d' ' -f 2` +echo "ID of PTG with name $ptg is $ptg_id" + +gbp endpointgroup-show $FORMAT $ptg || die "fail to show PTG $ptg" +gbp endpointgroup-show $FORMAT $ptg_id || die "fail to show PTG $ptg_id" + +gbp endpointgroup-update $FORMAT $ptg --description "desc" || die "fail to update PTG $ptg" +gbp endpointgroup-update $FORMAT $ptg_id --description "new" || die "fail to update PTG $ptg_id" + +gbp endpointgroup-list $FORMAT -c id -- --id fakeid || die "fail to list PTGs with column selection on empty list" + +cleanup +echo "Success! :)" + diff --git a/gbpclient/__init__.py b/gbpclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gbpclient/tests/__init__.py b/gbpclient/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gbpclient/tests/unit/__init__.py b/gbpclient/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gbpclient/tests/unit/test_auth.py b/gbpclient/tests/unit/test_auth.py new file mode 100644 index 0000000..5561ecc --- /dev/null +++ b/gbpclient/tests/unit/test_auth.py @@ -0,0 +1,567 @@ +# Copyright 2012 NEC Corporation +# All Rights Reserved +# +# 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. +# + +import json +import uuid + +import fixtures +import httpretty +from mox3 import mox +import requests +import six +import testtools + +from keystoneclient.auth.identity import v2 as ks_v2_auth +from keystoneclient.auth.identity import v3 as ks_v3_auth +from keystoneclient import exceptions as ks_exceptions +from keystoneclient.fixture import v2 as ks_v2_fixture +from keystoneclient.fixture import v3 as ks_v3_fixture +from keystoneclient import session + +from neutronclient import client +from neutronclient.common import exceptions +from neutronclient.common import utils +from neutronclient.openstack.common import jsonutils + + +USERNAME = 'testuser' +USER_ID = 'testuser_id' +TENANT_NAME = 'testtenant' +TENANT_ID = 'testtenant_id' +PASSWORD = 'password' +ENDPOINT_URL = 'localurl' +PUBLIC_ENDPOINT_URL = 'public_%s' % ENDPOINT_URL +ADMIN_ENDPOINT_URL = 'admin_%s' % ENDPOINT_URL +INTERNAL_ENDPOINT_URL = 'internal_%s' % ENDPOINT_URL +ENDPOINT_OVERRIDE = 'otherurl' +TOKEN = 'tokentoken' +TOKENID = uuid.uuid4().hex +REGION = 'RegionOne' +NOAUTH = 'noauth' + +KS_TOKEN_RESULT = { + 'access': { + 'token': {'id': TOKEN, + 'expires': '2012-08-11T07:49:01Z', + 'tenant': {'id': str(uuid.uuid1())}}, + 'user': {'id': str(uuid.uuid1())}, + 'serviceCatalog': [ + {'endpoints_links': [], + 'endpoints': [{'adminURL': ENDPOINT_URL, + 'internalURL': ENDPOINT_URL, + 'publicURL': ENDPOINT_URL, + 'region': REGION}], + 'type': 'network', + 'name': 'Neutron Service'} + ] + } +} + +ENDPOINTS_RESULT = { + 'endpoints': [{ + 'type': 'network', + 'name': 'Neutron Service', + 'region': REGION, + 'adminURL': ENDPOINT_URL, + 'internalURL': ENDPOINT_URL, + 'publicURL': ENDPOINT_URL + }] +} + +BASE_HOST = 'http://keystone.example.com' +BASE_URL = "%s:5000/" % BASE_HOST +UPDATED = '2013-03-06T00:00:00Z' + +# FIXME (bklei): A future release of keystoneclient will support +# a discovery fixture which can replace these constants and clean +# this up. +V2_URL = "%sv2.0" % BASE_URL +V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/' + 'openstack-identity-service/2.0/content/', + 'rel': 'describedby', + 'type': 'text/html'} + +V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident' + 'ity-service/2.0/identity-dev-guide-2.0.pdf', + 'rel': 'describedby', + 'type': 'application/pdf'} + +V2_VERSION = {'id': 'v2.0', + 'links': [{'href': V2_URL, 'rel': 'self'}, + V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], + 'status': 'stable', + 'updated': UPDATED} + +V3_URL = "%sv3" % BASE_URL +V3_MEDIA_TYPES = [{'base': 'application/json', + 'type': 'application/vnd.openstack.identity-v3+json'}, + {'base': 'application/xml', + 'type': 'application/vnd.openstack.identity-v3+xml'}] + +V3_VERSION = {'id': 'v3.0', + 'links': [{'href': V3_URL, 'rel': 'self'}], + 'media-types': V3_MEDIA_TYPES, + 'status': 'stable', + 'updated': UPDATED} + + +def _create_version_entry(version): + return jsonutils.dumps({'version': version}) + + +def _create_version_list(versions): + return jsonutils.dumps({'versions': {'values': versions}}) + + +V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) +V3_VERSION_ENTRY = _create_version_entry(V3_VERSION) +V2_VERSION_ENTRY = _create_version_entry(V2_VERSION) + + +def get_response(status_code, headers=None): + response = mox.Mox().CreateMock(requests.Response) + response.headers = headers or {} + response.status_code = status_code + return response + + +def setup_keystone_v2(): + v2_token = ks_v2_fixture.Token(token_id=TOKENID) + service = v2_token.add_service('network') + service.add_endpoint(PUBLIC_ENDPOINT_URL, region=REGION) + + httpretty.register_uri(httpretty.POST, + '%s/tokens' % (V2_URL), + body=json.dumps(v2_token)) + + auth_session = session.Session() + auth_plugin = ks_v2_auth.Password(V2_URL, 'xx', 'xx') + return auth_session, auth_plugin + + +def setup_keystone_v3(): + httpretty.register_uri(httpretty.GET, + V3_URL, + body=V3_VERSION_ENTRY) + + v3_token = ks_v3_fixture.Token() + service = v3_token.add_service('network') + service.add_standard_endpoints(public=PUBLIC_ENDPOINT_URL, + admin=ADMIN_ENDPOINT_URL, + internal=INTERNAL_ENDPOINT_URL, + region=REGION) + + httpretty.register_uri(httpretty.POST, + '%s/auth/tokens' % (V3_URL), + body=json.dumps(v3_token), + adding_headers={'X-Subject-Token': TOKENID}) + + auth_session = session.Session() + auth_plugin = ks_v3_auth.Password(V3_URL, + username='xx', + user_id='xx', + user_domain_name='xx', + user_domain_id='xx') + return auth_session, auth_plugin + + +AUTH_URL = V2_URL + + +class CLITestAuthNoAuth(testtools.TestCase): + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthNoAuth, self).setUp() + self.mox = mox.Mox() + self.client = client.HTTPClient(username=USERNAME, + tenant_name=TENANT_NAME, + password=PASSWORD, + endpoint_url=ENDPOINT_URL, + auth_strategy=NOAUTH, + region_name=REGION) + self.addCleanup(self.mox.VerifyAll) + self.addCleanup(self.mox.UnsetStubs) + + def test_get_noauth(self): + self.mox.StubOutWithMock(self.client, "request") + + res200 = get_response(200) + + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.IsA(dict), + ).AndReturn((res200, '')) + self.mox.ReplayAll() + + self.client.do_request('/resource', 'GET') + self.assertEqual(self.client.endpoint_url, ENDPOINT_URL) + + +class CLITestAuthKeystone(testtools.TestCase): + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthKeystone, self).setUp() + self.mox = mox.Mox() + + for var in ('http_proxy', 'HTTP_PROXY'): + self.useFixture(fixtures.EnvironmentVariableFixture(var)) + + self.client = client.construct_http_client( + username=USERNAME, + tenant_name=TENANT_NAME, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) + + self.addCleanup(self.mox.VerifyAll) + self.addCleanup(self.mox.UnsetStubs) + + def test_reused_token_get_auth_info(self): + """Test that Client.get_auth_info() works even if client was + instantiated with predefined token. + """ + client_ = client.HTTPClient(username=USERNAME, + tenant_name=TENANT_NAME, + token=TOKEN, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) + expected = {'auth_token': TOKEN, + 'auth_tenant_id': None, + 'auth_user_id': None, + 'endpoint_url': self.client.endpoint_url} + self.assertEqual(client_.get_auth_info(), expected) + + @httpretty.activate + def test_get_token(self): + auth_session, auth_plugin = setup_keystone_v2() + + self.client = client.construct_http_client( + username=USERNAME, + tenant_name=TENANT_NAME, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION, + session=auth_session, + auth=auth_plugin) + + self.mox.StubOutWithMock(self.client, "request") + res200 = get_response(200) + + self.client.request( + '/resource', 'GET', + authenticated=True + ).AndReturn((res200, '')) + + self.mox.ReplayAll() + + self.client.do_request('/resource', 'GET') + + def test_refresh_token(self): + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + self.client.endpoint_url = ENDPOINT_URL + + res200 = get_response(200) + res401 = get_response(401) + + # If a token is expired, neutron server retruns 401 + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res401, '')) + self.client.request( + AUTH_URL + '/tokens', 'POST', + body=mox.IsA(str), headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + def test_refresh_token_no_auth_url(self): + self.mox.StubOutWithMock(self.client, "request") + self.client.auth_url = None + + self.client.auth_token = TOKEN + self.client.endpoint_url = ENDPOINT_URL + + res401 = get_response(401) + + # If a token is expired, neutron server returns 401 + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res401, '')) + self.mox.ReplayAll() + self.assertRaises(exceptions.NoAuthURLProvided, + self.client.do_request, + '/resource', + 'GET') + + def test_get_endpoint_url_with_invalid_auth_url(self): + # Handle the case when auth_url is not provided + self.client.auth_url = None + self.assertRaises(exceptions.NoAuthURLProvided, + self.client._get_endpoint_url) + + def test_get_endpoint_url(self): + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + + res200 = get_response(200) + + self.client.request( + mox.StrContains(AUTH_URL + '/tokens/%s/endpoints' % TOKEN), 'GET', + headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(ENDPOINTS_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + def test_use_given_endpoint_url(self): + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, + endpoint_url=ENDPOINT_OVERRIDE) + self.assertEqual(self.client.endpoint_url, ENDPOINT_OVERRIDE) + + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + res200 = get_response(200) + + self.client.request( + mox.StrContains(ENDPOINT_OVERRIDE + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + self.assertEqual(self.client.endpoint_url, ENDPOINT_OVERRIDE) + + def test_get_endpoint_url_other(self): + self.client = client.HTTPClient( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='otherURL') + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + res200 = get_response(200) + + self.client.request( + mox.StrContains(AUTH_URL + '/tokens/%s/endpoints' % TOKEN), 'GET', + headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(ENDPOINTS_RESULT))) + self.mox.ReplayAll() + self.assertRaises(exceptions.EndpointTypeNotFound, + self.client.do_request, + '/resource', + 'GET') + + def test_get_endpoint_url_failed(self): + self.mox.StubOutWithMock(self.client, "request") + + self.client.auth_token = TOKEN + + res200 = get_response(200) + res401 = get_response(401) + + self.client.request( + mox.StrContains(AUTH_URL + '/tokens/%s/endpoints' % TOKEN), 'GET', + headers=mox.IsA(dict) + ).AndReturn((res401, '')) + self.client.request( + AUTH_URL + '/tokens', 'POST', + body=mox.IsA(str), headers=mox.IsA(dict) + ).AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) + self.client.request( + mox.StrContains(ENDPOINT_URL + '/resource'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', TOKEN) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + @httpretty.activate + def test_endpoint_type(self): + auth_session, auth_plugin = setup_keystone_v3() + + # Test default behavior is to choose public. + self.client = client.construct_http_client( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, + session=auth_session, auth=auth_plugin) + + self.client.authenticate() + self.assertEqual(self.client.endpoint_url, PUBLIC_ENDPOINT_URL) + + # Test admin url + self.client = client.construct_http_client( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='adminURL', + session=auth_session, auth=auth_plugin) + + self.client.authenticate() + self.assertEqual(self.client.endpoint_url, ADMIN_ENDPOINT_URL) + + # Test public url + self.client = client.construct_http_client( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='publicURL', + session=auth_session, auth=auth_plugin) + + self.client.authenticate() + self.assertEqual(self.client.endpoint_url, PUBLIC_ENDPOINT_URL) + + # Test internal url + self.client = client.construct_http_client( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='internalURL', + session=auth_session, auth=auth_plugin) + + self.client.authenticate() + self.assertEqual(self.client.endpoint_url, INTERNAL_ENDPOINT_URL) + + # Test url that isn't found in the service catalog + self.client = client.construct_http_client( + username=USERNAME, tenant_name=TENANT_NAME, password=PASSWORD, + auth_url=AUTH_URL, region_name=REGION, endpoint_type='privateURL', + session=auth_session, auth=auth_plugin) + + self.assertRaises( + ks_exceptions.EndpointNotFound, + self.client.authenticate) + + def test_strip_credentials_from_log(self): + def verify_no_credentials(kwargs): + return ('REDACTED' in kwargs['body']) and ( + self.client.password not in kwargs['body']) + + def verify_credentials(body): + return 'REDACTED' not in body and self.client.password in body + + self.mox.StubOutWithMock(self.client, "request") + self.mox.StubOutWithMock(utils, "http_log_req") + + res200 = get_response(200) + + utils.http_log_req(mox.IgnoreArg(), mox.IgnoreArg(), mox.Func( + verify_no_credentials)) + self.client.request( + mox.IsA(six.string_types), mox.IsA(six.string_types), + body=mox.Func(verify_credentials), + headers=mox.IgnoreArg() + ).AndReturn((res200, json.dumps(KS_TOKEN_RESULT))) + utils.http_log_req(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) + self.client.request( + mox.IsA(six.string_types), mox.IsA(six.string_types), + headers=mox.IsA(dict) + ).AndReturn((res200, '')) + self.mox.ReplayAll() + + self.client.do_request('/resource', 'GET') + + +class CLITestAuthKeystoneWithId(CLITestAuthKeystone): + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthKeystoneWithId, self).setUp() + self.client = client.HTTPClient(user_id=USER_ID, + tenant_id=TENANT_ID, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) + + +class CLITestAuthKeystoneWithIdandName(CLITestAuthKeystone): + + def setUp(self): + """Prepare the test environment.""" + super(CLITestAuthKeystoneWithIdandName, self).setUp() + self.client = client.HTTPClient(username=USERNAME, + user_id=USER_ID, + tenant_id=TENANT_ID, + tenant_name=TENANT_NAME, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION) + + +class TestKeystoneClientVersions(testtools.TestCase): + + def setUp(self): + """Prepare the test environment.""" + super(TestKeystoneClientVersions, self).setUp() + self.mox = mox.Mox() + self.addCleanup(self.mox.VerifyAll) + self.addCleanup(self.mox.UnsetStubs) + + @httpretty.activate + def test_v2_auth(self): + auth_session, auth_plugin = setup_keystone_v2() + res200 = get_response(200) + + self.client = client.construct_http_client( + username=USERNAME, + tenant_name=TENANT_NAME, + password=PASSWORD, + auth_url=AUTH_URL, + region_name=REGION, + session=auth_session, + auth=auth_plugin) + + self.mox.StubOutWithMock(self.client, "request") + + self.client.request( + '/resource', 'GET', + authenticated=True + ).AndReturn((res200, '')) + + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') + + @httpretty.activate + def test_v3_auth(self): + auth_session, auth_plugin = setup_keystone_v3() + res200 = get_response(200) + + self.client = client.construct_http_client( + user_id=USER_ID, + tenant_id=TENANT_ID, + password=PASSWORD, + auth_url=V3_URL, + region_name=REGION, + session=auth_session, + auth=auth_plugin) + + self.mox.StubOutWithMock(self.client, "request") + + self.client.request( + '/resource', 'GET', + authenticated=True + ).AndReturn((res200, '')) + + self.mox.ReplayAll() + self.client.do_request('/resource', 'GET') diff --git a/gbpclient/tests/unit/test_cli20.py b/gbpclient/tests/unit/test_cli20.py new file mode 100644 index 0000000..38b5aa1 --- /dev/null +++ b/gbpclient/tests/unit/test_cli20.py @@ -0,0 +1,766 @@ +# Copyright 2012 OpenStack Foundation. +# All Rights Reserved +# +# 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. +# + +import contextlib +import itertools +import sys +import urllib + +import fixtures +from mox3 import mox +from oslotest import base +import requests +import six +import six.moves.urllib.parse as urlparse + +from neutronclient.common import constants +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronV2_0 +from neutronclient import shell +from neutronclient.v2_0 import client + +API_VERSION = "2.0" +FORMAT = 'json' +TOKEN = 'testtoken' +ENDURL = 'localurl' + + +@contextlib.contextmanager +def capture_std_streams(): + fake_stdout, fake_stderr = six.StringIO(), six.StringIO() + stdout, stderr = sys.stdout, sys.stderr + try: + sys.stdout, sys.stderr = fake_stdout, fake_stderr + yield fake_stdout, fake_stderr + finally: + sys.stdout, sys.stderr = stdout, stderr + + +class FakeStdout: + + def __init__(self): + self.content = [] + + def write(self, text): + self.content.append(text) + + def make_string(self): + result = '' + for line in self.content: + result = result + line + return result + + +class MyResp(object): + def __init__(self, status_code, headers=None, reason=None): + self.status_code = status_code + self.headers = headers or {} + self.reason = reason + + +class MyApp(object): + def __init__(self, _stdout): + self.stdout = _stdout + + +def end_url(path, query=None, format=FORMAT): + _url_str = ENDURL + "/v" + API_VERSION + path + "." + format + return query and _url_str + "?" + query or _url_str + + +class MyUrlComparator(mox.Comparator): + def __init__(self, lhs, client): + self.lhs = lhs + self.client = client + + def equals(self, rhs): + lhsp = urlparse.urlparse(self.lhs) + rhsp = urlparse.urlparse(rhs) + + return (lhsp.scheme == rhsp.scheme and + lhsp.netloc == rhsp.netloc and + lhsp.path == rhsp.path and + urlparse.parse_qs(lhsp.query) == urlparse.parse_qs(rhsp.query)) + + def __str__(self): + if self.client and self.client.format != FORMAT: + lhs_parts = self.lhs.split("?", 1) + if len(lhs_parts) == 2: + lhs = ("%s.%s?%s" % (lhs_parts[0][:-4], + self.client.format, + lhs_parts[1])) + else: + lhs = ("%s.%s" % (lhs_parts[0][:-4], + self.client.format)) + return lhs + return self.lhs + + def __repr__(self): + return str(self) + + +class MyComparator(mox.Comparator): + def __init__(self, lhs, client): + self.lhs = lhs + self.client = client + + def _com_dict(self, lhs, rhs): + if len(lhs) != len(rhs): + return False + for key, value in six.iteritems(lhs): + if key not in rhs: + return False + rhs_value = rhs[key] + if not self._com(value, rhs_value): + return False + return True + + def _com_list(self, lhs, rhs): + if len(lhs) != len(rhs): + return False + for lhs_value in lhs: + if lhs_value not in rhs: + return False + return True + + def _com(self, lhs, rhs): + if lhs is None: + return rhs is None + if isinstance(lhs, dict): + if not isinstance(rhs, dict): + return False + return self._com_dict(lhs, rhs) + if isinstance(lhs, list): + if not isinstance(rhs, list): + return False + return self._com_list(lhs, rhs) + if isinstance(lhs, tuple): + if not isinstance(rhs, tuple): + return False + return self._com_list(lhs, rhs) + return lhs == rhs + + def equals(self, rhs): + if self.client: + rhs = self.client.deserialize(rhs, 200) + return self._com(self.lhs, rhs) + + def __repr__(self): + if self.client: + return self.client.serialize(self.lhs) + return str(self.lhs) + + +class CLITestV20Base(base.BaseTestCase): + + format = 'json' + test_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + id_field = 'id' + + def _find_resourceid(self, client, resource, name_or_id, + cmd_resource=None, parent_id=None): + return name_or_id + + def _get_attr_metadata(self): + return self.metadata + client.Client.EXTED_PLURALS.update(constants.PLURALS) + client.Client.EXTED_PLURALS.update({'tags': 'tag'}) + return {'plurals': client.Client.EXTED_PLURALS, + 'xmlns': constants.XML_NS_V20, + constants.EXT_NS: {'prefix': 'http://xxxx.yy.com'}} + + def setUp(self, plurals=None): + """Prepare the test environment.""" + super(CLITestV20Base, self).setUp() + client.Client.EXTED_PLURALS.update(constants.PLURALS) + if plurals is not None: + client.Client.EXTED_PLURALS.update(plurals) + self.metadata = {'plurals': client.Client.EXTED_PLURALS, + 'xmlns': constants.XML_NS_V20, + constants.EXT_NS: {'prefix': + 'http://xxxx.yy.com'}} + self.mox = mox.Mox() + self.endurl = ENDURL + self.fake_stdout = FakeStdout() + self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.fake_stdout)) + self.useFixture(fixtures.MonkeyPatch( + 'neutronclient.neutron.v2_0.find_resourceid_by_name_or_id', + self._find_resourceid)) + self.useFixture(fixtures.MonkeyPatch( + 'neutronclient.neutron.v2_0.find_resourceid_by_id', + self._find_resourceid)) + self.useFixture(fixtures.MonkeyPatch( + 'neutronclient.v2_0.client.Client.get_attr_metadata', + self._get_attr_metadata)) + self.client = client.Client(token=TOKEN, endpoint_url=self.endurl) + + def _test_create_resource(self, resource, cmd, name, myid, args, + position_names, position_values, + tenant_id=None, tags=None, admin_state_up=True, + extra_body=None, cmd_resource=None, + parent_id=None, **kwargs): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + non_admin_status_resources = ['subnet', 'floatingip', 'security_group', + 'security_group_rule', 'qos_queue', + 'network_gateway', 'gateway_device', + 'credential', 'network_profile', + 'policy_profile', 'ikepolicy', + 'ipsecpolicy', 'metering_label', + 'metering_label_rule', 'net_partition'] + if not cmd_resource: + cmd_resource = resource + if (resource in non_admin_status_resources): + body = {resource: {}, } + else: + body = {resource: {'admin_state_up': admin_state_up, }, } + if tenant_id: + body[resource].update({'tenant_id': tenant_id}) + if tags: + body[resource].update({'tags': tags}) + if extra_body: + body[resource].update(extra_body) + body[resource].update(kwargs) + + for i in range(len(position_names)): + body[resource].update({position_names[i]: position_values[i]}) + ress = {resource: + {self.id_field: myid}, } + if name: + ress[resource].update({'name': name}) + self.client.format = self.format + resstr = self.client.serialize(ress) + # url method body + resource_plural = neutronV2_0._get_resource_plural(cmd_resource, + self.client) + path = getattr(self.client, resource_plural + "_path") + if parent_id: + path = path % parent_id + # Work around for LP #1217791. XML deserializer called from + # MyComparator does not decodes XML string correctly. + if self.format == 'json': + mox_body = MyComparator(body, self.client) + else: + mox_body = self.client.serialize(body) + self.client.httpclient.request( + end_url(path, format=self.format), 'POST', + body=mox_body, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser('create_' + resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertIn(myid, _str) + if name: + self.assertIn(name, _str) + + def _test_list_columns(self, cmd, resources, + resources_out, args=('-f', 'json'), + cmd_resources=None, parent_id=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + self.client.format = self.format + if not cmd_resources: + cmd_resources = resources + + resstr = self.client.serialize(resources_out) + + path = getattr(self.client, cmd_resources + "_path") + if parent_id: + path = path % parent_id + self.client.httpclient.request( + end_url(path, format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + args = tuple(args) + ('--request-format', self.format) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + cmd_resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_list_resources(self, resources, cmd, detail=False, tags=(), + fields_1=(), fields_2=(), page_size=None, + sort_key=(), sort_dir=(), response_contents=None, + base_args=None, path=None, cmd_resources=None, + parent_id=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resources: + cmd_resources = resources + if response_contents is None: + contents = [{self.id_field: 'myid1', }, + {self.id_field: 'myid2', }, ] + else: + contents = response_contents + reses = {resources: contents} + self.client.format = self.format + resstr = self.client.serialize(reses) + # url method body + query = "" + args = base_args if base_args is not None else [] + if detail: + args.append('-D') + args.extend(['--request-format', self.format]) + if fields_1: + for field in fields_1: + args.append('--fields') + args.append(field) + + if tags: + args.append('--') + args.append("--tag") + for tag in tags: + args.append(tag) + if isinstance(tag, unicode): + tag = urllib.quote(tag.encode('utf-8')) + if query: + query += "&tag=" + tag + else: + query = "tag=" + tag + if (not tags) and fields_2: + args.append('--') + if fields_2: + args.append("--fields") + for field in fields_2: + args.append(field) + if detail: + query = query and query + '&verbose=True' or 'verbose=True' + for field in itertools.chain(fields_1, fields_2): + if query: + query += "&fields=" + field + else: + query = "fields=" + field + if page_size: + args.append("--page-size") + args.append(str(page_size)) + if query: + query += "&limit=%s" % page_size + else: + query = "limit=%s" % page_size + if sort_key: + for key in sort_key: + args.append('--sort-key') + args.append(key) + if query: + query += '&' + query += 'sort_key=%s' % key + if sort_dir: + len_diff = len(sort_key) - len(sort_dir) + if len_diff > 0: + sort_dir = tuple(sort_dir) + ('asc',) * len_diff + elif len_diff < 0: + sort_dir = sort_dir[:len(sort_key)] + for dir in sort_dir: + args.append('--sort-dir') + args.append(dir) + if query: + query += '&' + query += 'sort_dir=%s' % dir + if path is None: + path = getattr(self.client, cmd_resources + "_path") + if parent_id: + path = path % parent_id + self.client.httpclient.request( + MyUrlComparator(end_url(path, query, format=self.format), + self.client), + 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + cmd_resources) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + if response_contents is None: + self.assertIn('myid1', _str) + return _str + + def _test_list_resources_with_pagination(self, resources, cmd, + cmd_resources=None, + parent_id=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resources: + cmd_resources = resources + + path = getattr(self.client, cmd_resources + "_path") + if parent_id: + path = path % parent_id + fake_query = "marker=myid2&limit=2" + reses1 = {resources: [{'id': 'myid1', }, + {'id': 'myid2', }], + '%s_links' % resources: [{'href': end_url(path, fake_query), + 'rel': 'next'}]} + reses2 = {resources: [{'id': 'myid3', }, + {'id': 'myid4', }]} + self.client.format = self.format + resstr1 = self.client.serialize(reses1) + resstr2 = self.client.serialize(reses2) + self.client.httpclient.request( + end_url(path, "", format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr1)) + self.client.httpclient.request( + MyUrlComparator(end_url(path, fake_query, format=self.format), + self.client), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr2)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_" + cmd_resources) + args = ['--request-format', self.format] + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_update_resource(self, resource, cmd, myid, args, extrafields, + cmd_resource=None, parent_id=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resource: + cmd_resource = resource + + body = {resource: extrafields} + path = getattr(self.client, cmd_resource + "_path") + if parent_id: + path = path % (parent_id, myid) + else: + path = path % myid + self.client.format = self.format + # Work around for LP #1217791. XML deserializer called from + # MyComparator does not decodes XML string correctly. + if self.format == 'json': + mox_body = MyComparator(body, self.client) + else: + mox_body = self.client.serialize(body) + self.client.httpclient.request( + MyUrlComparator(end_url(path, format=self.format), + self.client), + 'PUT', + body=mox_body, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("update_" + cmd_resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertIn(myid, _str) + + def _test_show_resource(self, resource, cmd, myid, args, fields=(), + cmd_resource=None, parent_id=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resource: + cmd_resource = resource + + query = "&".join(["fields=%s" % field for field in fields]) + expected_res = {resource: + {self.id_field: myid, + 'name': 'myname', }, } + self.client.format = self.format + resstr = self.client.serialize(expected_res) + path = getattr(self.client, cmd_resource + "_path") + if parent_id: + path = path % (parent_id, myid) + else: + path = path % myid + self.client.httpclient.request( + end_url(path, query, format=self.format), 'GET', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(200), resstr)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("show_" + cmd_resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertIn(myid, _str) + self.assertIn('myname', _str) + + def _test_delete_resource(self, resource, cmd, myid, args, + cmd_resource=None, parent_id=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resource: + cmd_resource = resource + path = getattr(self.client, cmd_resource + "_path") + if parent_id: + path = path % (parent_id, myid) + else: + path = path % (myid) + self.client.httpclient.request( + end_url(path, format=self.format), 'DELETE', + body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), None)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("delete_" + cmd_resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertIn(myid, _str) + + def _test_update_resource_action(self, resource, cmd, myid, action, args, + body, retval=None, cmd_resource=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if not cmd_resource: + cmd_resource = resource + path = getattr(self.client, cmd_resource + "_path") + path_action = '%s/%s' % (myid, action) + self.client.httpclient.request( + end_url(path % path_action, format=self.format), 'PUT', + body=MyComparator(body, self.client), + headers=mox.ContainsKeyValue( + 'X-Auth-Token', TOKEN)).AndReturn((MyResp(204), retval)) + args.extend(['--request-format', self.format]) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("delete_" + cmd_resource) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertIn(myid, _str) + + +class ClientV2TestJson(CLITestV20Base): + def test_do_request_unicode(self): + self.client.format = self.format + self.mox.StubOutWithMock(self.client.httpclient, "request") + unicode_text = u'\u7f51\u7edc' + # url with unicode + action = u'/test' + expected_action = action.encode('utf-8') + # query string with unicode + params = {'test': unicode_text} + expect_query = urllib.urlencode({'test': + unicode_text.encode('utf-8')}) + # request body with unicode + body = params + expect_body = self.client.serialize(body) + # headers with unicode + self.client.httpclient.auth_token = unicode_text + expected_auth_token = unicode_text.encode('utf-8') + + self.client.httpclient.request( + end_url(expected_action, query=expect_query, format=self.format), + 'PUT', body=expect_body, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', + expected_auth_token)).AndReturn((MyResp(200), expect_body)) + + self.mox.ReplayAll() + res_body = self.client.do_request('PUT', action, body=body, + params=params) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + # test response with unicode + self.assertEqual(res_body, body) + + def test_do_request_error_without_response_body(self): + self.client.format = self.format + self.mox.StubOutWithMock(self.client.httpclient, "request") + params = {'test': 'value'} + expect_query = six.moves.urllib.parse.urlencode(params) + self.client.httpclient.auth_token = 'token' + + self.client.httpclient.request( + MyUrlComparator(end_url( + '/test', query=expect_query, format=self.format), self.client), + 'PUT', body='', + headers=mox.ContainsKeyValue('X-Auth-Token', 'token') + ).AndReturn((MyResp(400, reason='An error'), '')) + + self.mox.ReplayAll() + error = self.assertRaises(exceptions.NeutronClientException, + self.client.do_request, 'PUT', '/test', + body='', params=params) + self.assertEqual("An error", str(error)) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + +class ClientV2UnicodeTestXML(ClientV2TestJson): + format = 'xml' + + +class CLITestV20ExceptionHandler(CLITestV20Base): + + def _test_exception_handler_v20( + self, expected_exception, status_code, expected_msg, + error_type=None, error_msg=None, error_detail=None, + error_content=None): + if error_content is None: + error_content = {'NeutronError': {'type': error_type, + 'message': error_msg, + 'detail': error_detail}} + + e = self.assertRaises(expected_exception, + client.exception_handler_v20, + status_code, error_content) + self.assertEqual(status_code, e.status_code) + + if expected_msg is None: + if error_detail: + expected_msg = '\n'.join([error_msg, error_detail]) + else: + expected_msg = error_msg + self.assertEqual(expected_msg, e.message) + + def test_exception_handler_v20_ip_address_in_use(self): + err_msg = ('Unable to complete operation for network ' + 'fake-network-uuid. The IP address fake-ip is in use.') + self._test_exception_handler_v20( + exceptions.IpAddressInUseClient, 409, err_msg, + 'IpAddressInUse', err_msg, '') + + def test_exception_handler_v20_neutron_known_error(self): + known_error_map = [ + ('NetworkNotFound', exceptions.NetworkNotFoundClient, 404), + ('PortNotFound', exceptions.PortNotFoundClient, 404), + ('NetworkInUse', exceptions.NetworkInUseClient, 409), + ('PortInUse', exceptions.PortInUseClient, 409), + ('StateInvalid', exceptions.StateInvalidClient, 400), + ('IpAddressInUse', exceptions.IpAddressInUseClient, 409), + ('IpAddressGenerationFailure', + exceptions.IpAddressGenerationFailureClient, 409), + ('MacAddressInUse', exceptions.MacAddressInUseClient, 409), + ('ExternalIpAddressExhausted', + exceptions.ExternalIpAddressExhaustedClient, 400), + ('OverQuota', exceptions.OverQuotaClient, 409), + ] + + error_msg = 'dummy exception message' + error_detail = 'sample detail' + for server_exc, client_exc, status_code in known_error_map: + self._test_exception_handler_v20( + client_exc, status_code, + error_msg + '\n' + error_detail, + server_exc, error_msg, error_detail) + + def test_exception_handler_v20_neutron_known_error_without_detail(self): + error_msg = 'Network not found' + error_detail = '' + self._test_exception_handler_v20( + exceptions.NetworkNotFoundClient, 404, + error_msg, + 'NetworkNotFound', error_msg, error_detail) + + def test_exception_handler_v20_unknown_error_to_per_code_exception(self): + for status_code, client_exc in exceptions.HTTP_EXCEPTION_MAP.items(): + error_msg = 'Unknown error' + error_detail = 'This is detail' + self._test_exception_handler_v20( + client_exc, status_code, + error_msg + '\n' + error_detail, + 'UnknownError', error_msg, error_detail) + + def test_exception_handler_v20_neutron_unknown_status_code(self): + error_msg = 'Unknown error' + error_detail = 'This is detail' + self._test_exception_handler_v20( + exceptions.NeutronClientException, 501, + error_msg + '\n' + error_detail, + 'UnknownError', error_msg, error_detail) + + def test_exception_handler_v20_bad_neutron_error(self): + error_content = {'NeutronError': {'unknown_key': 'UNKNOWN'}} + self._test_exception_handler_v20( + exceptions.NeutronClientException, 500, + expected_msg={'unknown_key': 'UNKNOWN'}, + error_content=error_content) + + def test_exception_handler_v20_error_dict_contains_message(self): + error_content = {'message': 'This is an error message'} + self._test_exception_handler_v20( + exceptions.NeutronClientException, 500, + expected_msg='This is an error message', + error_content=error_content) + + def test_exception_handler_v20_error_dict_not_contain_message(self): + error_content = {'error': 'This is an error message'} + expected_msg = '%s-%s' % (500, error_content) + self._test_exception_handler_v20( + exceptions.NeutronClientException, 500, + expected_msg=expected_msg, + error_content=error_content) + + def test_exception_handler_v20_default_fallback(self): + error_content = 'This is an error message' + expected_msg = '%s-%s' % (500, error_content) + self._test_exception_handler_v20( + exceptions.NeutronClientException, 500, + expected_msg=expected_msg, + error_content=error_content) + + def test_exception_status(self): + e = exceptions.BadRequest() + self.assertEqual(e.status_code, 400) + + e = exceptions.BadRequest(status_code=499) + self.assertEqual(e.status_code, 499) + + # SslCertificateValidationError has no explicit status_code, + # but should have a 'safe' defined fallback. + e = exceptions.SslCertificateValidationError() + self.assertIsNotNone(e.status_code) + + e = exceptions.SslCertificateValidationError(status_code=599) + self.assertEqual(e.status_code, 599) + + def test_connection_failed(self): + self.mox.StubOutWithMock(self.client.httpclient, 'request') + self.client.httpclient.auth_token = 'token' + + self.client.httpclient.request( + end_url('/test'), 'GET', + headers=mox.ContainsKeyValue('X-Auth-Token', 'token') + ).AndRaise(requests.exceptions.ConnectionError('Connection refused')) + + self.mox.ReplayAll() + + error = self.assertRaises(exceptions.ConnectionFailed, + self.client.get, '/test') + # NB: ConnectionFailed has no explicit status_code, so this + # tests that there is a fallback defined. + self.assertIsNotNone(error.status_code) + self.mox.VerifyAll() + self.mox.UnsetStubs() diff --git a/gbpclient/version.py b/gbpclient/version.py new file mode 100644 index 0000000..e96ab9b --- /dev/null +++ b/gbpclient/version.py @@ -0,0 +1,19 @@ +# 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. +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +import pbr.version + + +__version__ = pbr.version.VersionInfo( + 'python-group-based-policy-client').version_string() diff --git a/openstack-common.conf b/openstack-common.conf new file mode 100644 index 0000000..ea6c4c9 --- /dev/null +++ b/openstack-common.conf @@ -0,0 +1,7 @@ +[DEFAULT] + +# The list of modules to copy from openstack-common +modules=gettextutils,jsonutils,strutils,timeutils + +# The base module to hold the copy of openstack.common +base=gbpclient diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ed37bb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +python-neutronclient>=2.3.6,<3 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9cc558e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name = python-group-based-policy-client +summary = CLI and Client Library for Group Based Policy +description-file = + README.rst +author = Group Based Policy +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Developers + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 2.6 + +[files] +packages = + gbpclient + +[global] +setup-hooks = + pbr.hooks.setup_hook + +[entry_points] +console_scripts = + gbp = gbpclient.shell:main + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source + +[wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7363757 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..dc32c24 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,17 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +hacking>=0.8.0,<0.9 + +cliff-tablib>=1.0 +coverage>=3.6 +discover +fixtures>=0.3.14 +httpretty>=0.8.0,!=0.8.1,!=0.8.2,!=0.8.3 +mox3>=0.7.0 +oslosphinx>=2.2.0.0a2 +oslotest>=1.1.0.0a2 +python-subunit>=0.0.18 +sphinx>=1.1.2,!=1.2.0,<1.3 +testrepository>=0.0.18 +testtools>=0.9.34 diff --git a/tools/gbp.bash_completion b/tools/gbp.bash_completion new file mode 100644 index 0000000..7702a9e --- /dev/null +++ b/tools/gbp.bash_completion @@ -0,0 +1,27 @@ +_gbp_opts="" # lazy init +_gbp_flags="" # lazy init +_gbp_opts_exp="" # lazy init +_gbp() +{ + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [ "x$_gbp_opts" == "x" ] ; then + nbc="`gbp bash-completion`" + _gbp_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _gbp_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _gbp_opts_exp="`echo "$_gbp_opts" | sed -e "s/\s/|/g"`" + fi + + if [[ " ${COMP_WORDS[@]} " =~ " "($_gbp_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.gbpclient/*/*-cache + cflags="$_gbp_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_gbp_opts}" -- ${cur})) + fi + return 0 +} +complete -F _gbp gbp diff --git a/tools/policy.bash_completion b/tools/policy.bash_completion new file mode 100644 index 0000000..dbfe187 --- /dev/null +++ b/tools/policy.bash_completion @@ -0,0 +1,27 @@ +_policy_opts="" # lazy init +_policy_flags="" # lazy init +_policy_opts_exp="" # lazy init +_policy() +{ + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + if [ "x$_policy_opts" == "x" ] ; then + nbc="`policy bash-completion`" + _policy_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _policy_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" + _policy_opts_exp="`echo "$_policy_opts" | sed -e "s/\s/|/g"`" + fi + + if [[ " ${COMP_WORDS[@]} " =~ " "($_policy_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.policyclient/*/*-cache + cflags="$_policy_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_policy_opts}" -- ${cur})) + fi + return 0 +} +complete -F _policy policy diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..bac7c76 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = py26,py27,py33,pypy,pep8 +minversion = 1.6 +skipsdist = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C +usedevelop = True +install_command = pip install -U {opts} {packages} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --testr-args='{posargs}' + +[testenv:pep8] +commands = flake8 +distribute = false + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[testenv:docs] +commands= + python setup.py build_sphinx + +[tox:jenkins] +downloadcache = ~/cache/pip + +[flake8] +# E125 continuation line does not distinguish itself from next logical line +# H302 import only modules +ignore = E125,H302 +show-source = true +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools