diff --git a/blazar/tests/fake_requests.py b/blazar/tests/fake_requests.py new file mode 100644 index 00000000..da265220 --- /dev/null +++ b/blazar/tests/fake_requests.py @@ -0,0 +1,39 @@ +# 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. +"""Fakes relating to the `requests` module.""" + +import requests + + +class FakeResponse(requests.Response): + def __init__(self, status_code, content=None, headers=None): + """A requests.Response that can be used as a mock return_value. + + A key feature is that the instance will evaluate to True or False like + a real Response, based on the status_code. + + Properties like ok, status_code, text, and content, and methods like + json(), work as expected based on the inputs. + + :param status_code: Integer HTTP response code (200, 404, etc.) + :param content: String supplying the payload content of the response. + Using a json-encoded string will make the json() method + behave as expected. + :param headers: Dict of HTTP header values to set. + """ + super(FakeResponse, self).__init__() + self.status_code = status_code + if content: + self._content = content.encode('utf-8') + self.encoding = 'utf-8' + if headers: + self.headers = headers diff --git a/blazar/tests/utils/openstack/test_placement.py b/blazar/tests/utils/openstack/test_placement.py new file mode 100644 index 00000000..0ca2b627 --- /dev/null +++ b/blazar/tests/utils/openstack/test_placement.py @@ -0,0 +1,93 @@ +# 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 mock + +from blazar import tests +from blazar.tests import fake_requests +from blazar.utils.openstack import placement + +from oslo_config import cfg +from oslo_config import fixture as conf_fixture + +CONF = cfg.CONF +PLACEMENT_MICROVERSION = 1.29 + + +class TestPlacementClient(tests.TestCase): + def setUp(self): + super(TestPlacementClient, self).setUp() + self.cfg = self.useFixture(conf_fixture.Config(CONF)) + self.cfg.config(os_auth_host='foofoo') + self.cfg.config(os_auth_port='8080') + self.cfg.config(os_auth_prefix='identity') + self.cfg.config(os_auth_version='v3') + self.client = placement.BlazarPlacementClient() + + def test_client_auth_url(self): + self.assertEqual("http://foofoo:8080/identity/v3", + self.client._client.session.auth.auth_url) + + @mock.patch('keystoneauth1.session.Session.request') + def test_get(self, kss_req): + kss_req.return_value = fake_requests.FakeResponse(200) + url = '/resource_providers' + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + kss_req.assert_called_once_with( + url, 'GET', + endpoint_filter={'service_type': 'placement', + 'interface': 'public'}, + headers={'accept': 'application/json'}, + microversion=PLACEMENT_MICROVERSION, raise_exc=False) + + @mock.patch('keystoneauth1.session.Session.request') + def test_post(self, kss_req): + kss_req.return_value = fake_requests.FakeResponse(200) + url = '/resource_providers' + data = {'name': 'unicorn'} + resp = self.client.post(url, data) + self.assertEqual(200, resp.status_code) + kss_req.assert_called_once_with( + url, 'POST', json=data, + endpoint_filter={'service_type': 'placement', + 'interface': 'public'}, + headers={'accept': 'application/json'}, + microversion=PLACEMENT_MICROVERSION, raise_exc=False) + + @mock.patch('keystoneauth1.session.Session.request') + def test_put(self, kss_req): + kss_req.return_value = fake_requests.FakeResponse(200) + url = '/resource_providers' + data = {'name': 'unicorn'} + resp = self.client.put(url, data) + self.assertEqual(200, resp.status_code) + kss_req.assert_called_once_with( + url, 'PUT', json=data, + endpoint_filter={'service_type': 'placement', + 'interface': 'public'}, + headers={'accept': 'application/json'}, + microversion=PLACEMENT_MICROVERSION, raise_exc=False) + + @mock.patch('keystoneauth1.session.Session.request') + def test_delete(self, kss_req): + kss_req.return_value = fake_requests.FakeResponse(200) + url = '/resource_providers' + resp = self.client.delete(url) + self.assertEqual(200, resp.status_code) + kss_req.assert_called_once_with( + url, 'DELETE', + endpoint_filter={'service_type': 'placement', + 'interface': 'public'}, + headers={'accept': 'application/json'}, + microversion=PLACEMENT_MICROVERSION, raise_exc=False) diff --git a/blazar/utils/openstack/placement.py b/blazar/utils/openstack/placement.py new file mode 100644 index 00000000..a048347c --- /dev/null +++ b/blazar/utils/openstack/placement.py @@ -0,0 +1,93 @@ +# 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 keystoneauth1 import adapter +from keystoneauth1.identity import v3 +from keystoneauth1 import session + +from oslo_config import cfg +from oslo_log import log as logging + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +PLACEMENT_MICROVERSION = 1.29 + + +class BlazarPlacementClient(object): + """Client class for updating placement.""" + + def __init__(self, **kwargs): + """Initialize the report client. + + If a prepared keystoneauth1 adapter for API communication is + specified, it is used. + + Otherwise creates it via _create_client() function. + """ + adapter = kwargs.pop('adapter', None) + self._client = adapter or self._create_client(**kwargs) + + def _create_client(self, **kwargs): + """Create the HTTP session accessing the placement service.""" + username = kwargs.pop('username', + CONF.os_admin_username) + user_domain_name = kwargs.pop('user_domain_name', + CONF.os_admin_user_domain_name) + project_name = kwargs.pop('project_name', + CONF.os_admin_project_name) + password = kwargs.pop('password', + CONF.os_admin_password) + + project_domain_name = kwargs.pop('project_domain_name', + CONF.os_admin_project_domain_name) + auth_url = kwargs.pop('auth_url', None) + + if auth_url is None: + auth_url = "%s://%s:%s/%s/%s" % (CONF.os_auth_protocol, + CONF.os_auth_host, + CONF.os_auth_port, + CONF.os_auth_prefix, + CONF.os_auth_version) + + auth = v3.Password(auth_url=auth_url, + username=username, + password=password, + project_name=project_name, + user_domain_name=user_domain_name, + project_domain_name=project_domain_name) + sess = session.Session(auth=auth) + # Set accept header on every request to ensure we notify placement + # service of our response body media type preferences. + headers = {'accept': 'application/json'} + client = adapter.Adapter(session=sess, + service_type='placement', + interface='public', + additional_headers=headers) + return client + + def get(self, url, microversion=PLACEMENT_MICROVERSION): + return self._client.get(url, raise_exc=False, + microversion=microversion) + + def post(self, url, data, microversion=PLACEMENT_MICROVERSION): + return self._client.post(url, json=data, raise_exc=False, + microversion=microversion) + + def put(self, url, data, microversion=PLACEMENT_MICROVERSION): + return self._client.put(url, json=data, raise_exc=False, + microversion=microversion) + + def delete(self, url, microversion=PLACEMENT_MICROVERSION): + return self._client.delete(url, raise_exc=False, + microversion=microversion) diff --git a/requirements.txt b/requirements.txt index 405d1065..3c663649 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ Babel!=2.4.0,>=2.3.4 # BSD eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT Flask!=0.11,>=0.10 # BSD iso8601>=0.1.11 # MIT +keystoneauth1>=3.4.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0 kombu!=4.0.2,>=4.0.0 # BSD oslo.concurrency>=3.26.0 # Apache-2.0 @@ -26,6 +27,7 @@ netaddr>=0.7.18 # BSD python-keystoneclient>=3.8.0 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD sqlalchemy-migrate>=0.11.0 # Apache-2.0 +requests>=2.18.4 # Apache-2.0 Routes>=2.3.1 # MIT six>=1.10.0 # MIT SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT