diff --git a/rdomanager_oscplugin/exceptions.py b/rdomanager_oscplugin/exceptions.py index d91cc8c7d..9b25d1268 100644 --- a/rdomanager_oscplugin/exceptions.py +++ b/rdomanager_oscplugin/exceptions.py @@ -24,3 +24,13 @@ class UnsupportedVersion(Exception): class Timeout(Exception): """An operation timed out""" pass + + +class UnknownService(Exception): + """The service type is unknown""" + pass + + +class NotFound(Exception): + """Resource not found""" + pass diff --git a/rdomanager_oscplugin/tests/test_utils.py b/rdomanager_oscplugin/tests/test_utils.py index eca673b4e..178187017 100644 --- a/rdomanager_oscplugin/tests/test_utils.py +++ b/rdomanager_oscplugin/tests/test_utils.py @@ -15,8 +15,10 @@ from unittest import TestCase +from collections import namedtuple import mock +from rdomanager_oscplugin import exceptions from rdomanager_oscplugin import utils @@ -293,3 +295,378 @@ class TestWaitForDiscovery(TestCase): utils.remove_known_hosts('192.168.0.1') mock_check_call.assert_not_called() + + +class TestRegisterEndpoint(TestCase): + def setUp(self): + self.mock_identity = mock.Mock() + + Project = namedtuple('Project', 'id name') + self.mock_identity.projects.list.return_value = [ + Project(id='123', name='service'), + Project(id='234', name='admin') + ] + + def _role_list_side_effect(*args, **kwargs): + Role = namedtuple('Role', 'id name') + user = kwargs.get('user') + project = kwargs.get('project') + + if user and project: + return Role(id='123', name='admin') + else: + return [ + Role(id='123', name='admin'), + Role(id='345', name='ResellerAdmin'), + ] + self.mock_identity.roles.list.side_effect = _role_list_side_effect + + User = namedtuple('User', 'id name') + self.mock_identity.users.list.return_value = [ + User(id='123', name='nova') + ] + + self.services_create_mock = mock.Mock() + self.mock_identity.services.create.return_value = ( + self.services_create_mock) + + self.endpoints_create_mock = mock.Mock() + self.mock_identity.endpoints.create.return_value = ( + self.endpoints_create_mock) + + self.users_create_mock = mock.Mock() + self.mock_identity.users.create.return_value = ( + self.users_create_mock) + + def test_unknown_service(self): + self.mock_identity.reset_mock() + + self.assertRaises(exceptions.UnknownService, + utils.register_endpoint, + 'unknown_name', + 'unknown_endpoint_type', + 'unknown_url', + self.mock_identity) + + def test_no_admin_role(self): + local_mock_identity = mock.Mock() + local_mock_identity.roles.list.return_value = [] + self.assertRaises(exceptions.NotFound, + utils.register_endpoint, + 'name', + 'compute', + 'url', + local_mock_identity) + + def test_endpoint_is_dashboard(self): + self.mock_identity.reset_mock() + + utils.register_endpoint( + 'name', + 'dashboard', + 'url', + self.mock_identity, + description='description' + ) + + self.mock_identity.roles.list.assert_called_once_with() + + self.mock_identity.services.create.assert_called_once_with( + name='name', + type='dashboard', + description='description', + enabled=True + ) + + self.mock_identity.endpoints.create.assert_called_once_with( + 'regionOne', + self.services_create_mock.id, + "url/", + "url/admin", + "url/" + ) + + def test_endpoint_is_not_dashboard(self): + self.mock_identity.reset_mock() + + utils.register_endpoint( + 'nova', + 'compute', + 'url', + self.mock_identity, + description='description' + ) + + assert not self.mock_identity.users.create.called + self.mock_identity.users.list.assert_called_once_with() + + self.mock_identity.projects.list.assert_called_once_with() + + self.mock_identity.roles.list.assert_has_calls([ + mock.call(), + mock.call(user='123', project='123') + ]) + + self.mock_identity.services.create.assert_called_once_with( + name='nova', + type='compute', + description='description', + enabled=True + ) + + self.mock_identity.endpoints.create.assert_called_once_with( + 'regionOne', + self.services_create_mock.id, + "url/v2/$(tenant_id)s", + "url/v2/$(tenant_id)s", + "url/v2/$(tenant_id)s" + ) + + def test_endpoint_is_metering(self): + self.mock_identity.reset_mock() + + utils.register_endpoint( + 'ceilometer', + 'metering', + 'url', + self.mock_identity, + description='description', + password='password' + ) + + self.mock_identity.users.list.assert_called_once_with() + + self.mock_identity.users.create.assert_called_once_with( + name='ceilometer', + domain=None, + default_project='123', + password='password', + email='nobody@example.com', + description=None, + enabled=True + ) + self.mock_identity.services.create.assert_called_once_with( + name='ceilometer', + type='metering', + description='description', + enabled=True + ) + + self.mock_identity.endpoints.create.assert_called_once_with( + 'regionOne', + self.services_create_mock.id, + "url/", + "url/", + "url/" + ) + + self.mock_identity.roles.list.assert_has_calls([ + mock.call(), + mock.call(user=self.users_create_mock.id, project='123'), + mock.call(user=self.users_create_mock.id, project='234'), + ]) + + self.mock_identity.projects.list.assert_called_once_with() + + +class TestSetupEndpoints(TestCase): + def setUp(self): + self.mock_identity = mock.Mock() + + @mock.patch('rdomanager_oscplugin.utils.register_endpoint') + def test_setup_endpoints_all_ssl(self, mock_register_endpoint): + passwords = utils.generate_overcloud_passwords() + utils.setup_endpoints( + '127.0.0.1', + passwords, + self.mock_identity, + region='regionOne', + enable_horizon=True, + ssl='127.0.0.2' + ) + + # import sys + # print(mock_register_endpoint.mock_calls, file=sys.stderr) + mock_register_endpoint.assert_has_calls([ + mock.call('ceilometer', 'metering', 'https://127.0.0.2:8777', + self.mock_identity, + internal_url='http://127.0.0.1:13777', + description='Ceilometer Service', + password=passwords['OVERCLOUD_CEILOMETER_PASSWORD'], + region='regionOne'), + mock.call('cinder', 'volume', 'https://127.0.0.2:8776', + self.mock_identity, + internal_url='http://127.0.0.1:13776', + description='Cinder Volume Service', + password=passwords['OVERCLOUD_CINDER_PASSWORD'], + region='regionOne'), + mock.call('cinderv2', 'volumev2', 'https://127.0.0.2:8776', + self.mock_identity, + internal_url='http://127.0.0.1:13776', + description='Cinder Volume Service V2', + password=passwords['OVERCLOUD_CINDER_PASSWORD'], + region='regionOne'), + mock.call('ec2', 'ec2', 'https://127.0.0.2:8773', + self.mock_identity, + internal_url='http://127.0.0.1:13773', + description='EC2 Compatibility Layer', + region='regionOne'), + mock.call('glance', 'image', 'https://127.0.0.2:9292', + self.mock_identity, + internal_url='http://127.0.0.1:13292', + description='Glance Image Service', + password=passwords['OVERCLOUD_GLANCE_PASSWORD'], + region='regionOne'), + mock.call('heat', 'orchestration', 'https://127.0.0.2:8004', + self.mock_identity, + internal_url='http://127.0.0.1:13004', + description='Heat Service', + password=passwords['OVERCLOUD_HEAT_PASSWORD'], + region='regionOne'), + mock.call('neutron', 'network', 'https://127.0.0.2:9696', + self.mock_identity, + internal_url='http://127.0.0.1:13696', + description='Neutron Service', + password=passwords['OVERCLOUD_NEUTRON_PASSWORD'], + region='regionOne'), + mock.call('nova', 'compute', 'https://127.0.0.2:8774', + self.mock_identity, + internal_url='http://127.0.0.1:13774', + description='Nova Compute Service', + password=passwords['OVERCLOUD_NOVA_PASSWORD'], + region='regionOne'), + mock.call('nova', 'computev3', 'https://127.0.0.2:8774', + self.mock_identity, + internal_url='http://127.0.0.1:13774', + description='Nova Compute Service v3', + password=passwords['OVERCLOUD_NOVA_PASSWORD'], + region='regionOne'), + mock.call('swift', 'object-store', 'https://127.0.0.2:8080', + self.mock_identity, + internal_url='http://127.0.0.1:13080', + description='Swift Object Storage Service', + password=passwords['OVERCLOUD_SWIFT_PASSWORD'], + region='regionOne'), + # Tuskar not enabled yet + # mock.call('tuskar', 'management', 'https://127.0.0.2:8585', + # self.mock_identity, + # internal_url='http://127.0.0.1:8585', + # description='Tuskar Service', + # password=passwords['OVERCLOUD_TUSKAR_PASSWORD'], + # region='regionOne'), + mock.call('horizon', 'dashboard', 'http://127.0.0.1:', + self.mock_identity, + description='OpenStack Dashboard', + internal_url='http://127.0.0.1:', + region='regionOne') + ]) + + @mock.patch('rdomanager_oscplugin.utils.register_endpoint') + def test_setup_endpoints_all_no_ssl(self, mock_register_endpoint): + passwords = utils.generate_overcloud_passwords() + utils.setup_endpoints( + '127.0.0.1', + passwords, + self.mock_identity, + region='regionOne', + enable_horizon=True, + public='127.0.0.3' + ) + + # import sys + # print(mock_register_endpoint.mock_calls, file=sys.stderr) + mock_register_endpoint.assert_has_calls([ + mock.call('ceilometer', 'metering', 'http://127.0.0.3:8777', + self.mock_identity, + internal_url='http://127.0.0.1:8777', + description='Ceilometer Service', + password=passwords['OVERCLOUD_CEILOMETER_PASSWORD'], + region='regionOne'), + mock.call('cinder', 'volume', 'http://127.0.0.3:8776', + self.mock_identity, + internal_url='http://127.0.0.1:8776', + description='Cinder Volume Service', + password=passwords['OVERCLOUD_CINDER_PASSWORD'], + region='regionOne'), + mock.call('cinderv2', 'volumev2', 'http://127.0.0.3:8776', + self.mock_identity, + internal_url='http://127.0.0.1:8776', + description='Cinder Volume Service V2', + password=passwords['OVERCLOUD_CINDER_PASSWORD'], + region='regionOne'), + mock.call('ec2', 'ec2', 'http://127.0.0.3:8773', + self.mock_identity, + internal_url='http://127.0.0.1:8773', + description='EC2 Compatibility Layer', + region='regionOne'), + mock.call('glance', 'image', 'http://127.0.0.3:9292', + self.mock_identity, + internal_url='http://127.0.0.1:9292', + description='Glance Image Service', + password=passwords['OVERCLOUD_GLANCE_PASSWORD'], + region='regionOne'), + mock.call('heat', 'orchestration', 'http://127.0.0.3:8004', + self.mock_identity, + internal_url='http://127.0.0.1:8004', + description='Heat Service', + password=passwords['OVERCLOUD_HEAT_PASSWORD'], + region='regionOne'), + mock.call('neutron', 'network', 'http://127.0.0.3:9696', + self.mock_identity, + internal_url='http://127.0.0.1:9696', + description='Neutron Service', + password=passwords['OVERCLOUD_NEUTRON_PASSWORD'], + region='regionOne'), + mock.call('nova', 'compute', 'http://127.0.0.3:8774', + self.mock_identity, + internal_url='http://127.0.0.1:8774', + description='Nova Compute Service', + password=passwords['OVERCLOUD_NOVA_PASSWORD'], + region='regionOne'), + mock.call('nova', 'computev3', 'http://127.0.0.3:8774', + self.mock_identity, + internal_url='http://127.0.0.1:8774', + description='Nova Compute Service v3', + password=passwords['OVERCLOUD_NOVA_PASSWORD'], + region='regionOne'), + mock.call('swift', 'object-store', 'http://127.0.0.3:8080', + self.mock_identity, + internal_url='http://127.0.0.1:8080', + description='Swift Object Storage Service', + password=passwords['OVERCLOUD_SWIFT_PASSWORD'], + region='regionOne'), + # Tuskar not enabled yet + # mock.call('tuskar', 'management', 'https://127.0.0.2:8585', + # self.mock_identity, + # internal_url='http://127.0.0.1:8585', + # description='Tuskar Service', + # password=passwords['OVERCLOUD_TUSKAR_PASSWORD'], + # region='regionOne'), + mock.call('horizon', 'dashboard', 'http://127.0.0.1:', + self.mock_identity, + description='OpenStack Dashboard', + internal_url='http://127.0.0.1:', + region='regionOne') + ]) + + @mock.patch('rdomanager_oscplugin.utils.register_endpoint') + def test_setup_endpoints_skip_no_password(self, mock_register_endpoint): + mock_register_endpoint.reset_mock() + + passwords = dict((password, 'password') for password in ( + "OVERCLOUD_GLANCE_PASSWORD", + "OVERCLOUD_HEAT_PASSWORD", + "OVERCLOUD_NEUTRON_PASSWORD", + "OVERCLOUD_NOVA_PASSWORD", + )) + + utils.setup_endpoints( + '127.0.0.1', + passwords, + self.mock_identity, + region='regionOne', + enable_horizon=True, + ssl='127.0.0.2' + ) + + self.assertEqual(mock_register_endpoint.call_count, 7) diff --git a/rdomanager_oscplugin/tests/v1/overcloud_deploy/test_overcloud_deploy.py b/rdomanager_oscplugin/tests/v1/overcloud_deploy/test_overcloud_deploy.py index 29ef68624..d5a04434e 100644 --- a/rdomanager_oscplugin/tests/v1/overcloud_deploy/test_overcloud_deploy.py +++ b/rdomanager_oscplugin/tests/v1/overcloud_deploy/test_overcloud_deploy.py @@ -27,6 +27,7 @@ class TestDeployOvercloud(fakes.TestDeployOvercloud): # Get the command object to test self.cmd = overcloud_deploy.DeployOvercloud(self.app, None) + @mock.patch('rdomanager_oscplugin.utils.setup_endpoints') @mock.patch('time.sleep', return_value=None) @mock.patch('os_cloud_config.keystone.initialize') @mock.patch('rdomanager_oscplugin.utils.remove_known_hosts') @@ -44,7 +45,7 @@ class TestDeployOvercloud(fakes.TestDeployOvercloud): mock_get_templte_contents, mock_process_multiple_env, set_nodes_state_mock, wait_for_stack_ready_mock, mock_remove_known_hosts, mock_keystone_initialize, - mock_sleep): + mock_sleep, mock_setup_endpoints): arglist = ['--use-tripleo-heat-templates', ] verifylist = [ diff --git a/rdomanager_oscplugin/utils.py b/rdomanager_oscplugin/utils.py index 1854d0867..1b4312b5a 100644 --- a/rdomanager_oscplugin/utils.py +++ b/rdomanager_oscplugin/utils.py @@ -24,6 +24,8 @@ import sys import time import uuid +from rdomanager_oscplugin import exceptions + def _generate_password(): """Create a random password @@ -327,5 +329,246 @@ def remove_known_hosts(overcloud_ip): subprocess.check_call(command) -def setup_endpoints(overcloud_ip, passwords): - pass +def register_endpoint(name, + endpoint_type, + public_url, + identity_client, + password=None, + description=None, + admin_url=None, + internal_url=None, + region="regionOne"): + SUFFIXES = { + 'baremetal': {'suffix': '/'}, + 'compute': {'suffix': "/v2/$(tenant_id)s"}, + 'computev3': {'suffix': "/v3"}, + 'dashboard': {'suffix': "/", + 'admin_suffix': "/admin"}, + 'ec2': {'suffix': '/services/Cloud', + 'admin_suffix': '/service/Admin'}, + 'identity': {'suffix': "/v2.0"}, + 'image': {'suffix': '/'}, + 'management': {'suffix': "/v2"}, + 'metering': {'suffix': '/'}, + 'network': {'suffix': '/'}, + 'object-store': {'suffix': "/v1/AUTH_%%(tenant_id)s", + 'admin_suffix': "/v1"}, + 'orchestration': {'suffix': "/v1/%%(tenant_id)s"}, + 'volume': {'suffix': "/v1/%%(tenant_id)s"}, + 'volumev2': {'suffix': "/v2/%%(tenant_id)s"}, + } + + service = SUFFIXES.get(endpoint_type) + + if not service: + raise exceptions.UnknownService + + suffix = service['suffix'] + admin_suffix = service.get('admin_suffix', suffix) + + if not internal_url: + internal_url = public_url + + if not admin_url: + admin_url = internal_url + + roles = identity_client.roles.list() + admin_role_id = next((role.id for role in roles if role.name in 'admin'), + None) + if not admin_role_id: + raise exceptions.NotFound + + if endpoint_type not in 'dashboard': + projects = identity_client.projects.list() + service_project_id = next(project.id for project in + projects if project.name in 'service') + + if not password: + password = _generate_password() + + # Some services have multiple endpoints, the user doesn't need to + # be recreated + users = identity_client.users.list() + user_id = next((user.id for user in users if user.name in name), None) + if not user_id: + user = identity_client.users.create( + name=name, + domain=None, + default_project=service_project_id, + password=password, + email='nobody@example.com', + description=None, + enabled=True + ) + user_id = user.id + role = identity_client.roles.list( + user=user_id, + project=service_project_id) + if not role: + # Log "Creating user-role assignment for user $NAME, role admin, + # tenant service" + identity_client.roles.grant( + admin_role_id, + user=user_id, + project=service_project_id + ) + + # Add the admin tenant role for ceilometer user to enable polling + # services + if endpoint_type in 'metering': + admin_project_id = next(project.id for project in + projects if project.name in 'admin') + # Log "Creating user-role assignment for user $NAME, role admin, + # tenant admin" + role = identity_client.roles.list( + user=user_id, + project=admin_project_id + ) + if not role: + identity_client.roles.grant( + admin_role_id, + user=user_id, + project=admin_project_id + ) + + # swift polling requires ResellerAdmin role to be added to the + # Ceilometer user + reseller_admin_role_id = next(role.id for role in roles if + role.name in 'ResellerAdmin') + identity_client.roles.grant( + reseller_admin_role_id, + user=user_id, + project=admin_project_id + ) + + service = identity_client.services.create( + name=name, + type=endpoint_type, + description=description, + enabled=True + ) + # Assumes v2 identity_client + identity_client.endpoints.create( + region, + service.id, + "%s%s" % (public_url, suffix), + "%s%s" % (admin_url, admin_suffix), + "%s%s" % (internal_url, suffix) + ) + # Log "Service $TYPE created" + + +def setup_endpoints(overcloud_ip, + passwords, + identity_client, + region='regionOne', + enable_horizon=False, + ssl=None, + public=None): + """Perform initial setup of a cloud running on + + This will register ec2, image, orchestration, identity, network, + volume (optional), dashboard (optional), metering (optional) and + compute services as running on the default ports on controlplane-ip. + """ + + SERVICE_LIST = [ + {'name': 'ceilometer', 'type': 'metering', + 'description': 'Ceilometer Service', + 'port': 8777, 'ssl_port': 13777, + 'password_field': 'OVERCLOUD_CEILOMETER_PASSWORD'}, + {'name': 'cinder', 'type': 'volume', + 'description': 'Cinder Volume Service', + 'port': 8776, 'ssl_port': 13776, + 'password_field': 'OVERCLOUD_CINDER_PASSWORD'}, + {'name': 'cinderv2', 'type': 'volumev2', + 'description': 'Cinder Volume Service V2', + 'port': 8776, 'ssl_port': 13776, + 'password_field': 'OVERCLOUD_CINDER_PASSWORD'}, + {'name': 'ec2', 'type': 'ec2', + 'description': 'EC2 Compatibility Layer', + 'port': 8773, 'ssl_port': 13773}, + {'name': 'glance', 'type': 'image', + 'description': 'Glance Image Service', + 'port': 9292, 'ssl_port': 13292, + 'password_field': 'OVERCLOUD_GLANCE_PASSWORD'}, + {'name': 'heat', 'type': 'orchestration', + 'description': 'Heat Service', + 'port': 8004, 'ssl_port': 13004, + 'password_field': 'OVERCLOUD_HEAT_PASSWORD'}, + {'name': 'ironic', 'type': 'baremetal', + 'description': 'Ironic Service', + 'port': 6385, 'ssl_port': 6385, + 'password_field': 'OVERCLOUD_IRONIC_PASSWORD'}, + {'name': 'neutron', 'type': 'network', + 'description': 'Neutron Service', + 'port': 9696, 'ssl_port': 13696, + 'password_field': 'OVERCLOUD_NEUTRON_PASSWORD'}, + {'name': 'nova', 'type': 'compute', + 'description': 'Nova Compute Service', + 'port': 8774, 'ssl_port': 13774, + 'password_field': 'OVERCLOUD_NOVA_PASSWORD'}, + {'name': 'nova', 'type': 'computev3', + 'description': 'Nova Compute Service v3', + 'port': 8774, 'ssl_port': 13774, + 'password_field': 'OVERCLOUD_NOVA_PASSWORD'}, + {'name': 'swift', 'type': 'object-store', + 'description': 'Swift Object Storage Service', + 'port': 8080, 'ssl_port': 13080, + 'password_field': 'OVERCLOUD_SWIFT_PASSWORD'}, + {'name': 'tuskar', 'type': 'management', + 'description': 'Tuskar Service', + 'port': 8585, 'ssl_port': 8585, + 'password_field': 'OVERCLOUD_TUSKAR_PASSWORD'}, + ] + + skip_no_password = [ + 'metering', + 'volume', + 'volumev2', + 'object-store', + 'baremetal', + 'management' + ] + + internal_host = 'http://%s:' % overcloud_ip + + if ssl: + public_host = "https://%s:" % ssl + elif public: + public_host = "http://%s:" % public + else: + public_host = internal_host + + for service in SERVICE_LIST: + password_field = service.get('password_field', None) + password = passwords.get(password_field, None) + + if not password and service['type'] in skip_no_password: + continue + + port = service['port'] + ssl_port = service['ssl_port'] if ssl else port + args = ( + service['name'], + service['type'], + "%s%d" % (public_host, port), + identity_client, + ) + kwargs = { + 'description': service['description'], + 'region': region, + 'internal_url': "%s%d" % (internal_host, ssl_port), + } + + if password: + kwargs.update({'password': password}) + + register_endpoint(*args, **kwargs) + + if enable_horizon: + # Horizon is different enough to warrant a separate case + register_endpoint('horizon', 'dashboard', internal_host, + identity_client, description="OpenStack Dashboard", + internal_url=internal_host, + region=region) diff --git a/rdomanager_oscplugin/v1/overcloud_deploy.py b/rdomanager_oscplugin/v1/overcloud_deploy.py index 31a791c0e..032528ad3 100644 --- a/rdomanager_oscplugin/v1/overcloud_deploy.py +++ b/rdomanager_oscplugin/v1/overcloud_deploy.py @@ -265,7 +265,7 @@ class DeployOvercloud(command.Command): except ksc_exc.Conflict: pass - utils.setup_endpoints(overcloud_ip, self.passwords) + utils.setup_endpoints(overcloud_ip, self.passwords, identity_client) try: identity_client.roles.create(name='heat_stack_user')