2 # Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # * See the License for the specific language governing permissions and
14 # * limitations under the License.
20 import __builtin__ as builtins
23 from cloudify.exceptions import NonRecoverableError
25 from cloudify.mocks import MockCloudifyContext
26 import openstack_plugin_common as common
29 class ConfigTests(unittest.TestCase):
31 @mock.patch.dict('os.environ', clear=True)
32 def test__build_config_from_env_variables_empty(self):
33 cfg = common.Config._build_config_from_env_variables()
34 self.assertEqual({}, cfg)
36 @mock.patch.dict('os.environ', clear=True,
37 OS_AUTH_URL='test_url')
38 def test__build_config_from_env_variables_single(self):
39 cfg = common.Config._build_config_from_env_variables()
40 self.assertEqual({'auth_url': 'test_url'}, cfg)
42 @mock.patch.dict('os.environ', clear=True,
43 OS_AUTH_URL='test_url',
45 OS_REGION_NAME='region')
46 def test__build_config_from_env_variables_multiple(self):
47 cfg = common.Config._build_config_from_env_variables()
49 'auth_url': 'test_url',
51 'region_name': 'region',
54 @mock.patch.dict('os.environ', clear=True,
57 os_region_name='region')
58 def test__build_config_from_env_variables_all_ignored(self):
59 cfg = common.Config._build_config_from_env_variables()
60 self.assertEqual({}, cfg)
62 @mock.patch.dict('os.environ', clear=True,
63 OS_AUTH_URL='test_url',
65 OS_REGION_NAME='region',
68 os_region_name='region')
69 def test__build_config_from_env_variables_extract_valid(self):
70 cfg = common.Config._build_config_from_env_variables()
72 'auth_url': 'test_url',
74 'region_name': 'region',
77 def test_update_config_empty_target(self):
79 override = {'k1': 'u1'}
80 result = override.copy()
82 common.Config.update_config(target, override)
83 self.assertEqual(result, target)
85 def test_update_config_empty_override(self):
88 result = target.copy()
90 common.Config.update_config(target, override)
91 self.assertEqual(result, target)
93 def test_update_config_disjoint_configs(self):
95 override = {'k2': 'u2'}
96 result = target.copy()
97 result.update(override)
99 common.Config.update_config(target, override)
100 self.assertEqual(result, target)
102 def test_update_config_do_not_remove_empty_from_target(self):
105 result = target.copy()
107 common.Config.update_config(target, override)
108 self.assertEqual(result, target)
110 def test_update_config_no_empty_in_override(self):
111 target = {'k1': 'v1', 'k2': 'v2'}
112 override = {'k1': 'u2'}
113 result = target.copy()
114 result.update(override)
116 common.Config.update_config(target, override)
117 self.assertEqual(result, target)
119 def test_update_config_all_empty_in_override(self):
120 target = {'k1': '', 'k2': 'v2'}
121 override = {'k1': '', 'k3': ''}
122 result = target.copy()
124 common.Config.update_config(target, override)
125 self.assertEqual(result, target)
127 def test_update_config_misc(self):
128 target = {'k1': 'v1', 'k2': 'v2'}
129 override = {'k1': '', 'k2': 'u2', 'k3': '', 'k4': 'u4'}
130 result = {'k1': 'v1', 'k2': 'u2', 'k4': 'u4'}
132 common.Config.update_config(target, override)
133 self.assertEqual(result, target)
135 @mock.patch.object(common.Config, 'update_config')
136 @mock.patch.object(common.Config, '_build_config_from_env_variables',
138 @mock.patch.dict('os.environ', clear=True,
139 values={common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR:
140 '/this/should/not/exist.json'})
141 def test_get_missing_static_config_missing_file(self, from_env, update):
142 cfg = common.Config.get()
143 self.assertEqual({}, cfg)
144 from_env.assert_called_once_with()
145 update.assert_not_called()
147 @mock.patch.object(common.Config, 'update_config')
148 @mock.patch.object(common.Config, '_build_config_from_env_variables',
150 def test_get_empty_static_config_present_file(self, from_env, update):
151 file_cfg = {'k1': 'v1', 'k2': 'v2'}
152 env_var = common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR
153 file = tempfile.NamedTemporaryFile(delete=False)
154 json.dump(file_cfg, file)
157 with mock.patch.dict('os.environ', {env_var: file.name}, clear=True):
161 from_env.assert_called_once_with()
162 update.assert_called_once_with({}, file_cfg)
164 @mock.patch.object(common.Config, 'update_config')
165 @mock.patch.object(common.Config, '_build_config_from_env_variables',
166 return_value={'k1': 'v1'})
167 def test_get_present_static_config_empty_file(self, from_env, update):
169 env_var = common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR
170 file = tempfile.NamedTemporaryFile(delete=False)
171 json.dump(file_cfg, file)
174 with mock.patch.dict('os.environ', {env_var: file.name}, clear=True):
178 from_env.assert_called_once_with()
179 update.assert_called_once_with({'k1': 'v1'}, file_cfg)
181 @mock.patch.object(common.Config, 'update_config')
182 @mock.patch.object(common.Config, '_build_config_from_env_variables',
183 return_value={'k1': 'v1'})
184 @mock.patch.dict('os.environ', clear=True,
185 values={common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR:
186 '/this/should/not/exist.json'})
187 def test_get_present_static_config_missing_file(self, from_env, update):
188 cfg = common.Config.get()
189 self.assertEqual({'k1': 'v1'}, cfg)
190 from_env.assert_called_once_with()
191 update.assert_not_called()
193 @mock.patch.object(common.Config, 'update_config')
194 @mock.patch.object(common.Config, '_build_config_from_env_variables',
195 return_value={'k1': 'v1'})
196 def test_get_all_present(self, from_env, update):
197 file_cfg = {'k2': 'u2'}
198 env_var = common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR
199 file = tempfile.NamedTemporaryFile(delete=False)
200 json.dump(file_cfg, file)
203 with mock.patch.dict('os.environ', {env_var: file.name}, clear=True):
207 from_env.assert_called_once_with()
208 update.assert_called_once_with({'k1': 'v1'}, file_cfg)
211 class OpenstackClientTests(unittest.TestCase):
213 def test__merge_custom_configuration_no_custom_cfg(self):
215 new = common.OpenStackClient._merge_custom_configuration(cfg, "dummy")
216 self.assertEqual(cfg, new)
218 def test__merge_custom_configuration_client_present(self):
222 'custom_configuration': {
235 new = common.OpenStackClient._merge_custom_configuration(cfg, "dummy")
236 self.assertEqual(result, new)
237 self.assertEqual(cfg, bak)
239 def test__merge_custom_configuration_client_missing(self):
243 'custom_configuration': {
255 new = common.OpenStackClient._merge_custom_configuration(cfg, "baddy")
256 self.assertEqual(result, new)
257 self.assertEqual(cfg, bak)
259 def test__merge_custom_configuration_multi_client(self):
263 'custom_configuration': {
278 new = common.OpenStackClient._merge_custom_configuration(cfg, "bummy")
279 self.assertEqual(result, new)
280 self.assertEqual(cfg, bak)
282 @mock.patch.object(common, 'ctx')
283 def test__merge_custom_configuration_nova_url(self, mock_ctx):
285 'nova_url': 'gopher://nova',
290 common.OpenStackClient._merge_custom_configuration(
292 {'endpoint_override': 'gopher://nova'},
295 common.OpenStackClient._merge_custom_configuration(
299 self.assertEqual(cfg, bak)
300 mock_ctx.logger.warn.assert_has_calls([
302 "'nova_url' property is deprecated. Use `custom_configuration."
303 "nova_client.endpoint_override` instead."),
305 "'nova_url' property is deprecated. Use `custom_configuration."
306 "nova_client.endpoint_override` instead."),
309 @mock.patch('keystoneauth1.session.Session')
310 def test___init___multi_region(self, m_session):
311 mock_client_class = mock.MagicMock()
314 'auth_url': 'test-auth_url/v3',
315 'region': 'test-region',
318 with mock.patch.object(
323 "region": "region from file",
324 "other": "this one should get through"
330 common.OpenStackClient('fred', mock_client_class, cfg)
332 mock_client_class.assert_called_once_with(
333 region_name='test-region',
334 other='this one should get through',
335 session=m_session.return_value,
338 def test__validate_auth_params_missing(self):
339 with self.assertRaises(NonRecoverableError):
340 common.OpenStackClient._validate_auth_params({})
342 def test__validate_auth_params_too_much(self):
343 with self.assertRaises(NonRecoverableError):
344 common.OpenStackClient._validate_auth_params({
348 'tenant_name': 'tenant',
349 'project_id': 'project_test',
352 def test__validate_auth_params_v2(self):
353 common.OpenStackClient._validate_auth_params({
357 'tenant_name': 'tenant',
360 def test__validate_auth_params_v3(self):
361 common.OpenStackClient._validate_auth_params({
365 'project_id': 'project_test',
366 'user_domain_name': 'user_domain',
369 def test__validate_auth_params_v3_mod(self):
370 common.OpenStackClient._validate_auth_params({
374 'user_domain_name': 'user_domain',
375 'project_name': 'project_test_name',
376 'project_domain_name': 'project_domain',
379 def test__validate_auth_params_skip_insecure(self):
380 common.OpenStackClient._validate_auth_params({
384 'user_domain_name': 'user_domain',
385 'project_name': 'project_test_name',
386 'project_domain_name': 'project_domain',
390 def test__split_config(self):
391 auth = {'auth_url': 'url', 'password': 'pass'}
392 misc = {'misc1': 'val1', 'misc2': 'val2'}
396 a, m = common.OpenStackClient._split_config(all)
398 self.assertEqual(auth, a)
399 self.assertEqual(misc, m)
401 @mock.patch.object(common, 'loading')
402 @mock.patch.object(common, 'session')
403 def test__authenticate_secure(self, mock_session, mock_loading):
404 auth_params = {'k1': 'v1'}
405 common.OpenStackClient._authenticate(auth_params)
406 loader = mock_loading.get_plugin_loader.return_value
407 loader.load_from_options.assert_called_once_with(k1='v1')
408 auth = loader.load_from_options.return_value
409 mock_session.Session.assert_called_once_with(auth=auth, verify=True)
411 @mock.patch.object(common, 'loading')
412 @mock.patch.object(common, 'session')
413 def test__authenticate_secure_explicit(self, mock_session, mock_loading):
414 auth_params = {'k1': 'v1', 'insecure': False}
415 common.OpenStackClient._authenticate(auth_params)
416 loader = mock_loading.get_plugin_loader.return_value
417 loader.load_from_options.assert_called_once_with(k1='v1')
418 auth = loader.load_from_options.return_value
419 mock_session.Session.assert_called_once_with(auth=auth, verify=True)
421 @mock.patch.object(common, 'loading')
422 @mock.patch.object(common, 'session')
423 def test__authenticate_insecure(self, mock_session, mock_loading):
424 auth_params = {'k1': 'v1', 'insecure': True}
425 common.OpenStackClient._authenticate(auth_params)
426 loader = mock_loading.get_plugin_loader.return_value
427 loader.load_from_options.assert_called_once_with(k1='v1')
428 auth = loader.load_from_options.return_value
429 mock_session.Session.assert_called_once_with(auth=auth, verify=False)
431 @mock.patch.object(common, 'loading')
432 @mock.patch.object(common, 'session')
433 def test__authenticate_secure_misc(self, mock_session, mock_loading):
434 params = {'k1': 'v1'}
435 tests = ('', 'a', [], {}, set(), 4, 0, -1, 3.14, 0.0, None)
437 auth_params = params.copy()
438 auth_params['insecure'] = test
440 common.OpenStackClient._authenticate(auth_params)
441 loader = mock_loading.get_plugin_loader.return_value
442 loader.load_from_options.assert_called_with(**params)
443 auth = loader.load_from_options.return_value
444 mock_session.Session.assert_called_with(auth=auth, verify=True)
446 @mock.patch.object(common, 'cinder_client')
447 def test_cinder_client_get_name_from_resource(self, cc_mock):
448 ccws = common.CinderClientWithSugar()
449 mock_volume = mock.Mock()
453 ccws.get_name_from_resource(mock_volume))
456 class ClientsConfigTest(unittest.TestCase):
459 file = tempfile.NamedTemporaryFile(delete=False)
460 json.dump(self.get_file_cfg(), file)
462 self.addCleanup(os.unlink, file.name)
464 env_cfg = self.get_env_cfg()
465 env_cfg[common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR] = file.name
466 mock.patch.dict('os.environ', env_cfg, clear=True).start()
468 self.loading = mock.patch.object(common, 'loading').start()
469 self.session = mock.patch.object(common, 'session').start()
470 self.nova = mock.patch.object(common, 'nova_client').start()
471 self.neutron = mock.patch.object(common, 'neutron_client').start()
472 self.cinder = mock.patch.object(common, 'cinder_client').start()
473 self.addCleanup(mock.patch.stopall)
475 self.loader = self.loading.get_plugin_loader.return_value
476 self.auth = self.loader.load_from_options.return_value
479 class CustomConfigFromInputs(ClientsConfigTest):
481 def get_file_cfg(self):
483 'username': 'file-username',
484 'password': 'file-password',
485 'tenant_name': 'file-tenant-name',
486 'custom_configuration': {
488 'username': 'custom-username',
489 'password': 'custom-password',
490 'tenant_name': 'custom-tenant-name'
495 def get_inputs_cfg(self):
497 'auth_url': 'envar-auth-url',
498 'username': 'inputs-username',
499 'custom_configuration': {
501 'password': 'inputs-custom-password'
504 'password': 'inputs-custom-password',
505 'auth_url': 'inputs-custom-auth-url',
506 'extra_key': 'extra-value'
511 def get_env_cfg(self):
513 'OS_USERNAME': 'envar-username',
514 'OS_PASSWORD': 'envar-password',
515 'OS_TENANT_NAME': 'envar-tenant-name',
516 'OS_AUTH_URL': 'envar-auth-url',
517 common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR: file.name
521 common.NovaClientWithSugar(config=self.get_inputs_cfg())
522 self.loader.load_from_options.assert_called_once_with(
523 username='inputs-username',
524 password='file-password',
525 tenant_name='file-tenant-name',
526 auth_url='envar-auth-url'
528 self.session.Session.assert_called_with(auth=self.auth, verify=True)
529 self.nova.Client.assert_called_once_with(
530 '2', session=self.session.Session.return_value)
532 def test_neutron(self):
533 common.NeutronClientWithSugar(config=self.get_inputs_cfg())
534 self.loader.load_from_options.assert_called_once_with(
535 username='inputs-username',
536 password='inputs-custom-password',
537 tenant_name='file-tenant-name',
538 auth_url='envar-auth-url'
540 self.session.Session.assert_called_with(auth=self.auth, verify=True)
541 self.neutron.Client.assert_called_once_with(
542 session=self.session.Session.return_value)
544 def test_cinder(self):
545 common.CinderClientWithSugar(config=self.get_inputs_cfg())
546 self.loader.load_from_options.assert_called_once_with(
547 username='inputs-username',
548 password='inputs-custom-password',
549 tenant_name='file-tenant-name',
550 auth_url='inputs-custom-auth-url'
552 self.session.Session.assert_called_with(auth=self.auth, verify=True)
553 self.cinder.Client.assert_called_once_with(
554 '2', session=self.session.Session.return_value,
555 extra_key='extra-value')
558 class CustomConfigFromFile(ClientsConfigTest):
560 def get_file_cfg(self):
562 'username': 'file-username',
563 'password': 'file-password',
564 'tenant_name': 'file-tenant-name',
565 'custom_configuration': {
567 'username': 'custom-username',
568 'password': 'custom-password',
569 'tenant_name': 'custom-tenant-name'
574 def get_inputs_cfg(self):
576 'auth_url': 'envar-auth-url',
577 'username': 'inputs-username',
580 def get_env_cfg(self):
582 'OS_USERNAME': 'envar-username',
583 'OS_PASSWORD': 'envar-password',
584 'OS_TENANT_NAME': 'envar-tenant-name',
585 'OS_AUTH_URL': 'envar-auth-url',
586 common.Config.OPENSTACK_CONFIG_PATH_ENV_VAR: file.name
590 common.NovaClientWithSugar(config=self.get_inputs_cfg())
591 self.loader.load_from_options.assert_called_once_with(
592 username='custom-username',
593 password='custom-password',
594 tenant_name='custom-tenant-name',
595 auth_url='envar-auth-url'
597 self.session.Session.assert_called_with(auth=self.auth, verify=True)
598 self.nova.Client.assert_called_once_with(
599 '2', session=self.session.Session.return_value)
601 def test_neutron(self):
602 common.NeutronClientWithSugar(config=self.get_inputs_cfg())
603 self.loader.load_from_options.assert_called_once_with(
604 username='inputs-username',
605 password='file-password',
606 tenant_name='file-tenant-name',
607 auth_url='envar-auth-url'
609 self.session.Session.assert_called_with(auth=self.auth, verify=True)
610 self.neutron.Client.assert_called_once_with(
611 session=self.session.Session.return_value)
613 def test_cinder(self):
614 common.CinderClientWithSugar(config=self.get_inputs_cfg())
615 self.loader.load_from_options.assert_called_once_with(
616 username='inputs-username',
617 password='file-password',
618 tenant_name='file-tenant-name',
619 auth_url='envar-auth-url'
621 self.session.Session.assert_called_with(auth=self.auth, verify=True)
622 self.cinder.Client.assert_called_once_with(
623 '2', session=self.session.Session.return_value)
626 class PutClientInKwTests(unittest.TestCase):
628 def test_override_prop_empty_ctx(self):
630 ctx = MockCloudifyContext(node_id='a20846', properties=props)
633 'openstack_config': {
637 expected_cfg = kwargs['openstack_config']
639 client_class = mock.MagicMock()
640 common._put_client_in_kw('mock_client', client_class, kwargs)
641 client_class.assert_called_once_with(config=expected_cfg)
643 def test_override_prop_nonempty_ctx(self):
645 'openstack_config': {
650 props_copy = props.copy()
651 ctx = MockCloudifyContext(node_id='a20846', properties=props)
654 'openstack_config': {
665 client_class = mock.MagicMock()
666 common._put_client_in_kw('mock_client', client_class, kwargs)
667 client_class.assert_called_once_with(config=expected_cfg)
668 # Making sure that _put_client_in_kw will not modify
669 # 'openstack_config' property of a node.
670 self.assertEqual(props_copy, ctx.node.properties)
672 def test_override_runtime_prop(self):
674 'openstack_config': {
680 'openstack_config': {
684 props_copy = props.copy()
685 runtime_props_copy = runtime_props.copy()
686 ctx = MockCloudifyContext(node_id='a20847', properties=props,
687 runtime_properties=runtime_props)
695 client_class = mock.MagicMock()
696 common._put_client_in_kw('mock_client', client_class, kwargs)
697 client_class.assert_called_once_with(config=expected_cfg)
698 self.assertEqual(props_copy, ctx.node.properties)
699 self.assertEqual(runtime_props_copy, ctx.instance.runtime_properties)
702 class ResourceQuotaTests(unittest.TestCase):
704 def _test_quota_validation(self, amount, quota, failure_expected):
705 ctx = MockCloudifyContext(node_id='node_id', properties={})
706 client = mock.MagicMock()
708 def mock_cosmo_list(_):
709 return [x for x in range(0, amount)]
710 client.cosmo_list = mock_cosmo_list
712 def mock_get_quota(_):
714 client.get_quota = mock_get_quota
717 self.assertRaisesRegexp(
719 'cannot be created due to quota limitations',
720 common.validate_resource,
721 ctx=ctx, sugared_client=client,
722 openstack_type='openstack_type')
724 common.validate_resource(
725 ctx=ctx, sugared_client=client,
726 openstack_type='openstack_type')
728 def test_equals_quotas(self):
729 self._test_quota_validation(3, 3, True)
731 def test_exceeded_quota(self):
732 self._test_quota_validation(5, 3, True)
734 def test_infinite_quota(self):
735 self._test_quota_validation(5, -1, False)
738 class UseExternalResourceTests(unittest.TestCase):
740 def _test_use_external_resource(self,
744 properties = {'create_if_missing': create_if_missing,
745 'use_external_resource': is_external,
746 'resource_id': 'resource_id'}
747 client_mock = mock.MagicMock()
750 def _raise_error(*_):
751 raise NonRecoverableError('Error')
753 def _return_something(*_):
754 return mock.MagicMock()
756 return_value = _return_something if exists else _raise_error
758 properties.update({'resource_id': 'rid'})
760 node_context = MockCloudifyContext(node_id='a20847',
761 properties=properties)
763 'openstack_plugin_common._get_resource_by_name_or_id_from_ctx',
765 return common.use_external_resource(node_context,
766 client_mock, os_type)
768 def test_use_existing_resource(self):
769 self.assertIsNotNone(self._test_use_external_resource(True, True,
771 self.assertIsNotNone(self._test_use_external_resource(True, False,
774 def test_create_resource(self):
775 self.assertIsNone(self._test_use_external_resource(False, True, False))
776 self.assertIsNone(self._test_use_external_resource(False, False,
778 self.assertIsNone(self._test_use_external_resource(True, True, False))
780 def test_raise_error(self):
781 # If exists and shouldn't it is checked in resource
782 # validation so below scenario is not tested here
783 self.assertRaises(NonRecoverableError,
784 self._test_use_external_resource,
786 create_if_missing=False,
790 class ValidateResourceTests(unittest.TestCase):
792 def _test_validate_resource(self,
796 client_mock_provided=None):
797 properties = {'create_if_missing': create_if_missing,
798 'use_external_resource': is_external,
799 'resource_id': 'resource_id'}
800 client_mock = client_mock_provided or mock.MagicMock()
803 def _raise_error(*_):
804 raise NonRecoverableError('Error')
806 def _return_something(*_):
807 return mock.MagicMock()
808 return_value = _return_something if exists else _raise_error
810 properties.update({'resource_id': 'rid'})
812 node_context = MockCloudifyContext(node_id='a20847',
813 properties=properties)
815 'openstack_plugin_common._get_resource_by_name_or_id_from_ctx',
817 return common.validate_resource(node_context, client_mock, os_type)
819 def test_use_existing_resource(self):
820 self._test_validate_resource(True, True, True)
821 self._test_validate_resource(True, False, True)
823 def test_create_resource(self):
824 client_mock = mock.MagicMock()
825 client_mock.cosmo_list.return_value = ['a', 'b', 'c']
826 client_mock.get_quota.return_value = 5
827 self._test_validate_resource(False, True, False, client_mock)
828 self._test_validate_resource(False, False, False, client_mock)
829 self._test_validate_resource(True, True, False, client_mock)
831 def test_raise_error(self):
832 # If exists and shouldn't it is checked in resource
833 # validation so below scenario is not tested here
834 self.assertRaises(NonRecoverableError,
835 self._test_validate_resource,
837 create_if_missing=False,
840 def test_raise_quota_error(self):
841 client_mock = mock.MagicMock()
842 client_mock.cosmo_list.return_value = ['a', 'b', 'c']
843 client_mock.get_quota.return_value = 3
844 self.assertRaises(NonRecoverableError,
845 self._test_validate_resource,
847 create_if_missing=True,
849 client_mock_provided=client_mock)