From 161f5123bfe2707e33961e3c0b8b74b52828af69 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 19 Dec 2018 19:43:00 +0000 Subject: [PATCH] Compute location properly in server The shade layer pulls the az and project_id info out of the server record and feeds it into the location field. This is because the location field should be where the resource is, and the existing token should only be a fallback. This isn't happening in the resource layer, so start doing that. As a result of digging, make sure we always pass in the connection object to the Resources when we make them. And stop creating a bare resource in order to use the 'new' class method. It's a class method, we don't need to instantiate. Change-Id: Ic8850cd2863e2c806464b92cde2c749f6cc01a91 --- openstack/cloud/_normalize.py | 9 ++-- openstack/compute/v2/server.py | 8 ++++ openstack/proxy.py | 30 ++++++++++---- openstack/resource.py | 25 +++++------ openstack/tests/unit/cloud/test_fwaas.py | 41 +++++++++++++------ .../tests/unit/network/v2/test_floating_ip.py | 9 ++-- openstack/tests/unit/test_proxy.py | 40 +++++++++++++----- openstack/tests/unit/test_resource.py | 27 +++++++----- ...tion-server-resource-af77fdab5d35d421.yaml | 6 +++ 9 files changed, 132 insertions(+), 63 deletions(-) create mode 100644 releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml diff --git a/openstack/cloud/_normalize.py b/openstack/cloud/_normalize.py index 17ee787ed..ea7687fa7 100644 --- a/openstack/cloud/_normalize.py +++ b/openstack/cloud/_normalize.py @@ -483,10 +483,11 @@ class Normalizer(object): server, 'OS-EXT-AZ:availability_zone', None, self.strict_mode) # the server resource has this already, but it's missing az info # from the resource. - # TODO(mordred) Fix server resource to set az in the location - server.pop('location', None) - ret['location'] = self._get_current_location( - project_id=project_id, zone=az) + # TODO(mordred) create_server is still normalizing servers that aren't + # from the resource layer. + ret['location'] = server.pop( + 'location', self._get_current_location( + project_id=project_id, zone=az)) # Ensure volumes is always in the server dict, even if empty ret['volumes'] = _pop_or_get( diff --git a/openstack/compute/v2/server.py b/openstack/compute/v2/server.py index a9db33f9a..85fc71e46 100644 --- a/openstack/compute/v2/server.py +++ b/openstack/compute/v2/server.py @@ -138,6 +138,14 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin): #: only. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name') + def __init__(self, *args, **kwargs): + super(Server, self).__init__(*args, **kwargs) + + if self._connection: + self.location = self._connection._get_current_location( + project_id=self.project_id, + zone=self.availability_zone) + def _prepare_request(self, requires_id=True, prepend_key=True, base_path=None): request = super(Server, self)._prepare_request(requires_id=requires_id, diff --git a/openstack/proxy.py b/openstack/proxy.py index 339f5dc93..c003473d6 100644 --- a/openstack/proxy.py +++ b/openstack/proxy.py @@ -56,6 +56,17 @@ class Proxy(_adapter.OpenStackSDKAdapter): self.retriable_status_codes) super(Proxy, self).__init__(*args, **kwargs) + def _get_connection(self): + """Get the Connection object associated with this Proxy. + + When the Session is created, a reference to the Connection is attached + to the ``_sdk_connection`` attribute. We also add a reference to it + directly on ourselves. Use one of them. + """ + return getattr( + self, '_connection', getattr( + self.session, '_sdk_connection', None)) + def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on @@ -69,16 +80,19 @@ class Proxy(_adapter.OpenStackSDKAdapter): :param path_args: A dict containing arguments for forming the request URL, if needed. """ + conn = self._get_connection() if value is None: # Create a bare resource - res = resource_type.new(**attrs) + res = resource_type.new(connection=conn, **attrs) elif (isinstance(value, dict) and not isinstance(value, resource.Resource)): - res = resource_type._from_munch(value) + res = resource_type._from_munch( + value, connection=conn) res._update(**attrs) elif not isinstance(value, resource_type): # Create from an ID - res = resource_type.new(id=value, **attrs) + res = resource_type.new( + id=value, connection=conn, **attrs) else: # An existing resource instance res = value @@ -205,7 +219,8 @@ class Proxy(_adapter.OpenStackSDKAdapter): :returns: The result of the ``create`` :rtype: :class:`~openstack.resource.Resource` """ - res = resource_type.new(**attrs) + conn = self._get_connection() + res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) @_check_resource(strict=False) @@ -265,9 +280,10 @@ class Proxy(_adapter.OpenStackSDKAdapter): :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ - res = self._get_resource(resource_type, value, **attrs) - return res.list(self, paginated=paginated, - base_path=base_path, **attrs) + return resource_type.list( + self, paginated=paginated, + base_path=base_path, + **attrs) def _head(self, resource_type, value=None, base_path=None, **attrs): """Retrieve a resource's header diff --git a/openstack/resource.py b/openstack/resource.py index 8ecc39e55..912b9fb60 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -765,11 +765,7 @@ class Resource(dict): :param dict kwargs: Each of the named arguments will be set as attributes on the resulting Resource object. """ - res = cls(_synchronized=True, **kwargs) - # TODO(shade) Done as a second call rather than a constructor param - # because otherwise the mocking in the tests goes nuts. - res._connection = connection - return res + return cls(_synchronized=True, connection=connection, **kwargs) @classmethod def _from_munch(cls, obj, synchronized=True, connection=None): @@ -1338,11 +1334,10 @@ class Resource(dict): # argument and is practically a reserved word. raw_resource.pop("self", None) - value = cls.existing(microversion=microversion, **raw_resource) - # TODO(shade) Done as a second call rather than a constructor - # param because otherwise the mocking in the tests goes nuts. - if hasattr(session, '_sdk_connection'): - value._connection = session._sdk_connection + value = cls.existing( + microversion=microversion, + connection=session._get_connection(), + **raw_resource) marker = value.id yield value total_yielded += 1 @@ -1447,13 +1442,13 @@ class Resource(dict): :raises: :class:`openstack.exceptions.ResourceNotFound` if nothing is found and ignore_missing is ``False``. """ + session = cls._get_session(session) # Try to short-circuit by looking directly for a matching ID. try: - match = cls.existing(id=name_or_id, **params) - # TODO(shade) Done as a second call rather than a constructor - # param because otherwise the mocking in the tests goes nuts. - if hasattr(session, '_sdk_connection'): - match._connection = session._sdk_connection + match = cls.existing( + id=name_or_id, + connection=session._get_connection(), + **params) return match.fetch(session) except exceptions.NotFoundException: pass diff --git a/openstack/tests/unit/cloud/test_fwaas.py b/openstack/tests/unit/cloud/test_fwaas.py index abfc3a498..4ad163d3c 100644 --- a/openstack/tests/unit/cloud/test_fwaas.py +++ b/openstack/tests/unit/cloud/test_fwaas.py @@ -48,9 +48,10 @@ class TestFirewallRule(FirewallTestCase): mock_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): - self.mock_firewall_rule = FirewallRule( - **self._mock_firewall_rule_attrs).to_dict() super(TestFirewallRule, self).setUp() + self.mock_firewall_rule = FirewallRule( + connection=self.cloud, + **self._mock_firewall_rule_attrs).to_dict() def test_create_firewall_rule(self): # attributes that are passed to the tested function @@ -260,9 +261,10 @@ class TestFirewallPolicy(FirewallTestCase): mock_firewall_policy = None def setUp(self, cloud_config_fixture='clouds.yaml'): - self.mock_firewall_policy = FirewallPolicy( - **self._mock_firewall_policy_attrs).to_dict() super(TestFirewallPolicy, self).setUp() + self.mock_firewall_policy = FirewallPolicy( + connection=self.cloud, + **self._mock_firewall_policy_attrs).to_dict() def test_create_firewall_policy(self): # attributes that are passed to the tested method @@ -272,7 +274,7 @@ class TestFirewallPolicy(FirewallTestCase): # policy that is returned by the POST request created_attrs = deepcopy(self._mock_firewall_policy_attrs) created_attrs['firewall_rules'][0] = TestFirewallRule.firewall_rule_id - created_policy = FirewallPolicy(**created_attrs) + created_policy = FirewallPolicy(connection=self.cloud, **created_attrs) # attributes used to validate the request inside register_uris() validate_attrs = deepcopy(created_attrs) @@ -421,7 +423,9 @@ class TestFirewallPolicy(FirewallTestCase): self.mock_firewall_policy.copy(), self.mock_firewall_policy.copy()]}) ]) - policy = FirewallPolicy(**self.mock_firewall_policy) + policy = FirewallPolicy( + connection=self.cloud, + **self.mock_firewall_policy) self.assertListEqual(self.cloud.list_firewall_policies(), [policy, policy]) self.assert_calls() @@ -434,12 +438,16 @@ class TestFirewallPolicy(FirewallTestCase): json={'firewall_policies': [ self.mock_firewall_policy]}) ]) - self.assertListEqual(self.cloud.list_firewall_policies(filters), - [FirewallPolicy(**self.mock_firewall_policy)]) + self.assertListEqual( + self.cloud.list_firewall_policies(filters), [ + FirewallPolicy( + connection=self.cloud, + **self.mock_firewall_policy)]) self.assert_calls() def test_update_firewall_policy(self): lookup_rule = FirewallRule( + connection=self.cloud, **TestFirewallRule._mock_firewall_rule_attrs).to_dict() params = {'firewall_rules': [lookup_rule['id']], 'description': 'updated!'} @@ -520,7 +528,9 @@ class TestFirewallPolicy(FirewallTestCase): self.cloud.network.find_firewall_policy = _find def test_insert_rule_into_policy(self): - rule0 = FirewallRule(**TestFirewallRule._mock_firewall_rule_attrs) + rule0 = FirewallRule( + connection=self.cloud, + **TestFirewallRule._mock_firewall_rule_attrs) _rule1_attrs = deepcopy( TestFirewallRule._mock_firewall_rule_attrs) @@ -834,15 +844,19 @@ class TestFirewallGroup(FirewallTestCase): mock_returned_firewall_rule = None def setUp(self, cloud_config_fixture='clouds.yaml'): + super(TestFirewallGroup, self).setUp() self.mock_egress_policy = FirewallPolicy( + connection=self.cloud, **self._mock_egress_policy_attrs).to_dict() self.mock_ingress_policy = FirewallPolicy( + connection=self.cloud, **self._mock_ingress_policy_attrs).to_dict() self.mock_firewall_group = FirewallGroup( + connection=self.cloud, **self._mock_firewall_group_attrs).to_dict() self.mock_returned_firewall_group = FirewallGroup( + connection=self.cloud, **self._mock_returned_firewall_group_attrs).to_dict() - super(TestFirewallGroup, self).setUp() def test_create_firewall_group(self): create_group_attrs = self._mock_firewall_group_attrs.copy() @@ -901,7 +915,10 @@ class TestFirewallGroup(FirewallTestCase): ]) r = self.cloud.create_firewall_group(**firewall_group) self.assertDictEqual( - FirewallGroup(**created_firewall).to_dict(), r.to_dict()) + FirewallGroup( + connection=self.cloud, + **created_firewall).to_dict(), + r.to_dict()) self.assert_calls() def test_delete_firewall_group(self): @@ -1010,7 +1027,7 @@ class TestFirewallGroup(FirewallTestCase): uri=self._make_mock_url('firewall_groups'), json={'firewall_groups': [returned_attrs, returned_attrs]}) ]) - group = FirewallGroup(**returned_attrs) + group = FirewallGroup(connection=self.cloud, **returned_attrs) self.assertListEqual([group, group], self.cloud.list_firewall_groups()) self.assert_calls() diff --git a/openstack/tests/unit/network/v2/test_floating_ip.py b/openstack/tests/unit/network/v2/test_floating_ip.py index 77cc2002e..544520f69 100644 --- a/openstack/tests/unit/network/v2/test_floating_ip.py +++ b/openstack/tests/unit/network/v2/test_floating_ip.py @@ -10,11 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneauth1 import adapter import mock -from openstack.tests.unit import base +from openstack import proxy from openstack.network.v2 import floating_ip +from openstack.tests.unit import base IDENTIFIER = 'IDENTIFIER' EXAMPLE = { @@ -73,9 +73,10 @@ class TestFloatingIP(base.TestCase): self.assertEqual(EXAMPLE['tags'], sot.tags) def test_find_available(self): - mock_session = mock.Mock(spec=adapter.Adapter) + mock_session = mock.Mock(spec=proxy.Proxy) mock_session.get_filter = mock.Mock(return_value={}) mock_session.default_microversion = None + mock_session.session = self.cloud.session data = {'id': 'one', 'floating_ip_address': '10.0.0.1'} fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: [data]} @@ -93,7 +94,7 @@ class TestFloatingIP(base.TestCase): microversion=None) def test_find_available_nada(self): - mock_session = mock.Mock(spec=adapter.Adapter) + mock_session = mock.Mock(spec=proxy.Proxy) mock_session.default_microversion = None fake_response = mock.Mock() body = {floating_ip.FloatingIP.resources_key: []} diff --git a/openstack/tests/unit/test_proxy.py b/openstack/tests/unit/test_proxy.py index 297a06a0a..abf6d1cfb 100644 --- a/openstack/tests/unit/test_proxy.py +++ b/openstack/tests/unit/test_proxy.py @@ -54,7 +54,10 @@ class TestProxyPrivate(base.TestCase): self.sot = mock.Mock() self.sot.method = method - self.fake_proxy = proxy.Proxy("session") + self.session = mock.Mock() + self.session._sdk_connection = self.cloud + self.fake_proxy = proxy.Proxy(self.session) + self.fake_proxy._connection = self.cloud def _test_correct(self, value): decorated = proxy._check_resource(strict=False)(self.sot.method) @@ -119,7 +122,7 @@ class TestProxyPrivate(base.TestCase): result = self.fake_proxy._get_resource(fake_type, None, **attrs) - fake_type.new.assert_called_with(**attrs) + fake_type.new.assert_called_with(connection=self.cloud, **attrs) self.assertEqual(value, result) def test__get_resource_from_id(self): @@ -143,7 +146,8 @@ class TestProxyPrivate(base.TestCase): result = self.fake_proxy._get_resource(Fake, id, **attrs) - self.assertDictEqual(dict(id=id, **attrs), Fake.call) + self.assertDictEqual( + dict(id=id, connection=mock.ANY, **attrs), Fake.call) self.assertEqual(value, result) def test__get_resource_from_resource(self): @@ -169,7 +173,7 @@ class TestProxyPrivate(base.TestCase): result = self.fake_proxy._get_resource(cls, m, **attrs) - cls._from_munch.assert_called_once_with(m) + cls._from_munch.assert_called_once_with(m, connection=self.cloud) res._update.assert_called_once_with(**attrs) self.assertEqual(result, res) @@ -180,6 +184,7 @@ class TestProxyDelete(base.TestCase): super(TestProxyDelete, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_id = 1 self.res = mock.Mock(spec=DeleteableResource) @@ -187,6 +192,7 @@ class TestProxyDelete(base.TestCase): self.res.delete = mock.Mock() self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud DeleteableResource.new = mock.Mock(return_value=self.res) def test_delete(self): @@ -194,7 +200,8 @@ class TestProxyDelete(base.TestCase): self.res.delete.assert_called_with(self.sot) self.sot._delete(DeleteableResource, self.fake_id) - DeleteableResource.new.assert_called_with(id=self.fake_id) + DeleteableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.delete.assert_called_with(self.sot) # Delete generally doesn't return anything, so we will normally @@ -244,6 +251,7 @@ class TestProxyUpdate(base.TestCase): self.res.commit = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud self.attrs = {"x": 1, "y": 2, "z": 3} @@ -278,12 +286,14 @@ class TestProxyCreate(base.TestCase): super(TestProxyCreate, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_result = "fake_result" self.res = mock.Mock(spec=CreateableResource) self.res.create = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud def test_create_attributes(self): CreateableResource.new = mock.Mock(return_value=self.res) @@ -292,7 +302,8 @@ class TestProxyCreate(base.TestCase): rv = self.sot._create(CreateableResource, **attrs) self.assertEqual(rv, self.fake_result) - CreateableResource.new.assert_called_once_with(**attrs) + CreateableResource.new.assert_called_once_with( + connection=self.cloud, **attrs) self.res.create.assert_called_once_with(self.sot, base_path=None) def test_create_attributes_override_base_path(self): @@ -303,7 +314,8 @@ class TestProxyCreate(base.TestCase): rv = self.sot._create(CreateableResource, base_path=base_path, **attrs) self.assertEqual(rv, self.fake_result) - CreateableResource.new.assert_called_once_with(**attrs) + CreateableResource.new.assert_called_once_with( + connection=self.cloud, **attrs) self.res.create.assert_called_once_with(self.sot, base_path=base_path) @@ -313,6 +325,7 @@ class TestProxyGet(base.TestCase): super(TestProxyGet, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_id = 1 self.fake_name = "fake_name" @@ -322,6 +335,7 @@ class TestProxyGet(base.TestCase): self.res.fetch = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud RetrieveableResource.new = mock.Mock(return_value=self.res) def test_get_resource(self): @@ -346,7 +360,8 @@ class TestProxyGet(base.TestCase): def test_get_id(self): rv = self.sot._get(RetrieveableResource, self.fake_id) - RetrieveableResource.new.assert_called_with(id=self.fake_id) + RetrieveableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=None, error_message=mock.ANY) @@ -357,7 +372,8 @@ class TestProxyGet(base.TestCase): rv = self.sot._get(RetrieveableResource, self.fake_id, base_path=base_path) - RetrieveableResource.new.assert_called_with(id=self.fake_id) + RetrieveableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.fetch.assert_called_with( self.sot, requires_id=True, base_path=base_path, error_message=mock.ANY) @@ -383,6 +399,7 @@ class TestProxyList(base.TestCase): self.fake_response = [resource.Resource()] self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud ListableResource.list = mock.Mock() ListableResource.list.return_value = self.fake_response @@ -410,6 +427,7 @@ class TestProxyHead(base.TestCase): super(TestProxyHead, self).setUp() self.session = mock.Mock() + self.session._sdk_connection = self.cloud self.fake_id = 1 self.fake_name = "fake_name" @@ -419,6 +437,7 @@ class TestProxyHead(base.TestCase): self.res.head = mock.Mock(return_value=self.fake_result) self.sot = proxy.Proxy(self.session) + self.sot._connection = self.cloud HeadableResource.new = mock.Mock(return_value=self.res) def test_head_resource(self): @@ -437,6 +456,7 @@ class TestProxyHead(base.TestCase): def test_head_id(self): rv = self.sot._head(HeadableResource, self.fake_id) - HeadableResource.new.assert_called_with(id=self.fake_id) + HeadableResource.new.assert_called_with( + connection=self.cloud, id=self.fake_id) self.res.head.assert_called_with(self.sot, base_path=None) self.assertEqual(rv, self.fake_result) diff --git a/openstack/tests/unit/test_resource.py b/openstack/tests/unit/test_resource.py index f46e9ea68..f1719f79c 100644 --- a/openstack/tests/unit/test_resource.py +++ b/openstack/tests/unit/test_resource.py @@ -1068,6 +1068,8 @@ class TestResourceActions(base.TestCase): self.session.post = mock.Mock(return_value=self.response) self.session.delete = mock.Mock(return_value=self.response) self.session.head = mock.Mock(return_value=self.response) + self.session.session = self.session + self.session._get_connection = mock.Mock(return_value=self.cloud) self.session.default_microversion = None self.session.retriable_status_codes = None @@ -2048,20 +2050,23 @@ class TestResourceFind(base.TestCase): mock_match.fetch.return_value = value return mock_match - result = Test.find("session", "name") + result = Test.find(self.cloud.compute, "name") self.assertEqual(result, value) def test_no_match_raise(self): self.assertRaises(exceptions.ResourceNotFound, self.no_results.find, - "session", "name", ignore_missing=False) + self.cloud.compute, "name", ignore_missing=False) def test_no_match_return(self): self.assertIsNone( - self.no_results.find("session", "name", ignore_missing=True)) + self.no_results.find( + self.cloud.compute, "name", ignore_missing=True)) def test_find_result(self): - self.assertEqual(self.result, self.one_result.find("session", "name")) + self.assertEqual( + self.result, + self.one_result.find(self.cloud.compute, "name")) def test_match_empty_results(self): self.assertIsNone(resource.Resource._get_one_match("name", [])) @@ -2126,7 +2131,7 @@ class TestWaitForStatus(base.TestCase): res.status = status result = resource.wait_for_status( - "session", res, status, "failures", "interval", "wait") + self.cloud.compute, res, status, "failures", "interval", "wait") self.assertTrue(result, res) @@ -2136,7 +2141,7 @@ class TestWaitForStatus(base.TestCase): res.status = status result = resource.wait_for_status( - "session", res, 'lOling', "failures", "interval", "wait") + self.cloud.compute, res, 'lOling', "failures", "interval", "wait") self.assertTrue(result, res) @@ -2146,7 +2151,7 @@ class TestWaitForStatus(base.TestCase): res.mood = status result = resource.wait_for_status( - "session", res, status, "failures", "interval", "wait", + self.cloud.compute, res, status, "failures", "interval", "wait", attribute='mood') self.assertTrue(result, res) @@ -2236,7 +2241,7 @@ class TestWaitForStatus(base.TestCase): self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, - "session", res, status, None, 0.01, 0.1) + self.cloud.compute, res, status, None, 0.01, 0.1) def test_no_sleep(self): res = mock.Mock() @@ -2245,7 +2250,7 @@ class TestWaitForStatus(base.TestCase): self.assertRaises(exceptions.ResourceTimeout, resource.wait_for_status, - "session", res, "status", None, 0, -1) + self.cloud.compute, res, "status", None, 0, -1) class TestWaitForDelete(base.TestCase): @@ -2259,7 +2264,7 @@ class TestWaitForDelete(base.TestCase): None, None, exceptions.ResourceNotFound('Not Found', response)] - result = resource.wait_for_delete("session", res, 1, 3) + result = resource.wait_for_delete(self.cloud.compute, res, 1, 3) self.assertEqual(result, res) @@ -2271,7 +2276,7 @@ class TestWaitForDelete(base.TestCase): self.assertRaises( exceptions.ResourceTimeout, resource.wait_for_delete, - "session", res, 0.1, 0.3) + self.cloud.compute, res, 0.1, 0.3) @mock.patch.object(resource.Resource, '_get_microversion_for', autospec=True) diff --git a/releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml b/releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml new file mode 100644 index 000000000..d548881d8 --- /dev/null +++ b/releasenotes/notes/location-server-resource-af77fdab5d35d421.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Corrected the location property on the ``Server`` resource to + use the ``project_id`` from the remote resource rather than the + information from the token of the user.