diff --git a/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-error-resp.json b/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-error-resp.json new file mode 100644 index 000000000..85ed7e92b --- /dev/null +++ b/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-error-resp.json @@ -0,0 +1,6 @@ +{ + "validate-connection-info": { + "valid": false, + "message": "Unable to establish connection to http://openstack.awesome.our:5000/v3/auth/tokens: HTTPConnectionPool(host='openstack.awesome.our', port=5000): Max retries exceeded with url: /v3/auth/tokens (Caused by NewConnectionError(': Failed to establish a new connection: [Errno -2] No address found',))" + } +} diff --git a/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-req.json b/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-req.json new file mode 100644 index 000000000..b0e217843 --- /dev/null +++ b/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-req.json @@ -0,0 +1,16 @@ +{ + "validate-connection-info": { + "platform": "openstack", + "connection_info": { + "identity_api_version": 3, + "auth_url": "http://openstack.awesome.our:5000/v3", + "username": "admin", + "password": "Passw0rd", + "project_name": "admin", + "user_domain_name": "default", + "project_domain_name": "default", + "allow_untrusted": true + }, + "mapped_regions": [] + } +} diff --git a/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-resp.json b/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-resp.json new file mode 100644 index 000000000..9bb537623 --- /dev/null +++ b/coriolis/api-refs/api_samples/endpoint/endpoint-validate-connection-info-resp.json @@ -0,0 +1,6 @@ +{ + "validate-connection-info": { + "valid": true, + "message": null + } +} diff --git a/coriolis/api-refs/source/endpoint.inc b/coriolis/api-refs/source/endpoint.inc index 762ebc6cb..86c4ae8e4 100644 --- a/coriolis/api-refs/source/endpoint.inc +++ b/coriolis/api-refs/source/endpoint.inc @@ -601,6 +601,53 @@ Response .. literalinclude:: ../api_samples/endpoint/endpoint-validate-connection-error-resp.json :language: javascript +Validate Endpoint Connection Info +================================= + +.. rest_method:: POST /endpoints/actions + +Validates the given connection info for an endpoint platform. This action does +not depend on an existing endpoint and persists nothing, so it can be used to +validate the connection info of an endpoint before creating or updating it. + +Normal response codes: 200 + +Error response codes: badRequest(400), unauthorized(401), forbidden(403), +itemNotFound(404) + +Request +------- + +.. rest_parameters:: parameters.yaml + + - validate-connection-info : validate_connection_info + - platform : validate_connection_platform + - connection_info : connection_info + - mapped_regions : validate_connection_mapped_regions + +**Example of Endpoint Validate Connection Info Request** + .. literalinclude:: ../api_samples/endpoint/endpoint-validate-connection-info-req.json + :language: javascript + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - validate-connection-info : validate_connection_info + - valid : valid_connection + - message : validation_message + +**Example of Endpoint Validate Connection Info Response** + .. literalinclude:: ../api_samples/endpoint/endpoint-validate-connection-info-resp.json + :language: javascript + +.. note:: In case the validation fails, an error message will be returned, as in the example below + +**Example of Failed Endpoint Connection Info Validation Response** + .. literalinclude:: ../api_samples/endpoint/endpoint-validate-connection-info-error-resp.json + :language: javascript + Endpoint Minion Pool Options ============================ diff --git a/coriolis/api-refs/source/parameters.yaml b/coriolis/api-refs/source/parameters.yaml index 6b982029d..c2edb1075 100644 --- a/coriolis/api-refs/source/parameters.yaml +++ b/coriolis/api-refs/source/parameters.yaml @@ -1483,6 +1483,26 @@ validate_connection: in: body type: object required: true +validate_connection_info: + description: | + An object that contains the validate connection info action for the + given endpoint connection info. + in: body + type: object + required: true +validate_connection_mapped_regions: + description: | + List of IDs of the Coriolis regions whose workers should be used to + validate the connection. Any worker can be used if omitted. + in: body + type: array + required: false +validate_connection_platform: + description: | + The type of cloud service platform the connection info is for. + in: body + type: string + required: true validation_message: description: | The error message in case of endpoint validation failure. diff --git a/coriolis/api/v1/endpoint_actions.py b/coriolis/api/v1/endpoint_actions.py index f04c6b8b3..dab6b1439 100644 --- a/coriolis/api/v1/endpoint_actions.py +++ b/coriolis/api/v1/endpoint_actions.py @@ -5,6 +5,7 @@ from coriolis import exception from coriolis.api import wsgi as api_wsgi +from coriolis.api.v1 import utils as api_utils from coriolis.endpoints import api from coriolis.policies import endpoints as endpoint_policies @@ -28,6 +29,38 @@ def _validate_connection(self, req, id, body): except exception.InvalidParameterValue as ex: raise exc.HTTPNotFound(explanation=ex.msg) + @api_utils.format_keyerror_message(resource='endpoint', method='validate') + def _validate_connection_info_body(self, body): + connection_info_body = body["validate-connection-info"] + if not isinstance(connection_info_body, dict): + raise exception.InvalidInput( + 'The "validate-connection-info" body must be an object ' + 'containing the "platform" and "connection_info" of the endpoint' + ) + platform = connection_info_body["platform"] + connection_info = connection_info_body["connection_info"] + mapped_regions = connection_info_body.get("mapped_regions", []) + return (platform, connection_info, mapped_regions) + + @api_wsgi.action('validate-connection-info') + def _validate_connection_info(self, req, body): + context = req.environ['coriolis.context'] + context.can( + "%s:validate_connection_info" % (endpoint_policies.ENDPOINTS_POLICY_PREFIX) + ) + platform, connection_info, mapped_regions = self._validate_connection_info_body( + body + ) + try: + is_valid, message = self._endpoint_api.validate_connection_info( + context, platform, connection_info, mapped_regions + ) + return {"validate-connection-info": {"valid": is_valid, "message": message}} + except exception.NotFound as ex: + raise exc.HTTPNotFound(explanation=ex.msg) + except exception.InvalidParameterValue as ex: + raise exc.HTTPNotFound(explanation=ex.msg) + def create_resource(): return api_wsgi.Resource(EndpointActionsController()) diff --git a/coriolis/api/v1/router.py b/coriolis/api/v1/router.py index 53497a536..0c09562a3 100644 --- a/coriolis/api/v1/router.py +++ b/coriolis/api/v1/router.py @@ -123,6 +123,13 @@ def _setup_routes(self, mapper, ext_mgr): action='action', conditions={'method': 'POST'}, ) + mapper.connect( + 'endpoint_collection_actions', + '/{project_id}/endpoints/actions', + controller=self.resources['endpoint_actions'], + action='action', + conditions={'method': 'POST'}, + ) self.resources['endpoint_instances'] = endpoint_instances.create_resource() mapper.resource( diff --git a/coriolis/api/v1/utils.py b/coriolis/api/v1/utils.py index d660c207f..b80f5b0f1 100644 --- a/coriolis/api/v1/utils.py +++ b/coriolis/api/v1/utils.py @@ -68,6 +68,7 @@ def _build_keyerror_message(resource, method, key): method_mapping = { "create": "creation", "update": "update", + "validate": "validation", } if resource == key: diff --git a/coriolis/conductor/rpc/client.py b/coriolis/conductor/rpc/client.py index 57f33704f..0bc13f3d0 100644 --- a/coriolis/conductor/rpc/client.py +++ b/coriolis/conductor/rpc/client.py @@ -129,6 +129,17 @@ def get_endpoint_inventory_csv(self, ctxt, endpoint_id, source_environment): def validate_endpoint_connection(self, ctxt, endpoint_id): return self._call(ctxt, 'validate_endpoint_connection', endpoint_id=endpoint_id) + def validate_endpoint_connection_info( + self, ctxt, platform, connection_info, mapped_regions=None + ): + return self._call( + ctxt, + 'validate_endpoint_connection_info', + platform=platform, + connection_info=connection_info, + mapped_regions=mapped_regions, + ) + def validate_endpoint_target_environment(self, ctxt, endpoint_id, target_env): return self._call( ctxt, diff --git a/coriolis/conductor/rpc/server.py b/coriolis/conductor/rpc/server.py index 5655ad608..b427812a2 100644 --- a/coriolis/conductor/rpc/server.py +++ b/coriolis/conductor/rpc/server.py @@ -725,16 +725,31 @@ def get_endpoint_inventory_csv(self, ctxt, endpoint_id, source_environment): def validate_endpoint_connection(self, ctxt, endpoint_id): endpoint = self.get_endpoint(ctxt, endpoint_id) + return self._validate_endpoint_connection_info( + ctxt, + endpoint.type, + endpoint.connection_info, + [reg.id for reg in endpoint.mapped_regions], + ) + + def validate_endpoint_connection_info( + self, ctxt, platform, connection_info, mapped_regions=None + ): + return self._validate_endpoint_connection_info( + ctxt, platform, connection_info, mapped_regions + ) + + def _validate_endpoint_connection_info( + self, ctxt, platform, connection_info, mapped_regions=None + ): worker_rpc = self._get_worker_service_rpc_for_specs( ctxt, enabled=True, - region_sets=[[reg.id for reg in endpoint.mapped_regions]], - provider_requirements={endpoint.type: [constants.PROVIDER_TYPE_ENDPOINT]}, + region_sets=[mapped_regions or []], + provider_requirements={platform: [constants.PROVIDER_TYPE_ENDPOINT]}, ) - return worker_rpc.validate_endpoint_connection( - ctxt, endpoint.type, endpoint.connection_info - ) + return worker_rpc.validate_endpoint_connection(ctxt, platform, connection_info) def validate_endpoint_target_environment(self, ctxt, endpoint_id, target_env): endpoint = self.get_endpoint(ctxt, endpoint_id) diff --git a/coriolis/endpoints/api.py b/coriolis/endpoints/api.py index 7b5b05331..5b1523402 100644 --- a/coriolis/endpoints/api.py +++ b/coriolis/endpoints/api.py @@ -37,6 +37,13 @@ def validate_connection(self, ctxt, endpoint_id): ctxt, endpoint_id ) + def validate_connection_info( + self, ctxt, platform, connection_info, mapped_regions=None + ): + return self._rpc_conductor_client.validate_endpoint_connection_info( + ctxt, platform, connection_info, mapped_regions + ) + @utils.bad_request_on_error("Invalid destination environment: %s") def validate_target_environment(self, ctxt, endpoint_id, target_env): return self._rpc_conductor_client.validate_endpoint_target_environment( diff --git a/coriolis/policies/endpoints.py b/coriolis/policies/endpoints.py index fd54493cd..5195d920a 100644 --- a/coriolis/policies/endpoints.py +++ b/coriolis/policies/endpoints.py @@ -52,6 +52,12 @@ def get_endpoints_policy_label(rule_label): "Validate endpoint connection info", [{"path": "/endpoints/{endpoint_id}/actions", "method": "POST"}], ), + policy.DocumentedRuleDefault( + get_endpoints_policy_label('validate_connection_info'), + ENDPOINTS_POLICY_DEFAULT_RULE, + "Validate the given endpoint connection info without an endpoint", + [{"path": "/endpoints/actions", "method": "POST"}], + ), policy.DocumentedRuleDefault( get_endpoints_policy_label('list_instances'), ENDPOINTS_POLICY_DEFAULT_RULE, diff --git a/coriolis/tests/api/v1/test_endpoint_actions.py b/coriolis/tests/api/v1/test_endpoint_actions.py index 203022057..18d7b4fca 100644 --- a/coriolis/tests/api/v1/test_endpoint_actions.py +++ b/coriolis/tests/api/v1/test_endpoint_actions.py @@ -83,3 +83,194 @@ def test_validate_connection_except_invalid_parameter_value( body, ) mock_validate_connection.assert_called_once_with(mock_context, id) + + def test__validate_connection_info_body(self): + body = { + "validate-connection-info": { + "platform": "mock_platform", + "connection_info": "mock_connection_info", + "mapped_regions": ["mock_region"], + } + } + + result = testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info_body + )( + self.endpoint_api, + body, # type: ignore + ) + + self.assertEqual( + ("mock_platform", "mock_connection_info", ["mock_region"]), result + ) + + def test__validate_connection_info_body_no_mapped_regions(self): + body = { + "validate-connection-info": { + "platform": "mock_platform", + "connection_info": "mock_connection_info", + } + } + + result = testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info_body + )( + self.endpoint_api, + body, # type: ignore + ) + + self.assertEqual(("mock_platform", "mock_connection_info", []), result) + + def test__validate_connection_info_body_not_dict(self): + body = {"validate-connection-info": None} + + self.assertRaises( + exception.InvalidInput, + testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info_body + ), + self.endpoint_api, + body, + ) + + def test__validate_connection_info_body_missing_action(self): + body = {"validate-connection": None} + + self.assertRaises( + KeyError, + testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info_body + ), + self.endpoint_api, + body, + ) + + def test__validate_connection_info_body_missing_platform(self): + body = {"validate-connection-info": {"connection_info": "mock_conn_info"}} + + self.assertRaises( + KeyError, + testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info_body + ), + self.endpoint_api, + body, + ) + + def test__validate_connection_info_body_missing_connection_info(self): + body = {"validate-connection-info": {"platform": "mock_platform"}} + + self.assertRaises( + KeyError, + testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info_body + ), + self.endpoint_api, + body, + ) + + @mock.patch.object(api.API, 'validate_connection_info') + @mock.patch.object( + endpoint_actions.EndpointActionsController, '_validate_connection_info_body' + ) + def test_validate_connection_info( + self, mock__validate_connection_info_body, mock_validate_connection_info + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + body = mock.sentinel.body + mock__validate_connection_info_body.return_value = ( + mock.sentinel.platform, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + ) + is_valid = True + message = 'mock_message' + mock_validate_connection_info.return_value = (is_valid, message) + + expected_result = { + "validate-connection-info": {"valid": is_valid, "message": message} + } + result = testutils.get_wrapped_function( + self.endpoint_api._validate_connection_info + )( + mock_req, + body, # type: ignore + ) + + mock_context.can.assert_called_once_with( + 'migration:endpoints:validate_connection_info' + ) + mock__validate_connection_info_body.assert_called_once_with(body) + mock_validate_connection_info.assert_called_once_with( + mock_context, + mock.sentinel.platform, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + ) + self.assertEqual(expected_result, result) + + @mock.patch.object(api.API, 'validate_connection_info') + @mock.patch.object( + endpoint_actions.EndpointActionsController, '_validate_connection_info_body' + ) + def test_validate_connection_info_except_not_found( + self, mock__validate_connection_info_body, mock_validate_connection_info + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + body = mock.sentinel.body + mock__validate_connection_info_body.return_value = ( + mock.sentinel.platform, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + ) + mock_validate_connection_info.side_effect = exception.NotFound + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.endpoint_api._validate_connection_info), + mock_req, + body, + ) + mock_validate_connection_info.assert_called_once_with( + mock_context, + mock.sentinel.platform, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + ) + + @mock.patch.object(api.API, 'validate_connection_info') + @mock.patch.object( + endpoint_actions.EndpointActionsController, '_validate_connection_info_body' + ) + def test_validate_connection_info_except_invalid_parameter_value( + self, mock__validate_connection_info_body, mock_validate_connection_info + ): + mock_req = mock.Mock() + mock_context = mock.Mock() + mock_req.environ = {'coriolis.context': mock_context} + body = mock.sentinel.body + mock__validate_connection_info_body.return_value = ( + mock.sentinel.platform, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + ) + mock_validate_connection_info.side_effect = exception.InvalidParameterValue( + "mock_err" + ) + + self.assertRaises( + exc.HTTPNotFound, + testutils.get_wrapped_function(self.endpoint_api._validate_connection_info), + mock_req, + body, + ) + mock_validate_connection_info.assert_called_once_with( + mock_context, + mock.sentinel.platform, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + ) diff --git a/coriolis/tests/api/v1/test_router.py b/coriolis/tests/api/v1/test_router.py index 4a8a9128d..f4799d94a 100644 --- a/coriolis/tests/api/v1/test_router.py +++ b/coriolis/tests/api/v1/test_router.py @@ -212,6 +212,13 @@ def test_setup_routes( action='action', conditions={'method': 'POST'}, ), + mock.call( + 'endpoint_collection_actions', + '/{project_id}/endpoints/actions', + controller=mock_endpoint_actions_create_resource.return_value, + action='action', + conditions={'method': 'POST'}, + ), mock.call( 'transfer_actions', '/{project_id}/transfers/{id}/actions', diff --git a/coriolis/tests/conductor/rpc/test_client.py b/coriolis/tests/conductor/rpc/test_client.py index bdf4c93b6..882c08680 100644 --- a/coriolis/tests/conductor/rpc/test_client.py +++ b/coriolis/tests/conductor/rpc/test_client.py @@ -118,6 +118,14 @@ def test_validate_endpoint_connection(self): args = {"endpoint_id": "mock_endpoint_id"} self._test(self.client.validate_endpoint_connection, args) + def test_validate_endpoint_connection_info(self): + args = { + "platform": "mock_platform", + "connection_info": "mock_connection_info", + "mapped_regions": "mock_mapped_regions", + } + self._test(self.client.validate_endpoint_connection_info, args) + def test_validate_endpoint_target_environment(self): args = {"endpoint_id": "mock_endpoint_id", "target_env": "mock_target_env"} self._test(self.client.validate_endpoint_target_environment, args) diff --git a/coriolis/tests/conductor/rpc/test_server.py b/coriolis/tests/conductor/rpc/test_server.py index 10e5ef423..66c006690 100644 --- a/coriolis/tests/conductor/rpc/test_server.py +++ b/coriolis/tests/conductor/rpc/test_server.py @@ -614,6 +614,64 @@ def test_validate_endpoint_connection( mock_get_endpoint.return_value.connection_info, ) + @mock.patch.object( + server.ConductorServerEndpoint, "_get_worker_service_rpc_for_specs" + ) + @mock.patch.object(server.ConductorServerEndpoint, "get_endpoint") + def test_validate_endpoint_connection_info( + self, mock_get_endpoint, mock_get_worker_service_rpc_for_specs + ): + result = self.server.validate_endpoint_connection_info( + mock.sentinel.context, + mock.sentinel.platform, + mock.sentinel.connection_info, + ["mock_region"], + ) + + # No endpoint is looked up, since none has to exist. + mock_get_endpoint.assert_not_called() + mock_get_worker_service_rpc_for_specs.assert_called_once_with( + mock.sentinel.context, + enabled=True, + region_sets=[["mock_region"]], + provider_requirements={ + mock.sentinel.platform: [constants.PROVIDER_TYPE_ENDPOINT] + }, + ) + + rpc_return_value = mock_get_worker_service_rpc_for_specs.return_value + rpc_return_value.validate_endpoint_connection.assert_called_once_with( + mock.sentinel.context, + mock.sentinel.platform, + mock.sentinel.connection_info, + ) + self.assertEqual( + rpc_return_value.validate_endpoint_connection.return_value, result + ) + + @mock.patch.object( + server.ConductorServerEndpoint, "_get_worker_service_rpc_for_specs" + ) + @mock.patch.object(server.ConductorServerEndpoint, "get_endpoint") + def test_validate_endpoint_connection_info_no_mapped_regions( + self, mock_get_endpoint, mock_get_worker_service_rpc_for_specs + ): + self.server.validate_endpoint_connection_info( + mock.sentinel.context, + mock.sentinel.platform, + mock.sentinel.connection_info, + ) + + mock_get_endpoint.assert_not_called() + mock_get_worker_service_rpc_for_specs.assert_called_once_with( + mock.sentinel.context, + enabled=True, + region_sets=[[]], + provider_requirements={ + mock.sentinel.platform: [constants.PROVIDER_TYPE_ENDPOINT] + }, + ) + @mock.patch.object( server.ConductorServerEndpoint, "_get_worker_service_rpc_for_specs" ) diff --git a/coriolis/tests/endpoints/test_api.py b/coriolis/tests/endpoints/test_api.py index e19068f3f..9a43560d4 100644 --- a/coriolis/tests/endpoints/test_api.py +++ b/coriolis/tests/endpoints/test_api.py @@ -90,6 +90,24 @@ def test_validate_connection(self): result, ) + def test_validate_connection_info(self): + args = { + "ctxt": mock.sentinel.ctxt, + "platform": mock.sentinel.platform, + "connection_info": mock.sentinel.connection_info, + "mapped_regions": mock.sentinel.mapped_regions, + } + result = self.endpoints_api.validate_connection_info(**args) + ( + self.endpoints_api._rpc_conductor_client.validate_endpoint_connection_info.assert_called_once_with + )(*(args.values())) + self.assertEqual( + ( + self.endpoints_api._rpc_conductor_client.validate_endpoint_connection_info.return_value + ), + result, + ) + def test_validate_target_environment(self): args = { "ctxt": mock.sentinel.ctxt, diff --git a/coriolis/tests/integration/harness.py b/coriolis/tests/integration/harness.py index 5d8571c7e..da4b5f44f 100644 --- a/coriolis/tests/integration/harness.py +++ b/coriolis/tests/integration/harness.py @@ -202,6 +202,9 @@ def _setup_routes(self, mapper, ext_mgr): action_url_pairs = [ ('minion_pool_actions', '/minion_pools/{id}/actions'), ('endpoint_actions', '/endpoints/{id}/actions'), + # The endpoint actions controller also serves the collection, + # for the actions which do not need an existing endpoint. + ('endpoint_actions', '/endpoints/actions'), ('deployment_actions', '/deployments/{id}/actions'), ('transfer_actions', '/transfers/{id}/actions'), ( diff --git a/coriolis/tests/integration/test_endpoints.py b/coriolis/tests/integration/test_endpoints.py index 093c2382a..bda6e2d2f 100644 --- a/coriolis/tests/integration/test_endpoints.py +++ b/coriolis/tests/integration/test_endpoints.py @@ -5,6 +5,7 @@ Exercises endpoint-related operations via the Coriolis REST API: - validate_connection (success and failure) +- validate_connection_info, without an existing endpoint - endpoint update - get_networks - get_storage (list and default) @@ -60,6 +61,46 @@ def test_validate_connection_failure(self): self.assertFalse(valid) self.assertIsNotNone(message) + def _validate_connection_info(self, platform, connection_info): + body = { + "validate-connection-info": { + "platform": platform, + "connection_info": connection_info, + "mapped_regions": [], + } + } + response = self._client.endpoints.client.post( + "/endpoints/actions", json=body + ).json() + result = response["validate-connection-info"] + + return result.get("valid"), result.get("message") + + def test_validate_connection_info(self): + valid, message = self._validate_connection_info( + self._exp_platform, self._exp_conn_info + ) + self.assertTrue(valid, f"source: {message}") + + valid, message = self._validate_connection_info( + self._imp_platform, self._imp_conn_info + ) + self.assertTrue(valid, f"destination: {message}") + + def test_validate_connection_info_failure(self): + endpoints_count = len(self._client.endpoints.list()) + + valid, message = self._validate_connection_info( + self._exp_platform, + { + "pkey_path": "/root/.ssh/coriolis-no-such-key", + }, + ) + self.assertFalse(valid) + self.assertIsNotNone(message) + # Nothing was persisted for the connection info that failed. + self.assertEqual(endpoints_count, len(self._client.endpoints.list())) + def test_endpoint_update(self): updated = self._client.endpoints.update( self._src_endpoint.id, diff --git a/etc/coriolis/policy.yaml b/etc/coriolis/policy.yaml index 7622de595..8b6f91418 100644 --- a/etc/coriolis/policy.yaml +++ b/etc/coriolis/policy.yaml @@ -9,6 +9,7 @@ "migration:endpoints:update": "rule:admin_or_owner" "migration:endpoints:delete": "rule:admin_or_owner" "migration:endpoints:validate_connection": "rule:admin_or_owner" +"migration:endpoints:validate_connection_info": "rule:admin_or_owner" "migration:endpoints:list_instances": "rule:admin_or_owner" "migration:endpoints:get_instance": "rule:admin_or_owner" "migration:endpoints:list_networks": "rule:admin_or_owner"