From c9f1be6133d71e7e012ed9eecb40eb7c5a9a8405 Mon Sep 17 00:00:00 2001 From: Matt Mulsow Date: Tue, 22 Sep 2015 11:37:45 -0500 Subject: Added ignore_other_regions argument to allow modifying endpoints in one region without affecting other regions. --- keystone_service | 56 +++++-- tests/test_keystone_service.py | 351 +++++++++++++++++++++++------------------ 2 files changed, 243 insertions(+), 164 deletions(-) diff --git a/keystone_service b/keystone_service index c53cda2..9764cef 100644 --- a/keystone_service +++ b/keystone_service @@ -41,10 +41,18 @@ options: - allow use of self-signed SSL certificates required: no choices: [ "yes", "no" ] + default: no region: description: - region of service required: no + default: RegionOne + ignore_other_regions: + description: + - allow endpoint to exist in other regions + required: no + choices: [ "yes", "no" ] + default: no state: description: - Indicate desired state of the resource @@ -96,6 +104,7 @@ def authenticate(endpoint, token, login_user, login_password, tenant_name, password=login_password, tenant_name=tenant_name, insecure=insecure) + def get_service(keystone, name): """ Retrieve a service by name """ services = [x for x in keystone.services.list() if x.name == name] @@ -108,11 +117,16 @@ def get_service(keystone, name): return services[0] -def get_endpoint(keystone, name): +def get_endpoint(keystone, name, region, ignore_other_regions): """ Retrieve a service endpoint by name """ service = get_service(keystone, name) endpoints = [x for x in keystone.endpoints.list() if x.service_id == service.id] + + # If this is a multi-region cloud only look at this region's endpoints + if ignore_other_regions: + endpoints = [x for x in endpoints if x.region == region] + count = len(endpoints) if count == 0: raise KeyError("No keystone endpoints with service name %s" % name) @@ -123,7 +137,8 @@ def get_endpoint(keystone, name): def ensure_present(keystone, name, service_type, description, public_url, - internal_url, admin_url, region, check_mode): + internal_url, admin_url, region, ignore_other_regions, + check_mode): """ Ensure the service and its endpoint are present and have the right values. Returns a tuple, where the first element is a boolean that indicates @@ -134,9 +149,9 @@ def ensure_present(keystone, name, service_type, description, public_url, service = None endpoint = None try: service = get_service(keystone, name) - except: pass - try: endpoint = get_endpoint(keystone, name) - except: pass + except KeyError: pass + try: endpoint = get_endpoint(keystone, name, region, ignore_other_regions) + except KeyError: pass changed = False @@ -148,7 +163,8 @@ def ensure_present(keystone, name, service_type, description, public_url, endpoint.region == region if not identical: changed = True - ensure_endpoint_absent(keystone, name, check_mode) + ensure_endpoint_absent(keystone, name, check_mode, region, + ignore_other_regions) endpoint = None # Delete service and its endpoint if the service exists and doesn't match. @@ -158,7 +174,8 @@ def ensure_present(keystone, name, service_type, description, public_url, service.description == description if not identical: changed = True - ensure_endpoint_absent(keystone, name, check_mode) + ensure_endpoint_absent(keystone, name, check_mode, region, + ignore_other_regions) endpoint = None ensure_service_absent(keystone, name, check_mode) service = None @@ -196,6 +213,13 @@ def ensure_service_absent(keystone, name, check_mode): """ Ensure the service is absent""" try: service = get_service(keystone, name) + endpoints = [x for x in keystone.endpoints.list() + if x.service_id == service.id] + + # Don't delete the service if it still has endpoints + if endpoints: + return False + if not check_mode: keystone.services.delete(service.id) return True @@ -204,10 +228,11 @@ def ensure_service_absent(keystone, name, check_mode): return False -def ensure_endpoint_absent(keystone, name, check_mode): +def ensure_endpoint_absent(keystone, name, check_mode, region, + ignore_other_regions): """ Ensure the service endpoint """ try: - endpoint = get_endpoint(keystone, name) + endpoint = get_endpoint(keystone, name, region, ignore_other_regions) if not check_mode: keystone.endpoints.delete(endpoint.id) return True @@ -217,7 +242,8 @@ def ensure_endpoint_absent(keystone, name, check_mode): def dispatch(keystone, name, service_type, description, public_url, - internal_url, admin_url, region, state, check_mode): + internal_url, admin_url, region, ignore_other_regions, state, + check_mode): if state == 'present': (changed, service_id, endpoint_id) = ensure_present( @@ -229,11 +255,13 @@ def dispatch(keystone, name, service_type, description, public_url, internal_url, admin_url, region, + ignore_other_regions, check_mode, ) return dict(changed=changed, service_id=service_id, endpoint_id=endpoint_id) elif state == 'absent': - endpoint_changed = ensure_endpoint_absent(keystone, name, check_mode) + endpoint_changed = ensure_endpoint_absent(keystone, name, check_mode, + region, ignore_other_regions) service_changed = ensure_service_absent(keystone, name, check_mode) return dict(changed=service_changed or endpoint_changed) else: @@ -252,6 +280,7 @@ def main(): internal_url=dict(required=False, aliases=['internalurl']), admin_url=dict(required=False, aliases=['adminurl']), region=dict(required=False, default='RegionOne'), + ignore_other_regions=dict(required=False, default=False, choices=BOOLEANS), state=dict(default='present', choices=['present', 'absent']), endpoint=dict(required=False, default="http://127.0.0.1:35357/v2.0", @@ -286,6 +315,7 @@ def main(): if admin_url is None: admin_url = public_url region = module.params['region'] + ignore_other_regions = module.boolean(module.params['ignore_other_regions']) state = module.params['state'] keystone = authenticate(endpoint, token, login_user, login_password, @@ -294,8 +324,8 @@ def main(): try: d = dispatch(keystone, name, service_type, description, - public_url, internal_url, admin_url, region, state, - check_mode) + public_url, internal_url, admin_url, region, + ignore_other_regions, state, check_mode) except Exception: if check_mode: # If we have a failure in check mode diff --git a/tests/test_keystone_service.py b/tests/test_keystone_service.py index c3e09c5..dce8eb4 100644 --- a/tests/test_keystone_service.py +++ b/tests/test_keystone_service.py @@ -22,23 +22,62 @@ def setup(): return keystone +def setup_multi_region(): + keystone = mock.MagicMock() + + services = [ + mock.Mock(id="b6a7ff03f2574cd9b5c7c61186e0d781", + type="identity", + description="Keystone Identity Service"), + mock.Mock(id="a7ebed35051147d4abbe2ee049eeb346", + type="compute", + description="Compute Service") + ] + + # Can't set field in mock in initializer + services[0].name = "keystone" + services[1].name = "nova" + + keystone.services.list = mock.Mock(return_value=services) + + endpoints = [ + mock.Mock(id="600759628a214eb7b3acde39b1e85180", + service_id="b6a7ff03f2574cd9b5c7c61186e0d781", + publicurl="http://192.168.206.130:5000/v2.0", + internalurl="http://192.168.206.130:5000/v2.0", + adminurl="http://192.168.206.130:35357/v2.0", + region="RegionOne"), + mock.Mock(id="6bdf5ed5e0a54df8b9a049bd263bba0c", + service_id="b6a7ff03f2574cd9b5c7c61186e0d781", + publicurl="http://192.168.206.130:5000/v2.0", + internalurl="http://192.168.206.130:5000/v2.0", + adminurl="http://192.168.206.130:35357/v2.0", + region="RegionTwo"), + mock.Mock(id="cf65cfdc5b5a4fa39bfbe3d7e27f8526", + service_id="a7ebed35051147d4abbe2ee049eeb346", + publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + region="RegionTwo") + ] + + keystone.endpoints.list = mock.Mock(return_value=endpoints) + + return keystone + + @mock.patch('keystone_service.ensure_endpoint_absent') @mock.patch('keystone_service.ensure_service_absent') -@mock.patch('keystone_service.ensure_endpoint_present') -@mock.patch('keystone_service.ensure_service_present') -def test_dispatch_service_present(mock_ensure_service_present, - mock_ensure_endpoint_present, +@mock.patch('keystone_service.ensure_present') +def test_dispatch_service_present(mock_ensure_present, mock_ensure_service_absent, mock_ensure_endpoint_absent): """ Dispatch: service present """ # Setup - mock_ensure_service_present.return_value = (True, None) - mock_ensure_endpoint_present.return_value = (True, None) + mock_ensure_present.return_value = (True, None, None) manager = mock.MagicMock() - manager.attach_mock(mock_ensure_service_present, 'ensure_service_present') + manager.attach_mock(mock_ensure_present, 'ensure_present') manager.attach_mock(mock_ensure_service_absent, 'ensure_service_absent') - manager.attach_mock(mock_ensure_endpoint_present, - 'ensure_endpoint_present') manager.attach_mock(mock_ensure_endpoint_absent, 'ensure_endpoint_absent') @@ -51,43 +90,36 @@ def test_dispatch_service_present(mock_ensure_service_present, internal_url = "http://192.168.206.130:5000/v2.0" admin_url = "http://192.168.206.130:35357/v2.0" region = "RegionOne" + ignore_other_regions = False check_mode = False # Code under test keystone_service.dispatch(keystone, name, service_type, description, - public_url, internal_url, admin_url, region, state, check_mode) - - expected_calls = [mock.call.ensure_service_present(keystone, name, - service_type, - description, - check_mode), - mock.call.ensure_endpoint_present(keystone, name, - public_url, - internal_url, - admin_url, - region, - check_mode)] + public_url, internal_url, admin_url, region, + ignore_other_regions, state, check_mode) + + expected_calls = [mock.call.ensure_present(keystone, name, service_type, + description, public_url, + internal_url, admin_url, region, + ignore_other_regions, + check_mode)] assert_equal(manager.mock_calls, expected_calls) @mock.patch('keystone_service.ensure_endpoint_absent') @mock.patch('keystone_service.ensure_service_absent') -@mock.patch('keystone_service.ensure_endpoint_present') -@mock.patch('keystone_service.ensure_service_present') -def test_dispatch_service_absent(mock_ensure_service_present, - mock_ensure_endpoint_present, - mock_ensure_service_absent, - mock_ensure_endpoint_absent): +@mock.patch('keystone_service.ensure_present') +def test_dispatch_service_absent(mock_ensure_present, + mock_ensure_service_absent, + mock_ensure_endpoint_absent): """ Dispatch: service absent """ # Setup mock_ensure_service_absent.return_value = True mock_ensure_endpoint_absent.return_value = True manager = mock.MagicMock() - manager.attach_mock(mock_ensure_service_present, 'ensure_service_present') + manager.attach_mock(mock_ensure_present, 'ensure_present') manager.attach_mock(mock_ensure_service_absent, 'ensure_service_absent') - manager.attach_mock(mock_ensure_endpoint_present, - 'ensure_endpoint_present') manager.attach_mock(mock_ensure_endpoint_absent, 'ensure_endpoint_absent') @@ -96,6 +128,7 @@ def test_dispatch_service_absent(mock_ensure_service_present, service_type = "identity" description = "Keystone Identity Service" region = "RegionOne" + ignore_other_regions = False state = "absent" public_url = "http://192.168.206.130:5000/v2.0" internal_url = "http://192.168.206.130:5000/v2.0" @@ -104,213 +137,229 @@ def test_dispatch_service_absent(mock_ensure_service_present, # Code under test keystone_service.dispatch(keystone, name, service_type, description, - public_url, internal_url, admin_url, region, state, check_mode) + public_url, internal_url, admin_url, region, + ignore_other_regions, state, check_mode) expected_calls = [ - mock.call.ensure_endpoint_absent(keystone, name, check_mode), + mock.call.ensure_endpoint_absent(keystone, name, check_mode, region, + ignore_other_regions), mock.call.ensure_service_absent(keystone, name, check_mode) ] assert_list_equal(manager.mock_calls, expected_calls) -def test_ensure_service_present_when_present(): - """ ensure_services_present when the service is present""" +def test_ensure_present_when_present(): + """ ensure_present when the service and endpoint are present """ # Setup keystone = setup() name = "keystone" service_type = "identity" description = "Keystone Identity Service" + region = "RegionOne" + ignore_other_regions = False + public_url = "http://192.168.206.130:5000/v2.0" + internal_url = "http://192.168.206.130:5000/v2.0" + admin_url = "http://192.168.206.130:35357/v2.0" check_mode = False # Code under test - (changed, id) = keystone_service.ensure_service_present(keystone, name, - service_type, description, check_mode) + (changed, service_id, endpoint_id) = keystone_service.ensure_present( + keystone, name, service_type, description, public_url, + internal_url, admin_url, region, ignore_other_regions, check_mode) # Assertions assert not changed - assert_equal(id, "b6a7ff03f2574cd9b5c7c61186e0d781") + assert_equal(service_id, "b6a7ff03f2574cd9b5c7c61186e0d781") + assert_equal(endpoint_id, "600759628a214eb7b3acde39b1e85180") + -def test_ensure_service_present_when_present_check(): - """ ensure_services_present when the service is present, check mode""" +def test_ensure_present_when_present_multi_region(): + """ ensure_present when the service and endpoint are present in region """ # Setup - keystone = setup() + keystone = setup_multi_region() name = "keystone" service_type = "identity" description = "Keystone Identity Service" - check_mode = True + region = "RegionOne" + ignore_other_regions = True + public_url = "http://192.168.206.130:5000/v2.0" + internal_url = "http://192.168.206.130:5000/v2.0" + admin_url = "http://192.168.206.130:35357/v2.0" + check_mode = False # Code under test - (changed, id) = keystone_service.ensure_service_present(keystone, name, - service_type, description, check_mode) + (changed, service_id, endpoint_id) = keystone_service.ensure_present( + keystone, name, service_type, description, public_url, + internal_url, admin_url, region, ignore_other_regions, check_mode) # Assertions assert not changed - assert_equal(id, "b6a7ff03f2574cd9b5c7c61186e0d781") + assert_equal(service_id, "b6a7ff03f2574cd9b5c7c61186e0d781") + assert_equal(endpoint_id, "600759628a214eb7b3acde39b1e85180") -def test_ensure_service_present_when_absent(): - """ ensure_services_present when the service is absent""" +def test_ensure_present_when_present_check(): + """ ensure_present when the service and endpoint are present, check mode""" # Setup keystone = setup() - service = mock.Mock(id="a7ebed35051147d4abbe2ee049eeb346") - keystone.services.create = mock.Mock(return_value=service) - name = "nova" - service_type = "compute" - description = "Compute Service" - check_mode = False + name = "keystone" + service_type = "identity" + description = "Keystone Identity Service" + region = "RegionOne" + ignore_other_regions = False + public_url = "http://192.168.206.130:5000/v2.0" + internal_url = "http://192.168.206.130:5000/v2.0" + admin_url = "http://192.168.206.130:35357/v2.0" + check_mode = True # Code under test - (changed, id) = keystone_service.ensure_service_present(keystone, name, - service_type, description, check_mode) + (changed, service_id, endpoint_id) = keystone_service.ensure_present( + keystone, name, service_type, description, public_url, + internal_url, admin_url, region, ignore_other_regions, check_mode) # Assertions - assert changed - assert_equal(id, "a7ebed35051147d4abbe2ee049eeb346") - keystone.services.create.assert_called_with(name=name, - service_type=service_type, - description=description) + assert not changed + assert_equal(service_id, None) + assert_equal(endpoint_id, None) -def test_ensure_service_present_when_absent_check(): - """ ensure_services_present when the service is absent, check mode""" +def test_ensure_present_when_absent(): + """ ensure_present when the service and endpoint are absent """ # Setup keystone = setup() + + # Mock out the service and endpoint creates + endpoint = mock.Mock( + id="622386d836b14fd986d9cec7504d208a", + publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + region="RegionOne") + keystone.endpoints.create = mock.Mock(return_value=endpoint) service = mock.Mock(id="a7ebed35051147d4abbe2ee049eeb346") keystone.services.create = mock.Mock(return_value=service) + name = "nova" service_type = "compute" description = "Compute Service" - check_mode = True - - # Code under test - (changed, id) = keystone_service.ensure_service_present(keystone, name, - service_type, description, check_mode) - - # Assertions - assert changed - assert_equal(id, None) - assert not keystone.services.create.called - - -def test_get_endpoint_present(): - """ get_endpoint when endpoint is present """ - keystone = setup() - - endpoint = keystone_service.get_endpoint(keystone, "keystone") - - assert_equal(endpoint.id, "600759628a214eb7b3acde39b1e85180") - - -def test_ensure_endpoint_present_when_present(): - """ ensure_endpoint_present when the endpoint is present """ - # Setup - keystone = setup() - name = "keystone" - public_url = "http://192.168.206.130:5000/v2.0" - internal_url = "http://192.168.206.130:5000/v2.0" - admin_url = "http://192.168.206.130:35357/v2.0" + public_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" + internal_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" + admin_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" region = "RegionOne" + ignore_other_regions = False check_mode = False # Code under test - (changed, id) = keystone_service.ensure_endpoint_present(keystone, name, - public_url, internal_url, admin_url, region, - check_mode) - - # Assertions - assert not changed - assert_equal(id, "600759628a214eb7b3acde39b1e85180") - - -def test_ensure_endpoint_present_when_present_check(): - """ ensure_endpoint_present when the endpoint is present, check mode""" - # Setup - keystone = setup() - name = "keystone" - public_url = "http://192.168.206.130:5000/v2.0" - internal_url = "http://192.168.206.130:5000/v2.0" - admin_url = "http://192.168.206.130:35357/v2.0" - region = "RegionOne" - check_mode = True - - # Code under test - (changed, id) = keystone_service.ensure_endpoint_present(keystone, name, - public_url, internal_url, admin_url, region, check_mode) + (changed, service_id, endpoint_id) = keystone_service.ensure_present( + keystone, name, service_type, description, public_url, + internal_url, admin_url, region, ignore_other_regions, check_mode) # Assertions - assert not changed - assert_equal(id, "600759628a214eb7b3acde39b1e85180") + assert changed + assert_equal(service_id, "a7ebed35051147d4abbe2ee049eeb346") + keystone.services.create.assert_called_with(name=name, + service_type=service_type, + description=description) + assert_equal(endpoint_id, "622386d836b14fd986d9cec7504d208a") + keystone.endpoints.create.assert_called_with( + service_id="a7ebed35051147d4abbe2ee049eeb346", + publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + region="RegionOne") -def test_ensure_endpoint_present_when_absent(): - """ ensure_endpoint_present when the endpoint is absent """ +def test_ensure_present_when_absent_multi_region(): + """ ensure_present when the endpoint is absent in this region """ # Setup - keystone = setup() - # Mock out the endpoints create - endpoint = mock.Mock(id="622386d836b14fd986d9cec7504d208a", - publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", - internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", - adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", - region="RegionOne") - + keystone = setup_multi_region() + + # Mock out the service and endpoint creates + endpoint = mock.Mock( + id="622386d836b14fd986d9cec7504d208a", + publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + region="RegionOne") keystone.endpoints.create = mock.Mock(return_value=endpoint) - - # We need to add a service, but not an endpoint - service = mock.Mock(id="0ad62de6cfe044c7a77ad3a7f2851b5d", - type="compute", - description="Compute Service") - service.name = "nova" - keystone.services.list.return_value.append(service) + service = mock.Mock(id="a7ebed35051147d4abbe2ee049eeb346") + keystone.services.create = mock.Mock(return_value=service) + keystone.endpoints.delete = mock.Mock() name = "nova" + service_type = "compute" + description = "Compute Service" public_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" internal_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" admin_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" region = "RegionOne" + ignore_other_regions = True check_mode = False # Code under test - (changed, id) = keystone_service.ensure_endpoint_present(keystone, name, - public_url, internal_url, admin_url, region, - check_mode) + (changed, service_id, endpoint_id) = keystone_service.ensure_present( + keystone, name, service_type, description, public_url, + internal_url, admin_url, region, ignore_other_regions, check_mode) # Assertions assert changed - assert_equal(id, "622386d836b14fd986d9cec7504d208a") + assert_equal(service_id, "a7ebed35051147d4abbe2ee049eeb346") + assert not keystone.services.create.called + assert_equal(endpoint_id, "622386d836b14fd986d9cec7504d208a") keystone.endpoints.create.assert_called_with( - service_id="0ad62de6cfe044c7a77ad3a7f2851b5d", - publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", - internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", - adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + service_id="a7ebed35051147d4abbe2ee049eeb346", + publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", region="RegionOne") + assert not keystone.endpoints.delete.called -def test_ensure_endpoint_present_when_absent_check(): - """ ensure_endpoint_present when the endpoint is absent, check mode""" +def test_ensure_present_when_absent_check(): + """ ensure_present when the service and endpoint are absent, check mode """ # Setup keystone = setup() - # We need to add a service, but not an endpoint - service = mock.Mock(id="0ad62de6cfe044c7a77ad3a7f2851b5d", - type="compute", - description="Compute Service") - service.name = "nova" - keystone.services.list.return_value.append(service) + # Mock out the service and endpoint creates + endpoint = mock.Mock( + id="622386d836b14fd986d9cec7504d208a", + publicurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + internalurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + adminurl="http://192.168.206.130:8774/v2/%(tenant_id)s", + region="RegionOne") + keystone.endpoints.create = mock.Mock(return_value=endpoint) + service = mock.Mock(id="a7ebed35051147d4abbe2ee049eeb346") + keystone.services.create = mock.Mock(return_value=service) name = "nova" + service_type = "compute" + description = "Compute Service" public_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" internal_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" admin_url = "http://192.168.206.130:8774/v2/%(tenant_id)s" region = "RegionOne" + ignore_other_regions = False check_mode = True # Code under test - (changed, id) = keystone_service.ensure_endpoint_present(keystone, name, - public_url, internal_url, admin_url, region, check_mode) + (changed, service_id, endpoint_id) = keystone_service.ensure_present( + keystone, name, service_type, description, public_url, + internal_url, admin_url, region, ignore_other_regions, check_mode) # Assertions assert changed - assert_is_none(id) + assert_equal(service_id, None) + assert not keystone.services.create.called + assert_equal(endpoint_id, None) assert not keystone.endpoints.create.called + +def test_get_endpoint_present(): + """ get_endpoint when endpoint is present """ + keystone = setup() + + endpoint = keystone_service.get_endpoint(keystone, "keystone", "RegionOne", + False) + + assert_equal(endpoint.id, "600759628a214eb7b3acde39b1e85180") -- cgit v1.2.1