diff --git a/.gitignore b/.gitignore index 3d7f5ef..e210f65 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /python-novaclient-2.15.0.tar.gz /python-novaclient-2.16.0.tar.gz /python-novaclient-2.17.0.tar.gz +/python-novaclient-2.18.1.tar.gz diff --git a/0001-Remove-runtime-dependency-on-python-pbr.patch b/0001-Remove-runtime-dependency-on-python-pbr.patch index 7067e36..16d2c51 100644 --- a/0001-Remove-runtime-dependency-on-python-pbr.patch +++ b/0001-Remove-runtime-dependency-on-python-pbr.patch @@ -1,4 +1,4 @@ -From dceb0d263ebf6c4a42bc33213d49e0daf08a8400 Mon Sep 17 00:00:00 2001 +From ec7ab097fa59d53c99946d985d87e556cd732466 Mon Sep 17 00:00:00 2001 From: Jakub Ruzicka Date: Wed, 7 Aug 2013 19:35:08 +0200 Subject: [PATCH] Remove runtime dependency on python-pbr @@ -22,11 +22,11 @@ index bfa7553..18e8783 100644 -__version__ = pbr.version.VersionInfo('python-novaclient').version_string() +__version__ = "REDHATNOVACLIENTVERSION" diff --git a/setup.py b/setup.py -index 70c2b3f..afab729 100644 +index 7363757..09230a0 100644 --- a/setup.py +++ b/setup.py -@@ -18,5 +18,4 @@ - import setuptools +@@ -26,5 +26,4 @@ except ImportError: + pass setuptools.setup( - setup_requires=['pbr'], diff --git a/0002-Fix-session-handling-in-novaclient.patch b/0002-Fix-session-handling-in-novaclient.patch deleted file mode 100644 index 31f2972..0000000 --- a/0002-Fix-session-handling-in-novaclient.patch +++ /dev/null @@ -1,568 +0,0 @@ -From 9d2d572eaa0c4fbc48588b103a176a132e0476d9 Mon Sep 17 00:00:00 2001 -From: Boris Pavlovic -Date: Wed, 26 Mar 2014 15:22:03 +0400 -Subject: [PATCH] Fix session handling in novaclient - -Prior to this patch, novaclient was handling sessions in an inconsistent -manner. - -Every time we created a client instance, it would use a global -connection pool, which made it difficult to use in a process that is -meant to be forked. - -Obviously sessions like the ones provided by the requests library that -will automatically cause connections to be kept alive should not be -implicit. This patch moves the novaclient back to the age of a single -session-less request call by default, but also adds two more -resource-reuse friendly options that a user needs to be explicit about. - -The first one is that both v1_1 and v3 clients can now be used as -context managers,. where the session will be kept open (and thus the -connection kept-alive) for the duration of the with block. This is far -more ideal for a web worker use-case as the session can be made -request-long. - -The second one is the per-instance session. This is very similar to what -we had up until now, except it is not a global object so forking is -possible as long as each child instantiates it's own client. The session -once created will be kept open for the duration of the client object -lifetime. - -Please note: client instances are not thread safe. As can be seen from -above forking example - if you wish to use threading/multiprocessing, -you *must not* share client instances. - -DocImpact - -Related-bug: #1247056 -Closes-Bug: #1297796 -Co-authored-by: Nikola Dipanov -Change-Id: Id59e48f61bb3f3c6223302355c849e1e99673410 - -Conflicts: - novaclient/client.py - novaclient/tests/test_client.py - novaclient/tests/test_http.py - novaclient/v1_1/client.py - novaclient/v3/client.py ---- - novaclient/client.py | 74 +++++++++++++++-------- - novaclient/tests/test_auth_plugins.py | 8 +-- - novaclient/tests/test_client.py | 110 +++++++++++++++++++++++++++++++++- - novaclient/tests/test_http.py | 8 +-- - novaclient/tests/v1_1/test_auth.py | 12 ++-- - novaclient/v1_1/client.py | 27 ++++++++- - novaclient/v3/client.py | 27 ++++++++- - 7 files changed, 221 insertions(+), 45 deletions(-) - -diff --git a/novaclient/client.py b/novaclient/client.py -index 0b9aeee..7f5032b 100644 ---- a/novaclient/client.py -+++ b/novaclient/client.py -@@ -39,17 +39,19 @@ from novaclient import service_catalog - from novaclient import utils - - --_ADAPTERS = {} -+class _ClientConnectionPool(object): - -+ def __init__(self): -+ self._adapters = {} - --def _adapter_pool(url): -- """ -- Store and reuse HTTP adapters per Service URL. -- """ -- if url not in _ADAPTERS: -- _ADAPTERS[url] = adapters.HTTPAdapter() -+ def get(self, url): -+ """ -+ Store and reuse HTTP adapters per Service URL. -+ """ -+ if url not in self._adapters: -+ self._adapters[url] = adapters.HTTPAdapter() - -- return _ADAPTERS[url] -+ return self._adapters[url] - - - class HTTPClient(object): -@@ -64,12 +66,16 @@ class HTTPClient(object): - os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, -- cacert=None, tenant_id=None): -+ cacert=None, tenant_id=None, -+ connection_pool=False): - self.user = user - self.password = password - self.projectid = projectid - self.tenant_id = tenant_id - -+ self._connection_pool = (_ClientConnectionPool() -+ if connection_pool else None) -+ - # This will be called by #_get_password if self.password is None. - # EG if a password can only be obtained by prompting the user, but a - # token is available, you don't want to prompt until the token has -@@ -118,8 +124,8 @@ class HTTPClient(object): - - self.auth_system = auth_system - self.auth_plugin = auth_plugin -+ self._session = None - self._current_url = None -- self._http = None - self._logger = logging.getLogger(__name__) - - if self.http_log_debug and not self._logger.handlers: -@@ -180,19 +186,33 @@ class HTTPClient(object): - 'headers': resp.headers, - 'text': resp.text}) - -- def http(self, url): -- magic_tuple = parse.urlsplit(url) -- scheme, netloc, path, query, frag = magic_tuple -- service_url = '%s://%s' % (scheme, netloc) -- if self._current_url != service_url: -- # Invalidate Session object in case the url is somehow changed -- if self._http: -- self._http.close() -- self._current_url = service_url -- self._logger.debug("New session created for: (%s)" % service_url) -- self._http = requests.Session() -- self._http.mount(service_url, _adapter_pool(service_url)) -- return self._http -+ def open_session(self): -+ if not self._connection_pool: -+ self._session = requests.Session() -+ -+ def close_session(self): -+ if self._session and not self._connection_pool: -+ self._session.close() -+ self._session = None -+ -+ def _get_session(self, url): -+ if self._connection_pool: -+ magic_tuple = parse.urlsplit(url) -+ scheme, netloc, path, query, frag = magic_tuple -+ service_url = '%s://%s' % (scheme, netloc) -+ if self._current_url != service_url: -+ # Invalidate Session object in case the url is somehow changed -+ if self._session: -+ self._session.close() -+ self._current_url = service_url -+ self._logger.debug( -+ "New session created for: (%s)" % service_url) -+ self._session = requests.Session() -+ self._session.mount(service_url, -+ self._connection_pool.get(service_url)) -+ return self._session -+ elif self._session: -+ return self._session - - def request(self, url, method, **kwargs): - kwargs.setdefault('headers', kwargs.get('headers', {})) -@@ -207,7 +227,13 @@ class HTTPClient(object): - kwargs['verify'] = self.verify_cert - - self.http_log_req(method, url, kwargs) -- resp = self.http(url).request( -+ -+ request_func = requests.request -+ session = self._get_session(url) -+ if session: -+ request_func = session.request -+ -+ resp = request_func( - method, - url, - **kwargs) -diff --git a/novaclient/tests/test_auth_plugins.py b/novaclient/tests/test_auth_plugins.py -index a084594..1761b98 100644 ---- a/novaclient/tests/test_auth_plugins.py -+++ b/novaclient/tests/test_auth_plugins.py -@@ -92,7 +92,7 @@ class DeprecatedAuthPluginTest(utils.TestCase): - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - plugin = auth_plugin.DeprecatedAuthPlugin("fake") - cs = client.Client("username", "password", "project_id", -@@ -121,7 +121,7 @@ class DeprecatedAuthPluginTest(utils.TestCase): - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - auth_plugin.discover_auth_systems() - plugin = auth_plugin.DeprecatedAuthPlugin("notexists") -@@ -164,7 +164,7 @@ class DeprecatedAuthPluginTest(utils.TestCase): - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - plugin = auth_plugin.DeprecatedAuthPlugin("fakewithauthurl") - cs = client.Client("username", "password", "project_id", -@@ -197,7 +197,7 @@ class DeprecatedAuthPluginTest(utils.TestCase): - - - class AuthPluginTest(utils.TestCase): -- @mock.patch.object(requests.Session, "request") -+ @mock.patch.object(requests, "request") - @mock.patch.object(pkg_resources, "iter_entry_points") - def test_auth_system_success(self, mock_iter_entry_points, mock_request): - """Test that we can authenticate using the auth system.""" -diff --git a/novaclient/tests/test_client.py b/novaclient/tests/test_client.py -index d586702..96901b9 100644 ---- a/novaclient/tests/test_client.py -+++ b/novaclient/tests/test_client.py -@@ -27,6 +27,16 @@ import novaclient.v3.client - import json - - -+class ClientConnectionPoolTest(utils.TestCase): -+ -+ @mock.patch("novaclient.client.adapters.HTTPAdapter") -+ def test_get(self, mock_http_adapter): -+ mock_http_adapter.side_effect = lambda: mock.Mock() -+ pool = novaclient.client._ClientConnectionPool() -+ self.assertEqual(pool.get("abc"), pool.get("abc")) -+ self.assertNotEqual(pool.get("abc"), pool.get("def")) -+ -+ - class ClientTest(utils.TestCase): - - def test_client_with_timeout(self): -@@ -43,9 +53,9 @@ class ClientTest(utils.TestCase): - 'x-server-management-url': 'blah.com', - 'x-auth-token': 'blah', - } -- with mock.patch('requests.Session.request', mock_request): -+ with mock.patch('requests.request', mock_request): - instance.authenticate() -- requests.Session.request.assert_called_with(mock.ANY, mock.ANY, -+ requests.request.assert_called_with(mock.ANY, mock.ANY, - timeout=2, - headers=mock.ANY, - verify=mock.ANY) -@@ -61,7 +71,7 @@ class ClientTest(utils.TestCase): - instance.version = 'v2.0' - mock_request = mock.Mock() - mock_request.side_effect = novaclient.exceptions.Unauthorized(401) -- with mock.patch('requests.Session.request', mock_request): -+ with mock.patch('requests.request', mock_request): - try: - instance.get('/servers/detail') - except Exception: -@@ -197,6 +207,26 @@ class ClientTest(utils.TestCase): - cs.authenticate() - self.assertTrue(mock_authenticate.called) - -+ @mock.patch('novaclient.client.HTTPClient') -+ def test_contextmanager_v1_1(self, mock_http_client): -+ fake_client = mock.Mock() -+ mock_http_client.return_value = fake_client -+ with novaclient.v1_1.client.Client("user", "password", "project_id", -+ auth_url="foo/v2") as client: -+ pass -+ self.assertTrue(fake_client.open_session.called) -+ self.assertTrue(fake_client.close_session.called) -+ -+ @mock.patch('novaclient.client.HTTPClient') -+ def test_contextmanager_v3(self, mock_http_client): -+ fake_client = mock.Mock() -+ mock_http_client.return_value = fake_client -+ with novaclient.v3.client.Client("user", "password", "project_id", -+ auth_url="foo/v2") as client: -+ pass -+ self.assertTrue(fake_client.open_session.called) -+ self.assertTrue(fake_client.close_session.called) -+ - def test_get_password_simple(self): - cs = novaclient.client.HTTPClient("user", "password", "", "") - cs.password_func = mock.Mock() -@@ -216,3 +246,77 @@ class ClientTest(utils.TestCase): - cs.password_func = mock.Mock() - self.assertEqual(cs._get_password(), "password") - self.assertFalse(cs.password_func.called) -+ -+ def test_auth_url_rstrip_slash(self): -+ cs = novaclient.client.HTTPClient("user", "password", "project_id", -+ auth_url="foo/v2/") -+ self.assertEqual(cs.auth_url, "foo/v2") -+ -+ def test_token_and_bypass_url(self): -+ cs = novaclient.client.HTTPClient(None, None, None, -+ auth_token="12345", -+ bypass_url="compute/v100/") -+ self.assertIsNone(cs.auth_url) -+ self.assertEqual(cs.auth_token, "12345") -+ self.assertEqual(cs.bypass_url, "compute/v100") -+ self.assertEqual(cs.management_url, "compute/v100") -+ -+ @mock.patch("novaclient.client.requests.Session") -+ def test_session(self, mock_session): -+ fake_session = mock.Mock() -+ mock_session.return_value = fake_session -+ cs = novaclient.client.HTTPClient("user", None, "", "") -+ cs.open_session() -+ self.assertEqual(cs._session, fake_session) -+ cs.close_session() -+ self.assertIsNone(cs._session) -+ -+ def test_session_connection_pool(self): -+ cs = novaclient.client.HTTPClient("user", None, "", -+ "", connection_pool=True) -+ cs.open_session() -+ self.assertIsNone(cs._session) -+ cs.close_session() -+ self.assertIsNone(cs._session) -+ -+ def test_get_session(self): -+ cs = novaclient.client.HTTPClient("user", None, "", "") -+ self.assertIsNone(cs._get_session("http://nooooooooo.com")) -+ -+ @mock.patch("novaclient.client.requests.Session") -+ def test_get_session_open_session(self, mock_session): -+ fake_session = mock.Mock() -+ mock_session.return_value = fake_session -+ cs = novaclient.client.HTTPClient("user", None, "", "") -+ cs.open_session() -+ self.assertEqual(fake_session, cs._get_session("http://example.com")) -+ -+ @mock.patch("novaclient.client.requests.Session") -+ @mock.patch("novaclient.client._ClientConnectionPool") -+ def test_get_session_connection_pool(self, mock_pool, mock_session): -+ service_url = "http://example.com" -+ -+ pool = mock.MagicMock() -+ pool.get.return_value = "http_adapter" -+ mock_pool.return_value = pool -+ cs = novaclient.client.HTTPClient("user", None, "", -+ "", connection_pool=True) -+ cs._current_url = "http://another.com" -+ -+ session = cs._get_session(service_url) -+ self.assertEqual(session, mock_session.return_value) -+ pool.get.assert_called_once_with(service_url) -+ mock_session().mount.assert_called_once_with(service_url, -+ 'http_adapter') -+ -+ def test_init_without_connection_pool(self): -+ cs = novaclient.client.HTTPClient("user", None, "", "") -+ self.assertIsNone(cs._connection_pool) -+ -+ @mock.patch("novaclient.client._ClientConnectionPool") -+ def test_init_with_proper_connection_pool(self, mock_pool): -+ fake_pool = mock.Mock() -+ mock_pool.return_value = fake_pool -+ cs = novaclient.client.HTTPClient("user", None, "", -+ connection_pool=True) -+ self.assertEqual(cs._connection_pool, fake_pool) -diff --git a/novaclient/tests/test_http.py b/novaclient/tests/test_http.py -index e2fc4fa..aaa3a46 100644 ---- a/novaclient/tests/test_http.py -+++ b/novaclient/tests/test_http.py -@@ -56,7 +56,7 @@ class ClientTest(utils.TestCase): - def test_get(self): - cl = get_authed_client() - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - @mock.patch('time.time', mock.Mock(return_value=1234)) - def test_get_call(): - resp, body = cl.get("/hi") -@@ -78,7 +78,7 @@ class ClientTest(utils.TestCase): - def test_post(self): - cl = get_authed_client() - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_post_call(): - cl.post("/hi", body=[1, 2, 3]) - headers = { -@@ -110,7 +110,7 @@ class ClientTest(utils.TestCase): - def test_connection_refused(self): - cl = get_client() - -- @mock.patch.object(requests.Session, "request", refused_mock_request) -+ @mock.patch.object(requests, "request", refused_mock_request) - def test_refused_call(): - self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") - -@@ -119,7 +119,7 @@ class ClientTest(utils.TestCase): - def test_bad_request(self): - cl = get_client() - -- @mock.patch.object(requests.Session, "request", bad_req_mock_request) -+ @mock.patch.object(requests, "request", bad_req_mock_request) - def test_refused_call(): - self.assertRaises(exceptions.BadRequest, cl.get, "/hi") - -diff --git a/novaclient/tests/v1_1/test_auth.py b/novaclient/tests/v1_1/test_auth.py -index 7344bc7..7877145 100644 ---- a/novaclient/tests/v1_1/test_auth.py -+++ b/novaclient/tests/v1_1/test_auth.py -@@ -57,7 +57,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): - - mock_request = mock.Mock(return_value=(auth_response)) - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { -@@ -160,7 +160,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): - - mock_request = mock.Mock(side_effect=side_effect) - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { -@@ -248,7 +248,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): - - mock_request = mock.Mock(side_effect=side_effect) - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { -@@ -373,7 +373,7 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase): - - mock_request = mock.Mock(return_value=(auth_response)) - -- with mock.patch.object(requests.Session, "request", mock_request): -+ with mock.patch.object(requests, "request", mock_request): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, -@@ -432,7 +432,7 @@ class AuthenticationTests(utils.TestCase): - }) - mock_request = mock.Mock(return_value=(auth_response)) - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { -@@ -460,7 +460,7 @@ class AuthenticationTests(utils.TestCase): - auth_response = utils.TestResponse({'status_code': 401}) - mock_request = mock.Mock(return_value=(auth_response)) - -- @mock.patch.object(requests.Session, "request", mock_request) -+ @mock.patch.object(requests, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - -diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py -index efeb5c9..ce4aea1 100644 ---- a/novaclient/v1_1/client.py -+++ b/novaclient/v1_1/client.py -@@ -61,6 +61,20 @@ class Client(object): - >>> client.flavors.list() - ... - -+ It is also possible to use an instance as a context manager in which -+ case there will be a session kept alive for the duration of the with -+ statement:: -+ -+ >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: -+ ... client.servers.list() -+ ... client.flavors.list() -+ ... -+ -+ It is also possible to have a permanent (process-long) connection pool, -+ by passing a connection_pool=True:: -+ -+ >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, -+ ... AUTH_URL, connection_pool=True) - """ - - # FIXME(jesse): project_id isn't required to authenticate -@@ -73,7 +87,8 @@ class Client(object): - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, -- cacert=None, tenant_id=None): -+ cacert=None, tenant_id=None, -+ connection_pool=False): - # FIXME(comstud): Rename the api_key argument above when we - # know it's not being used as keyword argument - password = api_key -@@ -145,7 +160,15 @@ class Client(object): - bypass_url=bypass_url, - os_cache=self.os_cache, - http_log_debug=http_log_debug, -- cacert=cacert) -+ cacert=cacert, -+ connection_pool=connection_pool) -+ -+ def __enter__(self): -+ self.client.open_session() -+ return self -+ -+ def __exit__(self, t, v, tb): -+ self.client.close_session() - - def set_management_url(self, url): - self.client.set_management_url(url) -diff --git a/novaclient/v3/client.py b/novaclient/v3/client.py -index f26fdcf..214ba57 100644 ---- a/novaclient/v3/client.py -+++ b/novaclient/v3/client.py -@@ -47,6 +47,20 @@ class Client(object): - >>> client.flavors.list() - ... - -+ It is also possible to use an instance as a context manager in which -+ case there will be a session kept alive for the duration of the with -+ statement:: -+ -+ >>> with Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) as client: -+ ... client.servers.list() -+ ... client.flavors.list() -+ ... -+ -+ It is also possible to have a permanent (process-long) connection pool, -+ by passing a connection_pool=True:: -+ -+ >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, -+ ... AUTH_URL, connection_pool=True) - """ - - # FIXME(jesse): project_id isn't required to authenticate -@@ -59,7 +73,8 @@ class Client(object): - bypass_url=None, os_cache=False, no_cache=True, - http_log_debug=False, auth_system='keystone', - auth_plugin=None, auth_token=None, -- cacert=None, tenant_id=None): -+ cacert=None, tenant_id=None, -+ connection_pool=False): - self.projectid = project_id - self.tenant_id = tenant_id - self.os_cache = os_cache or not no_cache -@@ -110,7 +125,15 @@ class Client(object): - bypass_url=bypass_url, - os_cache=os_cache, - http_log_debug=http_log_debug, -- cacert=cacert) -+ cacert=cacert, -+ connection_pool=connection_pool) -+ -+ def __enter__(self): -+ self.client.open_session() -+ return self -+ -+ def __exit__(self, t, v, tb): -+ self.client.close_session() - - def set_management_url(self, url): - self.client.set_management_url(url) diff --git a/0002-Use-oslo.sphinx-instead-of-oslosphinx.patch b/0002-Use-oslo.sphinx-instead-of-oslosphinx.patch new file mode 100644 index 0000000..87134c5 --- /dev/null +++ b/0002-Use-oslo.sphinx-instead-of-oslosphinx.patch @@ -0,0 +1,22 @@ +From fb03e461cbb26f7c561eba4814cc4631997f8107 Mon Sep 17 00:00:00 2001 +From: Jakub Ruzicka +Date: Wed, 13 Aug 2014 16:02:22 +0200 +Subject: [PATCH] Use oslo.sphinx instead of oslosphinx + +--- + doc/source/conf.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/doc/source/conf.py b/doc/source/conf.py +index e37b8b8..81cb565 100644 +--- a/doc/source/conf.py ++++ b/doc/source/conf.py +@@ -88,7 +88,7 @@ gen_ref("v3", "Version 3 API Reference", + # 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', 'sphinx.ext.intersphinx', 'oslosphinx'] ++extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'oslo.sphinx'] + + # Add any paths that contain templates here, relative to this directory. + templates_path = ['_templates'] diff --git a/0003-Fix-authentication-bug-when-booting-an-server-in-V3.patch b/0003-Fix-authentication-bug-when-booting-an-server-in-V3.patch deleted file mode 100644 index 728413b..0000000 --- a/0003-Fix-authentication-bug-when-booting-an-server-in-V3.patch +++ /dev/null @@ -1,36 +0,0 @@ -From bc1b7808bb937a414217396b2aca7d03cacd166f Mon Sep 17 00:00:00 2001 -From: Haiwei Xu -Date: Sat, 8 Feb 2014 03:45:47 +0900 -Subject: [PATCH] Fix authentication bug when booting an server in V3 - -Currently when booting a server with V3, novaclient sends an empty -os_password to image_cs. This will cause 401(Unauthorized: Invalid -user/password) when trying to find image. This is is a result of -changes nova's V3 API: nova is no longer used as a proxy for the -image service. So novaclient uses two Client instances: one for -nova, the other for image service. This patch checks os_password -before creating the image Client and assigns it if it's empty. - -Change-Id: Ic54cef93e9b823fb98b1edd78776c9a1fc06ba46 -Closes-Bug: #1277425 ---- - novaclient/shell.py | 6 ++++++ - 1 file changed, 6 insertions(+) - -diff --git a/novaclient/shell.py b/novaclient/shell.py -index 919aa21..f751f21 100644 ---- a/novaclient/shell.py -+++ b/novaclient/shell.py -@@ -694,6 +694,12 @@ class OpenStackComputeShell(object): - # sometimes need to be able to look up images information - # via glance when connected to the nova api. - image_service_type = 'image' -+ # NOTE(hdd): the password is needed again because creating a new -+ # Client without specifying bypass_url will force authentication. -+ # We can't reuse self.cs's bypass_url, because that's the URL for -+ # the nova service; we need to get glance's URL for this Client -+ if not os_password: -+ os_password = helper.password - self.cs.image_cs = client.Client( - options.os_compute_api_version, os_username, - os_password, os_tenant_name, tenant_id=os_tenant_id, diff --git a/0004-Nova-CLI-for-server-groups.patch b/0004-Nova-CLI-for-server-groups.patch deleted file mode 100644 index 7b396b9..0000000 --- a/0004-Nova-CLI-for-server-groups.patch +++ /dev/null @@ -1,279 +0,0 @@ -From d7ca548883971b8042660af5523ba74e3eb78bf0 Mon Sep 17 00:00:00 2001 -From: Gary Kotton -Date: Thu, 13 Jun 2013 15:25:39 +0000 -Subject: [PATCH] Nova CLI for server groups - -CLI support for blueprint instance-group-api-extension - -REST API support:- https://review.openstack.org/#/c/62557/ - -DocImpact - - supports create, list, get and delete - - only V2 is supported - -Change-Id: Iaa5a2922b9a0eed9f682b7584c2acf582379b422 ---- - novaclient/tests/v1_1/fakes.py | 45 ++++++++++++++++++ - novaclient/tests/v1_1/test_server_groups.py | 52 +++++++++++++++++++++ - novaclient/v1_1/client.py | 2 + - novaclient/v1_1/server_groups.py | 71 +++++++++++++++++++++++++++++ - novaclient/v1_1/shell.py | 39 ++++++++++++++++ - 5 files changed, 209 insertions(+) - create mode 100644 novaclient/tests/v1_1/test_server_groups.py - create mode 100644 novaclient/v1_1/server_groups.py - -diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py -index dd8fdcc..50658c1 100644 ---- a/novaclient/tests/v1_1/fakes.py -+++ b/novaclient/tests/v1_1/fakes.py -@@ -1996,3 +1996,48 @@ class FakeHTTPClient(base_client.HTTPClient): - return (200, {}, {'events': [ - {'name': 'network-changed', - 'server_uuid': '1234'}]}) -+ -+ # -+ # Server Groups -+ # -+ -+ def get_os_server_groups(self, *kw): -+ return (200, {}, -+ {"server_groups": [ -+ {"members": [], "metadata": {}, -+ "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", -+ "policies": [], "name": "ig1"}, -+ {"members": [], "metadata": {}, -+ "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", -+ "policies": ["anti-affinity"], "name": "ig2"}, -+ {"members": [], "metadata": {"key": "value"}, -+ "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", -+ "policies": [], "name": "ig3"}, -+ {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], -+ "metadata": {}, -+ "id": "4890bb03-7070-45fb-8453-d34556c87d94", -+ "policies": ["anti-affinity"], "name": "ig2"}]}) -+ -+ def _return_server_group(self): -+ r = {'server_group': -+ self.get_os_server_groups()[2]['server_groups'][0]} -+ return (200, {}, r) -+ -+ def post_os_server_groups(self, body, **kw): -+ return self._return_server_group() -+ -+ def get_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, -+ **kw): -+ return self._return_server_group() -+ -+ def put_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b(self, -+ **kw): -+ return self._return_server_group() -+ -+ def post_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b_action( -+ self, body, **kw): -+ return self._return_server_group() -+ -+ def delete_os_server_groups_2cbd51f4_fafe_4cdb_801b_cf913a6f288b( -+ self, **kw): -+ return (202, {}, None) -diff --git a/novaclient/tests/v1_1/test_server_groups.py b/novaclient/tests/v1_1/test_server_groups.py -new file mode 100644 -index 0000000..fde5def ---- /dev/null -+++ b/novaclient/tests/v1_1/test_server_groups.py -@@ -0,0 +1,52 @@ -+# Copyright (c) 2014 VMware, Inc. -+# 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. -+ -+from novaclient.tests import utils -+from novaclient.tests.v1_1 import fakes -+from novaclient.v1_1 import server_groups -+ -+ -+cs = fakes.FakeClient() -+ -+ -+class ServerGroupsTest(utils.TestCase): -+ -+ def test_list_server_groups(self): -+ result = cs.server_groups.list() -+ cs.assert_called('GET', '/os-server-groups') -+ for server_group in result: -+ self.assertTrue(isinstance(server_group, -+ server_groups.ServerGroup)) -+ -+ def test_create_server_group(self): -+ kwargs = {'name': 'ig1', -+ 'policies': ['anti-affinity']} -+ server_group = cs.server_groups.create(**kwargs) -+ body = {'server_group': kwargs} -+ cs.assert_called('POST', '/os-server-groups', body) -+ self.assertTrue(isinstance(server_group, -+ server_groups.ServerGroup)) -+ -+ def test_get_server_group(self): -+ id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' -+ server_group = cs.server_groups.get(id) -+ cs.assert_called('GET', '/os-server-groups/%s' % id) -+ self.assertTrue(isinstance(server_group, -+ server_groups.ServerGroup)) -+ -+ def test_delete_server_group(self): -+ id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' -+ cs.server_groups.delete(id) -+ cs.assert_called('DELETE', '/os-server-groups/%s' % id) -diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py -index ce4aea1..9291270 100644 ---- a/novaclient/v1_1/client.py -+++ b/novaclient/v1_1/client.py -@@ -37,6 +37,7 @@ from novaclient.v1_1 import quota_classes - from novaclient.v1_1 import quotas - from novaclient.v1_1 import security_group_rules - from novaclient.v1_1 import security_groups -+from novaclient.v1_1 import server_groups - from novaclient.v1_1 import servers - from novaclient.v1_1 import services - from novaclient.v1_1 import usage -@@ -131,6 +132,7 @@ class Client(object): - self.os_cache = os_cache or not no_cache - self.availability_zones = \ - availability_zones.AvailabilityZoneManager(self) -+ self.server_groups = server_groups.ServerGroupsManager(self) - - # Add in any extensions... - if extensions: -diff --git a/novaclient/v1_1/server_groups.py b/novaclient/v1_1/server_groups.py -new file mode 100644 -index 0000000..be6ff8e ---- /dev/null -+++ b/novaclient/v1_1/server_groups.py -@@ -0,0 +1,71 @@ -+# Copyright (c) 2014 VMware, Inc. -+# 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. -+ -+""" -+Server group interface. -+""" -+ -+from novaclient import base -+ -+ -+class ServerGroup(base.Resource): -+ """ -+ A server group. -+ """ -+ NAME_ATTR = 'server_group_name' -+ -+ def __repr__(self): -+ return '' % self.id -+ -+ def delete(self): -+ self.manager.delete(self) -+ -+ -+class ServerGroupsManager(base.ManagerWithFind): -+ """ -+ Manage :class:`ServerGroup` resources. -+ """ -+ resource_class = ServerGroup -+ -+ def list(self): -+ """Get a list of all server groups. -+ -+ :rtype: list of :class:`ServerGroup`. -+ """ -+ return self._list('/os-server-groups', 'server_groups') -+ -+ def get(self, id): -+ """Get a specific server group. -+ -+ :param id: The ID of the :class:`ServerGroup` to get. -+ :rtype: :class:`ServerGroup` -+ """ -+ return self._get('/os-server-groups/%s' % id, -+ 'server_group') -+ -+ def delete(self, id): -+ """Delete a specific server group. -+ -+ :param id: The ID of the :class:`ServerGroup` to delete. -+ """ -+ self._delete('/os-server-groups/%s' % id) -+ -+ def create(self, **kwargs): -+ """Create (allocate) a server group. -+ -+ :rtype: list of :class:`ServerGroup` -+ """ -+ body = {'server_group': kwargs} -+ return self._create('/os-server-groups', body, 'server_group') -diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py -index a043d3c..c1754c9 100644 ---- a/novaclient/v1_1/shell.py -+++ b/novaclient/v1_1/shell.py -@@ -3512,3 +3512,42 @@ def do_availability_zone_list(cs, _args): - _translate_availability_zone_keys(result) - utils.print_list(result, ['Name', 'Status'], - sortby_index=None) -+ -+ -+def _print_server_group_details(server_group): -+ columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] -+ utils.print_list(server_group, columns) -+ -+ -+def do_server_group_list(cs, args): -+ """Print a list of all server groups.""" -+ server_groups = cs.server_groups.list() -+ _print_server_group_details(server_groups) -+ -+ -+@utils.arg('name', metavar='', help='Server group name.') -+@utils.arg('--policy', metavar='', action='append', -+ dest='policies', default=[], type=str, -+ help='Policies for the server groups') -+def do_server_group_create(cs, args): -+ """Create a new server group with the specified details.""" -+ kwargs = {'name': args.name, -+ 'policies': args.policies} -+ server_group = cs.server_groups.create(**kwargs) -+ _print_server_group_details([server_group]) -+ -+ -+@utils.arg('id', metavar='', -+ help="Unique ID of the server group to delete") -+def do_server_group_delete(cs, args): -+ """Delete a specific server group.""" -+ cs.server_groups.delete(args.id) -+ print("Instance group %s has been successfully deleted." % args.id) -+ -+ -+@utils.arg('id', metavar='', -+ help="Unique ID of the server group to get") -+def do_server_group_get(cs, args): -+ """Get a specific server group.""" -+ server_group = cs.server_groups.get(args.id) -+ _print_server_group_details([server_group]) diff --git a/0005-Avoid-AttributeError-in-servers.Server.__repr__.patch b/0005-Avoid-AttributeError-in-servers.Server.__repr__.patch deleted file mode 100644 index f1a5464..0000000 --- a/0005-Avoid-AttributeError-in-servers.Server.__repr__.patch +++ /dev/null @@ -1,68 +0,0 @@ -From 9ef187014875577fd62ca2b5454355c34cd22708 Mon Sep 17 00:00:00 2001 -From: ZhiQiang Fan -Date: Mon, 24 Mar 2014 15:29:01 +0800 -Subject: [PATCH] Avoid AttributeError in servers.Server.__repr__ - -servers.Server represents various object now, and some of them may -don't have attribute 'name', for example, the interface_list() result -object. It will cause AttributeError when we try to format string with -such object, so I add a check for the 'name' attribute in __repr__ -method, it will use 'unknown-name' instead when 'name' is not found. - -Change-Id: If4757d5d73721774543d58a4cc875710a6013f34 -Closes-Bug: #1280453 ---- - novaclient/tests/v1_1/test_servers.py | 26 ++++++++++++++++++++++++++ - novaclient/v1_1/servers.py | 2 +- - 2 files changed, 27 insertions(+), 1 deletion(-) - -diff --git a/novaclient/tests/v1_1/test_servers.py b/novaclient/tests/v1_1/test_servers.py -index a48204c..b70964a 100644 ---- a/novaclient/tests/v1_1/test_servers.py -+++ b/novaclient/tests/v1_1/test_servers.py -@@ -582,6 +582,32 @@ class ServersTest(utils.TestCase): - s.interface_list() - cs.assert_called('GET', '/servers/1234/os-interface') - -+ def test_interface_list_result_string_representable(self): -+ """Test for bugs.launchpad.net/python-novaclient/+bug/1280453.""" -+ # According to https://github.com/openstack/nova/blob/master/ -+ # nova/api/openstack/compute/contrib/attach_interfaces.py#L33, -+ # the attach_interface extension get method will return a json -+ # object partly like this: -+ interface_list = [{ -+ 'net_id': 'd7745cf5-63f9-4883-b0ae-983f061e4f23', -+ 'port_id': 'f35079da-36d5-4513-8ec1-0298d703f70e', -+ 'mac_addr': 'fa:16:3e:4c:37:c8', -+ 'port_state': 'ACTIVE', -+ 'fixed_ips': [{ -+ 'subnet_id': 'f1ad93ad-2967-46ba-b403-e8cbbe65f7fa', -+ 'ip_address': '10.2.0.96' -+ }] -+ }] -+ # If server is not string representable, it will raise an exception, -+ # because attribute named 'name' cannot be found. -+ # Parameter 'loaded' must be True or it will try to get attribute -+ # 'id' then fails (lazy load detail), this is exactly same as -+ # novaclient.base.Manager._list() -+ s = servers.Server(servers.ServerManager, interface_list[0], -+ loaded=True) -+ # Trigger the __repr__ magic method -+ self.assertEqual('', '%r' % s) -+ - def test_interface_attach(self): - s = cs.servers.get(1234) - s.interface_attach(None, None, None) -diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py -index cf284d9..d4ac6a7 100644 ---- a/novaclient/v1_1/servers.py -+++ b/novaclient/v1_1/servers.py -@@ -36,7 +36,7 @@ class Server(base.Resource): - HUMAN_ID = True - - def __repr__(self): -- return "" % self.name -+ return '' % getattr(self, 'name', 'unknown-name') - - def delete(self): - """ diff --git a/0006-Enable-delete-multiple-server-groups-in-one-request.patch b/0006-Enable-delete-multiple-server-groups-in-one-request.patch deleted file mode 100644 index 876853f..0000000 --- a/0006-Enable-delete-multiple-server-groups-in-one-request.patch +++ /dev/null @@ -1,86 +0,0 @@ -From 55b72401b7cfce4c8b9edec827a96ab8b815ed30 Mon Sep 17 00:00:00 2001 -From: Jay Lau -Date: Sun, 6 Apr 2014 13:46:53 +0800 -Subject: [PATCH] Enable delete multiple server groups in one request - -Currently, "nova server-group-delete" can only delete one server -group in one request, this patch was enabling nova client support -removing multiple server groups in one request. - -Change-Id: I373151bc27cbe8617e2023ba99f6fb3f0108d592 -Closes-Bug: #1302954 - -Conflicts: - novaclient/tests/v1_1/test_shell.py ---- - novaclient/tests/v1_1/fakes.py | 6 ++++++ - novaclient/tests/v1_1/test_shell.py | 5 +++++ - novaclient/v1_1/shell.py | 21 ++++++++++++++++----- - 3 files changed, 27 insertions(+), 5 deletions(-) - -diff --git a/novaclient/tests/v1_1/fakes.py b/novaclient/tests/v1_1/fakes.py -index 50658c1..61926c1 100644 ---- a/novaclient/tests/v1_1/fakes.py -+++ b/novaclient/tests/v1_1/fakes.py -@@ -405,6 +405,12 @@ class FakeHTTPClient(base_client.HTTPClient): - fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) - return (204, {}, body) - -+ def delete_os_server_groups_12345(self, **kw): -+ return (202, {}, None) -+ -+ def delete_os_server_groups_56789(self, **kw): -+ return (202, {}, None) -+ - def delete_servers_1234(self, **kw): - return (202, {}, None) - -diff --git a/novaclient/tests/v1_1/test_shell.py b/novaclient/tests/v1_1/test_shell.py -index 580d2ed..503c813 100644 ---- a/novaclient/tests/v1_1/test_shell.py -+++ b/novaclient/tests/v1_1/test_shell.py -@@ -1918,6 +1918,11 @@ class ShellTest(utils.TestCase): - mock_system.assert_called_with("ssh -6 -p22 " - "root@2607:f0d0:1002::4 -1") - -+ def test_delete_multi_server_groups(self): -+ self.run_command('server-group-delete 12345 56789') -+ self.assert_called('DELETE', '/os-server-groups/56789') -+ self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) -+ - - class GetSecgroupTest(utils.TestCase): - def test_with_integer(self): -diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py -index c1754c9..3bf12f6 100644 ---- a/novaclient/v1_1/shell.py -+++ b/novaclient/v1_1/shell.py -@@ -3537,12 +3537,23 @@ def do_server_group_create(cs, args): - _print_server_group_details([server_group]) - - --@utils.arg('id', metavar='', -- help="Unique ID of the server group to delete") -+@utils.arg('id', metavar='', nargs='+', -+ help="Unique ID(s) of the server group to delete") - def do_server_group_delete(cs, args): -- """Delete a specific server group.""" -- cs.server_groups.delete(args.id) -- print("Instance group %s has been successfully deleted." % args.id) -+ """Delete specific server group(s).""" -+ failure_count = 0 -+ -+ for sg in args.id: -+ try: -+ cs.server_groups.delete(sg) -+ print(_("Server group %s has been successfully deleted.") % sg) -+ except Exception as e: -+ failure_count += 1 -+ print(_("Delete for server group %(sg)s failed: %(e)s") % -+ {'sg': sg, 'e': e}) -+ if failure_count == len(args.id): -+ raise exceptions.CommandError(_("Unable to delete any of the " -+ "specified server groups.")) - - - @utils.arg('id', metavar='', diff --git a/python-novaclient.spec b/python-novaclient.spec index 320f03d..c7218fa 100644 --- a/python-novaclient.spec +++ b/python-novaclient.spec @@ -1,7 +1,7 @@ Name: python-novaclient Epoch: 1 -Version: 2.17.0 -Release: 3%{?dist} +Version: 2.18.1 +Release: 1%{?dist} Summary: Python API and CLI for OpenStack Nova Group: Development/Languages @@ -9,16 +9,8 @@ License: ASL 2.0 URL: http://pypi.python.org/pypi/%{name} Source0: http://pypi.python.org/packages/source/p/%{name}/%{name}-%{version}.tar.gz - -# -# patches_base=2.17.0 -# Patch0001: 0001-Remove-runtime-dependency-on-python-pbr.patch -Patch0002: 0002-Fix-session-handling-in-novaclient.patch -Patch0003: 0003-Fix-authentication-bug-when-booting-an-server-in-V3.patch -Patch0004: 0004-Nova-CLI-for-server-groups.patch -Patch0005: 0005-Avoid-AttributeError-in-servers.Server.__repr__.patch -Patch0006: 0006-Enable-delete-multiple-server-groups-in-one-request.patch +Patch0002: 0002-Use-oslo.sphinx-instead-of-oslosphinx.patch BuildArch: noarch BuildRequires: python-setuptools @@ -46,6 +38,7 @@ Summary: Documentation for OpenStack Nova API Client Group: Documentation BuildRequires: python-sphinx +BuildRequires: python-oslo-sphinx %description doc This is a client for the OpenStack Nova API. There's a Python API (the @@ -59,10 +52,6 @@ This package contains auto-generated documentation. %patch0001 -p1 %patch0002 -p1 -%patch0003 -p1 -%patch0004 -p1 -%patch0005 -p1 -%patch0006 -p1 # We provide version like this in order to remove runtime dep on pbr. sed -i s/REDHATNOVACLIENTVERSION/%{version}/ novaclient/__init__.py @@ -107,6 +96,11 @@ rm -fr html/.doctrees html/.buildinfo %doc html %changelog +* Wed Aug 13 2014 Jakub Ruzicka 1:2.18.1-1 +- Update to upstream 2.18.1 +- New Requires: python-oslo-sphinx +- Use oslo.sphinx instead of oslosphinx + * Sat Jun 07 2014 Fedora Release Engineering - 1:2.17.0-3 - Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild diff --git a/sources b/sources index 1c229da..d0a98ff 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -b9f4b00a0e3b029553f1796c393490cf python-novaclient-2.17.0.tar.gz +f9476c3c254d657cd82372f3e524b188 python-novaclient-2.18.1.tar.gz