Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
{
"validate-connection": null
"validate-connection": {
"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": []
}
}
17 changes: 8 additions & 9 deletions coriolis/api-refs/source/endpoint.inc
Original file line number Diff line number Diff line change
Expand Up @@ -556,18 +556,15 @@ Endpoint Actions
Validate Endpoint Connection
============================

.. rest_method:: POST /endpoints/{endpoint_id}/actions

Validates connection to endpoint.

**Preconditions**

The endpoint must exist.
.. rest_method:: POST /endpoints/actions

Validates the connection info for an endpoint platform, without requiring the
endpoint to exist. Nothing is persisted, so this can be used to validate the
connection info of an endpoint before creating or updating it.

Normal response codes: 200

Error response codes: unauthorized(401), forbidden(403),
Error response codes: badRequest(400), unauthorized(401), forbidden(403),
itemNotFound(404)

Request
Expand All @@ -576,7 +573,9 @@ Request
.. rest_parameters:: parameters.yaml

- validate-connection : validate_connection
- endpoint_id : endpoint_id_path
- platform : validate_connection_platform
- connection_info : connection_info
- mapped_regions : validate_connection_mapped_regions

**Example of Endpoint Validate Connection Request**
.. literalinclude:: ../api_samples/endpoint/endpoint-validate-connection-req.json
Expand Down
17 changes: 15 additions & 2 deletions coriolis/api-refs/source/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1478,11 +1478,24 @@ valid_connection:
required: true
validate_connection:
description: |
An object that contains the validate connection action for the
endpoint connection.
An object that contains the validate connection action, holding the
endpoint platform and connection info to be validated.
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.
Expand Down
21 changes: 19 additions & 2 deletions coriolis/api/v1/endpoint_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -14,14 +15,30 @@ def __init__(self):
self._endpoint_api = api.API()
super(EndpointActionsController, self).__init__()

@api_utils.format_keyerror_message(resource='endpoint', method='validate')
def _validate_connection_body(self, body):
validate_connection = body["validate-connection"]
if not isinstance(validate_connection, dict):
raise exception.InvalidInput(
'The "validate-connection" body must be an object containing '
'the "platform" and "connection_info" of the endpoint'
)
platform = validate_connection["platform"]
connection_info = validate_connection["connection_info"]
mapped_regions = validate_connection.get("mapped_regions", [])
return (platform, connection_info, mapped_regions)

@api_wsgi.action('validate-connection')
def _validate_connection(self, req, id, body):
def _validate_endpoint(self, req, body):
context = req.environ['coriolis.context']
context.can(
"%s:validate_connection" % (endpoint_policies.ENDPOINTS_POLICY_PREFIX)
)
platform, connection_info, mapped_regions = self._validate_connection_body(body)
try:
is_valid, message = self._endpoint_api.validate_connection(context, id)
is_valid, message = self._endpoint_api.validate_connection(
context, platform, connection_info, mapped_regions
)
return {"validate-connection": {"valid": is_valid, "message": message}}
except exception.NotFound as ex:
raise exc.HTTPNotFound(explanation=ex.msg)
Expand Down
3 changes: 1 addition & 2 deletions coriolis/api/v1/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,9 @@ def _setup_routes(self, mapper, ext_mgr):

endpoint_actions_resource = endpoint_actions.create_resource()
self.resources['endpoint_actions'] = endpoint_actions_resource
endpoint_path = '/{project_id}/endpoints/{id}'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this API? We can add a new one that validates the connection info, while still allowing an existing endpoint to be validated.

mapper.connect(
'endpoint_actions',
endpoint_path + '/actions',
'/{project_id}/endpoints/actions',
controller=self.resources['endpoint_actions'],
action='action',
conditions={'method': 'POST'},
Expand Down
1 change: 1 addition & 0 deletions coriolis/api/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def _build_keyerror_message(resource, method, key):
method_mapping = {
"create": "creation",
"update": "update",
"validate": "validation",
}

if resource == key:
Expand Down
12 changes: 10 additions & 2 deletions coriolis/conductor/rpc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,16 @@ def get_endpoint_inventory_csv(self, ctxt, endpoint_id, source_environment):
source_environment=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(
self, ctxt, platform, connection_info, mapped_regions
):
return self._call(
ctxt,
'validate_endpoint_connection',
platform=platform,
connection_info=connection_info,
mapped_regions=mapped_regions,
)

def validate_endpoint_target_environment(self, ctxt, endpoint_id, target_env):
return self._call(
Expand Down
14 changes: 6 additions & 8 deletions coriolis/conductor/rpc/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,19 +722,17 @@ def get_endpoint_inventory_csv(self, ctxt, endpoint_id, source_environment):
ctxt, endpoint.type, endpoint.connection_info, source_environment
)

def validate_endpoint_connection(self, ctxt, endpoint_id):
endpoint = self.get_endpoint(ctxt, endpoint_id)

def validate_endpoint_connection(
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)
Expand Down
4 changes: 2 additions & 2 deletions coriolis/endpoints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def get_endpoints(self, ctxt):
def get_endpoint(self, ctxt, endpoint_id):
return self._rpc_conductor_client.get_endpoint(ctxt, endpoint_id)

def validate_connection(self, ctxt, endpoint_id):
def validate_connection(self, ctxt, platform, connection_info, mapped_regions):
return self._rpc_conductor_client.validate_endpoint_connection(
ctxt, endpoint_id
ctxt, platform, connection_info, mapped_regions
)

@utils.bad_request_on_error("Invalid destination environment: %s")
Expand Down
2 changes: 1 addition & 1 deletion coriolis/policies/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_endpoints_policy_label(rule_label):
get_endpoints_policy_label('validate_connection'),
ENDPOINTS_POLICY_DEFAULT_RULE,
"Validate endpoint connection info",
[{"path": "/endpoints/{endpoint_id}/actions", "method": "POST"}],
[{"path": "/endpoints/actions", "method": "POST"}],
),
policy.DocumentedRuleDefault(
get_endpoints_policy_label('list_instances'),
Expand Down
137 changes: 121 additions & 16 deletions coriolis/tests/api/v1/test_endpoint_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,68 +18,173 @@ def setUp(self):
super(EndpointActionsControllerTestCase, self).setUp()
self.endpoint_api = endpoint_actions.EndpointActionsController()

def test__validate_connection_body(self):
body = {
"validate-connection": {
"platform": "mock_platform",
"connection_info": "mock_connection_info",
"mapped_regions": ["mock_region"],
}
}

result = testutils.get_wrapped_function(
self.endpoint_api._validate_connection_body
)(
self.endpoint_api,
body, # type: ignore
)

self.assertEqual(
("mock_platform", "mock_connection_info", ["mock_region"]), result
)

def test__validate_connection_body_no_mapped_regions(self):
body = {
"validate-connection": {
"platform": "mock_platform",
"connection_info": "mock_connection_info",
}
}

result = testutils.get_wrapped_function(
self.endpoint_api._validate_connection_body
)(
self.endpoint_api,
body, # type: ignore
)

self.assertEqual(("mock_platform", "mock_connection_info", []), result)

def test__validate_connection_body_not_dict(self):
body = {"validate-connection": None}

self.assertRaises(
exception.InvalidInput,
testutils.get_wrapped_function(self.endpoint_api._validate_connection_body),
self.endpoint_api,
body,
)

def test__validate_connection_body_missing_platform(self):
body = {"validate-connection": {"connection_info": "mock_connection_info"}}

self.assertRaises(
KeyError,
testutils.get_wrapped_function(self.endpoint_api._validate_connection_body),
self.endpoint_api,
body,
)

def test__validate_connection_body_missing_connection_info(self):
body = {"validate-connection": {"platform": "mock_platform"}}

self.assertRaises(
KeyError,
testutils.get_wrapped_function(self.endpoint_api._validate_connection_body),
self.endpoint_api,
body,
)

@mock.patch.object(api.API, 'validate_connection')
def test_validate_connection(self, mock_validate_connection):
@mock.patch.object(
endpoint_actions.EndpointActionsController, '_validate_connection_body'
)
def test_validate_endpoint(
self, mock__validate_connection_body, mock_validate_connection
):
mock_req = mock.Mock()
mock_context = mock.Mock()
mock_req.environ = {'coriolis.context': mock_context}
id = mock.sentinel.id
body = mock.sentinel.body
mock__validate_connection_body.return_value = (
mock.sentinel.platform,
mock.sentinel.connection_info,
mock.sentinel.mapped_regions,
)
is_valid = True
message = 'mock_message'
mock_validate_connection.return_value = (is_valid, message)

expected_result = {
"validate-connection": {"valid": is_valid, "message": message}
}
result = testutils.get_wrapped_function(self.endpoint_api._validate_connection)(
result = testutils.get_wrapped_function(self.endpoint_api._validate_endpoint)(
mock_req,
id,
body, # type: ignore
)

mock_context.can.assert_called_once_with(
'migration:endpoints:validate_connection'
)
mock_validate_connection.assert_called_once_with(mock_context, id)
mock__validate_connection_body.assert_called_once_with(body)
mock_validate_connection.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')
def test_validate_connection_except_not_found(self, mock_validate_connection):
@mock.patch.object(
endpoint_actions.EndpointActionsController, '_validate_connection_body'
)
def test_validate_endpoint_except_not_found(
self, mock__validate_connection_body, mock_validate_connection
):
mock_req = mock.Mock()
mock_context = mock.Mock()
mock_req.environ = {'coriolis.context': mock_context}
id = mock.sentinel.id
body = mock.sentinel.body
mock__validate_connection_body.return_value = (
mock.sentinel.platform,
mock.sentinel.connection_info,
mock.sentinel.mapped_regions,
)
mock_validate_connection.side_effect = exception.NotFound

self.assertRaises(
exc.HTTPNotFound,
testutils.get_wrapped_function(self.endpoint_api._validate_connection),
testutils.get_wrapped_function(self.endpoint_api._validate_endpoint),
mock_req,
id,
body,
)
mock_validate_connection.assert_called_once_with(mock_context, id)
mock_validate_connection.assert_called_once_with(
mock_context,
mock.sentinel.platform,
mock.sentinel.connection_info,
mock.sentinel.mapped_regions,
)

@mock.patch.object(api.API, 'validate_connection')
def test_validate_connection_except_invalid_parameter_value(
self, mock_validate_connection
@mock.patch.object(
endpoint_actions.EndpointActionsController, '_validate_connection_body'
)
def test_validate_endpoint_except_invalid_parameter_value(
self, mock__validate_connection_body, mock_validate_connection
):
mock_req = mock.Mock()
mock_context = mock.Mock()
mock_req.environ = {'coriolis.context': mock_context}
id = mock.sentinel.id
body = mock.sentinel.body
mock__validate_connection_body.return_value = (
mock.sentinel.platform,
mock.sentinel.connection_info,
mock.sentinel.mapped_regions,
)
mock_validate_connection.side_effect = exception.InvalidParameterValue(
"mock_err"
)

self.assertRaises(
exc.HTTPNotFound,
testutils.get_wrapped_function(self.endpoint_api._validate_connection),
testutils.get_wrapped_function(self.endpoint_api._validate_endpoint),
mock_req,
id,
body,
)
mock_validate_connection.assert_called_once_with(mock_context, id)
mock_validate_connection.assert_called_once_with(
mock_context,
mock.sentinel.platform,
mock.sentinel.connection_info,
mock.sentinel.mapped_regions,
)
Loading
Loading