mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
chore(openstack): support multi-region in the same provider (#10135)
This commit is contained in:
committed by
GitHub
parent
61076c755f
commit
030d053c84
@@ -62,6 +62,9 @@ def set_mocked_openstack_provider(
|
||||
# Mock connection
|
||||
provider.connection = MagicMock()
|
||||
|
||||
# Mock regional connections (single-region default)
|
||||
provider.regional_connections = {region_name: provider.connection}
|
||||
|
||||
# Mock audit config
|
||||
provider.audit_config = audit_config or {}
|
||||
provider.fixer_config = {}
|
||||
|
||||
@@ -14,11 +14,14 @@ from prowler.config.config import (
|
||||
)
|
||||
from prowler.providers.common.models import Connection
|
||||
from prowler.providers.openstack.exceptions.exceptions import (
|
||||
OpenStackAmbiguousRegionError,
|
||||
OpenStackAuthenticationError,
|
||||
OpenStackCloudNotFoundError,
|
||||
OpenStackConfigFileNotFoundError,
|
||||
OpenStackCredentialsError,
|
||||
OpenStackInvalidConfigError,
|
||||
OpenStackInvalidProviderIdError,
|
||||
OpenStackNoRegionError,
|
||||
)
|
||||
from prowler.providers.openstack.models import OpenStackIdentityInfo, OpenStackSession
|
||||
from prowler.providers.openstack.openstack_provider import OpenstackProvider
|
||||
@@ -1023,3 +1026,623 @@ clouds:
|
||||
assert provider.session.auth_url == "https://openstack.example.com:5000/v3"
|
||||
assert provider.session.username == "test-user"
|
||||
assert provider.session.project_id == "test-project"
|
||||
|
||||
|
||||
class TestOpenstackProviderRegionValidation:
|
||||
"""Test suite for OpenStack Provider region validation (region_name XOR regions)."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_openstack_env(self, monkeypatch):
|
||||
"""Ensure clean OpenStack environment for all tests."""
|
||||
openstack_env_vars = [
|
||||
"OS_AUTH_URL",
|
||||
"OS_USERNAME",
|
||||
"OS_PASSWORD",
|
||||
"OS_PROJECT_ID",
|
||||
"OS_REGION_NAME",
|
||||
"OS_CLOUD",
|
||||
"OS_IDENTITY_API_VERSION",
|
||||
"OS_USER_DOMAIN_NAME",
|
||||
"OS_PROJECT_DOMAIN_NAME",
|
||||
]
|
||||
for env_var in openstack_env_vars:
|
||||
monkeypatch.delenv(env_var, raising=False)
|
||||
|
||||
def test_clouds_yaml_content_with_region_name_only(self):
|
||||
"""Test that clouds.yaml content with only region_name produces a valid session."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
region_name: RegionOne
|
||||
identity_api_version: 3
|
||||
"""
|
||||
session = OpenstackProvider._setup_session_from_clouds_yaml_content(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert session.region_name == "RegionOne"
|
||||
assert session.regions is None
|
||||
|
||||
def test_clouds_yaml_content_with_regions_list_only(self):
|
||||
"""Test that clouds.yaml content with only regions list produces a valid session."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
regions:
|
||||
- RegionOne
|
||||
- RegionTwo
|
||||
identity_api_version: 3
|
||||
"""
|
||||
session = OpenstackProvider._setup_session_from_clouds_yaml_content(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert session.region_name is None
|
||||
assert session.regions == ["RegionOne", "RegionTwo"]
|
||||
|
||||
def test_clouds_yaml_content_with_both_region_name_and_regions(self):
|
||||
"""Test that clouds.yaml content with both region_name and regions raises error."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
region_name: RegionOne
|
||||
regions:
|
||||
- RegionOne
|
||||
- RegionTwo
|
||||
identity_api_version: 3
|
||||
"""
|
||||
with pytest.raises(OpenStackAmbiguousRegionError) as excinfo:
|
||||
OpenstackProvider._setup_session_from_clouds_yaml_content(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert "both 'region_name' and 'regions'" in str(excinfo.value)
|
||||
|
||||
def test_clouds_yaml_content_with_neither_region_name_nor_regions(self):
|
||||
"""Test that clouds.yaml content with neither region_name nor regions raises error."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
identity_api_version: 3
|
||||
"""
|
||||
with pytest.raises(OpenStackNoRegionError) as excinfo:
|
||||
OpenstackProvider._setup_session_from_clouds_yaml_content(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert "neither 'region_name' nor 'regions'" in str(excinfo.value)
|
||||
|
||||
def test_clouds_yaml_file_with_regions_list(self, tmp_path):
|
||||
"""Test loading clouds.yaml file with regions list."""
|
||||
clouds_yaml = tmp_path / "clouds.yaml"
|
||||
clouds_yaml.write_text(
|
||||
"""
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
regions:
|
||||
- RegionOne
|
||||
- RegionTwo
|
||||
identity_api_version: 3
|
||||
"""
|
||||
)
|
||||
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.authorize.return_value = None
|
||||
mock_connection.current_user_id = None
|
||||
mock_connection.current_project_id = "test-project-id"
|
||||
mock_connection.identity.get_project.return_value = None
|
||||
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = mock_connection
|
||||
|
||||
provider = OpenstackProvider(
|
||||
clouds_yaml_file=str(clouds_yaml),
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
assert provider.session.region_name is None
|
||||
assert provider.session.regions == ["RegionOne", "RegionTwo"]
|
||||
|
||||
def test_clouds_yaml_file_with_both_regions_raises_error(self, tmp_path):
|
||||
"""Test that clouds.yaml file with both region_name and regions raises error."""
|
||||
clouds_yaml = tmp_path / "clouds.yaml"
|
||||
clouds_yaml.write_text(
|
||||
"""
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
region_name: RegionOne
|
||||
regions:
|
||||
- RegionOne
|
||||
- RegionTwo
|
||||
identity_api_version: 3
|
||||
"""
|
||||
)
|
||||
|
||||
with pytest.raises(OpenStackAmbiguousRegionError):
|
||||
OpenstackProvider(
|
||||
clouds_yaml_file=str(clouds_yaml),
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
def test_clouds_yaml_file_with_no_region_raises_error(self, tmp_path):
|
||||
"""Test that clouds.yaml file with neither region_name nor regions raises error."""
|
||||
clouds_yaml = tmp_path / "clouds.yaml"
|
||||
clouds_yaml.write_text(
|
||||
"""
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
identity_api_version: 3
|
||||
"""
|
||||
)
|
||||
|
||||
with pytest.raises(OpenStackNoRegionError):
|
||||
OpenstackProvider(
|
||||
clouds_yaml_file=str(clouds_yaml),
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
)
|
||||
|
||||
def test_session_as_sdk_config_with_regions_list(self):
|
||||
"""Test OpenStackSession.as_sdk_config() with regions list uses first region."""
|
||||
session = OpenStackSession(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
identity_api_version="3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project",
|
||||
regions=["RegionOne", "RegionTwo"],
|
||||
user_domain_name="Default",
|
||||
project_domain_name="Default",
|
||||
)
|
||||
|
||||
sdk_config = session.as_sdk_config()
|
||||
|
||||
# SDK does not iterate over regions automatically, so we pass the
|
||||
# first region as region_name for the default connection
|
||||
assert sdk_config["region_name"] == "RegionOne"
|
||||
assert "regions" not in sdk_config
|
||||
|
||||
def test_session_as_sdk_config_with_region_name(self):
|
||||
"""Test OpenStackSession.as_sdk_config() with single region_name."""
|
||||
session = OpenStackSession(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
identity_api_version="3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project",
|
||||
region_name="RegionOne",
|
||||
user_domain_name="Default",
|
||||
project_domain_name="Default",
|
||||
)
|
||||
|
||||
sdk_config = session.as_sdk_config()
|
||||
|
||||
assert sdk_config["region_name"] == "RegionOne"
|
||||
assert "regions" not in sdk_config
|
||||
|
||||
|
||||
class TestOpenstackProviderIdValidation:
|
||||
"""Test suite for OpenStack Provider ID validation."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_openstack_env(self, monkeypatch):
|
||||
"""Ensure clean OpenStack environment for all tests."""
|
||||
openstack_env_vars = [
|
||||
"OS_AUTH_URL",
|
||||
"OS_USERNAME",
|
||||
"OS_PASSWORD",
|
||||
"OS_PROJECT_ID",
|
||||
"OS_REGION_NAME",
|
||||
"OS_CLOUD",
|
||||
"OS_IDENTITY_API_VERSION",
|
||||
"OS_USER_DOMAIN_NAME",
|
||||
"OS_PROJECT_DOMAIN_NAME",
|
||||
]
|
||||
for env_var in openstack_env_vars:
|
||||
monkeypatch.delenv(env_var, raising=False)
|
||||
|
||||
def test_test_connection_provider_id_matches(self):
|
||||
"""Test test_connection succeeds when provider_id matches project_id."""
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.authorize.return_value = None
|
||||
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = mock_connection
|
||||
|
||||
connection_result = OpenstackProvider.test_connection(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project-id",
|
||||
region_name="RegionOne",
|
||||
provider_id="test-project-id",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection_result.is_connected is True
|
||||
assert connection_result.error is None
|
||||
|
||||
def test_test_connection_provider_id_does_not_match(self):
|
||||
"""Test test_connection fails when provider_id doesn't match project_id."""
|
||||
connection_result = OpenstackProvider.test_connection(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="actual-project-id",
|
||||
region_name="RegionOne",
|
||||
provider_id="different-project-id",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection_result.is_connected is False
|
||||
assert isinstance(connection_result.error, OpenStackInvalidProviderIdError)
|
||||
|
||||
def test_test_connection_provider_id_mismatch_raises(self):
|
||||
"""Test test_connection raises when provider_id doesn't match and raise_on_exception=True."""
|
||||
with pytest.raises(OpenStackInvalidProviderIdError) as excinfo:
|
||||
OpenstackProvider.test_connection(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="actual-project-id",
|
||||
region_name="RegionOne",
|
||||
provider_id="different-project-id",
|
||||
raise_on_exception=True,
|
||||
)
|
||||
|
||||
assert "different-project-id" in str(excinfo.value)
|
||||
assert "actual-project-id" in str(excinfo.value)
|
||||
|
||||
def test_test_connection_no_provider_id_skips_validation(self):
|
||||
"""Test test_connection skips provider_id validation when not provided."""
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.authorize.return_value = None
|
||||
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = mock_connection
|
||||
|
||||
connection_result = OpenstackProvider.test_connection(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project-id",
|
||||
region_name="RegionOne",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection_result.is_connected is True
|
||||
|
||||
def test_test_connection_provider_id_with_clouds_yaml_content(self):
|
||||
"""Test test_connection validates provider_id against clouds.yaml content project_id."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: yaml-project-id
|
||||
region_name: RegionOne
|
||||
"""
|
||||
connection_result = OpenstackProvider.test_connection(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
provider_id="wrong-project-id",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection_result.is_connected is False
|
||||
assert isinstance(connection_result.error, OpenStackInvalidProviderIdError)
|
||||
|
||||
def test_test_connection_region_error_surfaced(self):
|
||||
"""Test test_connection surfaces region validation errors."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
test-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
identity_api_version: 3
|
||||
"""
|
||||
connection_result = OpenstackProvider.test_connection(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="test-cloud",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert connection_result.is_connected is False
|
||||
assert isinstance(connection_result.error, OpenStackNoRegionError)
|
||||
|
||||
|
||||
class TestOpenstackProviderRegionalConnections:
|
||||
"""Test suite for OpenStack Provider regional_connections."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_openstack_env(self, monkeypatch):
|
||||
"""Ensure clean OpenStack environment for all tests."""
|
||||
openstack_env_vars = [
|
||||
"OS_AUTH_URL",
|
||||
"OS_USERNAME",
|
||||
"OS_PASSWORD",
|
||||
"OS_PROJECT_ID",
|
||||
"OS_REGION_NAME",
|
||||
"OS_CLOUD",
|
||||
"OS_IDENTITY_API_VERSION",
|
||||
"OS_USER_DOMAIN_NAME",
|
||||
"OS_PROJECT_DOMAIN_NAME",
|
||||
]
|
||||
for env_var in openstack_env_vars:
|
||||
monkeypatch.delenv(env_var, raising=False)
|
||||
|
||||
def test_single_region_regional_connections(self):
|
||||
"""Test regional_connections has one entry for single-region provider."""
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.authorize.return_value = None
|
||||
mock_connection.current_user_id = None
|
||||
mock_connection.current_project_id = "test-project"
|
||||
mock_connection.identity.get_project.return_value = None
|
||||
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = mock_connection
|
||||
|
||||
provider = OpenstackProvider(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project",
|
||||
region_name="RegionOne",
|
||||
)
|
||||
|
||||
assert len(provider.regional_connections) == 1
|
||||
assert "RegionOne" in provider.regional_connections
|
||||
assert provider.regional_connections["RegionOne"] is provider.connection
|
||||
mock_connect.assert_called_once()
|
||||
|
||||
def test_multi_region_regional_connections(self):
|
||||
"""Test regional_connections has entries for each region in multi-region setup."""
|
||||
mock_conn_region1 = MagicMock()
|
||||
mock_conn_region1.authorize.return_value = None
|
||||
mock_conn_region1.current_user_id = None
|
||||
mock_conn_region1.current_project_id = "test-project-id"
|
||||
mock_conn_region1.identity.get_project.return_value = None
|
||||
|
||||
mock_conn_region2 = MagicMock()
|
||||
mock_conn_region2.authorize.return_value = None
|
||||
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
multi-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
regions:
|
||||
- UK1
|
||||
- DE1
|
||||
identity_api_version: 3
|
||||
"""
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.side_effect = [mock_conn_region1, mock_conn_region2]
|
||||
|
||||
provider = OpenstackProvider(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="multi-cloud",
|
||||
)
|
||||
|
||||
assert len(provider.regional_connections) == 2
|
||||
assert "UK1" in provider.regional_connections
|
||||
assert "DE1" in provider.regional_connections
|
||||
assert provider.regional_connections["UK1"] is mock_conn_region1
|
||||
assert provider.regional_connections["DE1"] is mock_conn_region2
|
||||
# Default connection should be the first region
|
||||
assert provider.connection is mock_conn_region1
|
||||
assert mock_connect.call_count == 2
|
||||
|
||||
def test_multi_region_test_connection_tests_all_regions(self):
|
||||
"""Test test_connection tests connectivity to every region."""
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.authorize.return_value = None
|
||||
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
multi-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
regions:
|
||||
- UK1
|
||||
- DE1
|
||||
identity_api_version: 3
|
||||
"""
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = mock_connection
|
||||
|
||||
result = OpenstackProvider.test_connection(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="multi-cloud",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert result.is_connected is True
|
||||
# Should have called connect once per region
|
||||
assert mock_connect.call_count == 2
|
||||
|
||||
def test_multi_region_test_connection_fails_if_one_region_fails(self):
|
||||
"""Test test_connection fails if any region fails."""
|
||||
mock_conn_ok = MagicMock()
|
||||
mock_conn_ok.authorize.return_value = None
|
||||
|
||||
mock_conn_fail = MagicMock()
|
||||
mock_conn_fail.authorize.side_effect = openstack_exceptions.SDKException(
|
||||
"Connection failed in DE1"
|
||||
)
|
||||
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
multi-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
regions:
|
||||
- UK1
|
||||
- DE1
|
||||
identity_api_version: 3
|
||||
"""
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.side_effect = [mock_conn_ok, mock_conn_fail]
|
||||
|
||||
result = OpenstackProvider.test_connection(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="multi-cloud",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert result.is_connected is False
|
||||
|
||||
def test_session_as_sdk_config_region_override(self):
|
||||
"""Test as_sdk_config with region_override overrides region_name."""
|
||||
session = OpenStackSession(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
identity_api_version="3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project",
|
||||
region_name="RegionOne",
|
||||
user_domain_name="Default",
|
||||
project_domain_name="Default",
|
||||
)
|
||||
|
||||
sdk_config = session.as_sdk_config(region_override="RegionTwo")
|
||||
assert sdk_config["region_name"] == "RegionTwo"
|
||||
|
||||
def test_session_as_sdk_config_region_override_with_regions_list(self):
|
||||
"""Test as_sdk_config with region_override overrides regions list."""
|
||||
session = OpenStackSession(
|
||||
auth_url="https://openstack.example.com:5000/v3",
|
||||
identity_api_version="3",
|
||||
username="test-user",
|
||||
password="test-password",
|
||||
project_id="test-project",
|
||||
regions=["UK1", "DE1"],
|
||||
user_domain_name="Default",
|
||||
project_domain_name="Default",
|
||||
)
|
||||
|
||||
sdk_config = session.as_sdk_config(region_override="DE1")
|
||||
assert sdk_config["region_name"] == "DE1"
|
||||
|
||||
def test_multi_region_test_connection_provider_id_matches(self):
|
||||
"""Test test_connection validates provider_id in multi-region setup."""
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.authorize.return_value = None
|
||||
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
multi-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: test-project-id
|
||||
regions:
|
||||
- UK1
|
||||
- DE1
|
||||
identity_api_version: 3
|
||||
"""
|
||||
with patch(
|
||||
"prowler.providers.openstack.openstack_provider.connect"
|
||||
) as mock_connect:
|
||||
mock_connect.return_value = mock_connection
|
||||
|
||||
result = OpenstackProvider.test_connection(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="multi-cloud",
|
||||
provider_id="test-project-id",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert result.is_connected is True
|
||||
|
||||
def test_multi_region_test_connection_provider_id_mismatch(self):
|
||||
"""Test test_connection fails when provider_id doesn't match in multi-region."""
|
||||
clouds_yaml_content = """
|
||||
clouds:
|
||||
multi-cloud:
|
||||
auth:
|
||||
auth_url: https://openstack.example.com:5000/v3
|
||||
username: test-user
|
||||
password: test-password
|
||||
project_id: actual-project-id
|
||||
regions:
|
||||
- UK1
|
||||
- DE1
|
||||
identity_api_version: 3
|
||||
"""
|
||||
result = OpenstackProvider.test_connection(
|
||||
clouds_yaml_content=clouds_yaml_content,
|
||||
clouds_yaml_cloud="multi-cloud",
|
||||
provider_id="wrong-project-id",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert isinstance(result.error, OpenStackInvalidProviderIdError)
|
||||
|
||||
@@ -28,9 +28,10 @@ class TestComputeService:
|
||||
assert compute.service_name == "Compute"
|
||||
assert compute.provider == provider
|
||||
assert compute.connection == provider.connection
|
||||
assert compute.regional_connections == provider.regional_connections
|
||||
assert compute.audited_regions == [OPENSTACK_REGION]
|
||||
assert compute.region == OPENSTACK_REGION
|
||||
assert compute.project_id == OPENSTACK_PROJECT_ID
|
||||
assert compute.client == provider.connection.compute
|
||||
assert compute.instances == []
|
||||
mock_list.assert_called_once()
|
||||
|
||||
@@ -303,6 +304,8 @@ class TestComputeService:
|
||||
assert hasattr(compute, "service_name")
|
||||
assert hasattr(compute, "provider")
|
||||
assert hasattr(compute, "connection")
|
||||
assert hasattr(compute, "regional_connections")
|
||||
assert hasattr(compute, "audited_regions")
|
||||
assert hasattr(compute, "session")
|
||||
assert hasattr(compute, "region")
|
||||
assert hasattr(compute, "project_id")
|
||||
@@ -343,3 +346,153 @@ class TestComputeService:
|
||||
assert len(compute.instances) == 1
|
||||
assert compute.instances[0].id == "instance-1"
|
||||
assert compute.instances[0].networks == {} # Should default to empty dict
|
||||
|
||||
def test_compute_list_instances_multi_region(self):
|
||||
"""Test listing instances across multiple regions."""
|
||||
provider = set_mocked_openstack_provider()
|
||||
|
||||
# Create two mock connections for two regions
|
||||
mock_conn_uk1 = MagicMock()
|
||||
mock_conn_de1 = MagicMock()
|
||||
|
||||
# Set up regional connections
|
||||
provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1}
|
||||
|
||||
mock_server_uk = MagicMock()
|
||||
mock_server_uk.id = "instance-uk"
|
||||
mock_server_uk.name = "Instance UK"
|
||||
mock_server_uk.status = "ACTIVE"
|
||||
mock_server_uk.flavor = {"id": "flavor-1"}
|
||||
mock_server_uk.security_groups = [{"name": "default"}]
|
||||
mock_server_uk.is_locked = False
|
||||
mock_server_uk.locked_reason = ""
|
||||
mock_server_uk.key_name = ""
|
||||
mock_server_uk.user_id = ""
|
||||
mock_server_uk.access_ipv4 = ""
|
||||
mock_server_uk.access_ipv6 = ""
|
||||
mock_server_uk.public_v4 = ""
|
||||
mock_server_uk.public_v6 = ""
|
||||
mock_server_uk.private_v4 = "10.0.0.1"
|
||||
mock_server_uk.private_v6 = ""
|
||||
mock_server_uk.addresses = {"private": [{"version": 4, "addr": "10.0.0.1"}]}
|
||||
mock_server_uk.has_config_drive = False
|
||||
mock_server_uk.metadata = {}
|
||||
mock_server_uk.user_data = ""
|
||||
mock_server_uk.trusted_image_certificates = []
|
||||
|
||||
mock_server_de = MagicMock()
|
||||
mock_server_de.id = "instance-de"
|
||||
mock_server_de.name = "Instance DE"
|
||||
mock_server_de.status = "ACTIVE"
|
||||
mock_server_de.flavor = {"id": "flavor-2"}
|
||||
mock_server_de.security_groups = [{"name": "default"}]
|
||||
mock_server_de.is_locked = False
|
||||
mock_server_de.locked_reason = ""
|
||||
mock_server_de.key_name = ""
|
||||
mock_server_de.user_id = ""
|
||||
mock_server_de.access_ipv4 = ""
|
||||
mock_server_de.access_ipv6 = ""
|
||||
mock_server_de.public_v4 = ""
|
||||
mock_server_de.public_v6 = ""
|
||||
mock_server_de.private_v4 = "10.0.0.2"
|
||||
mock_server_de.private_v6 = ""
|
||||
mock_server_de.addresses = {"private": [{"version": 4, "addr": "10.0.0.2"}]}
|
||||
mock_server_de.has_config_drive = False
|
||||
mock_server_de.metadata = {}
|
||||
mock_server_de.user_data = ""
|
||||
mock_server_de.trusted_image_certificates = []
|
||||
|
||||
mock_conn_uk1.compute.servers.return_value = [mock_server_uk]
|
||||
mock_conn_de1.compute.servers.return_value = [mock_server_de]
|
||||
|
||||
compute = Compute(provider)
|
||||
|
||||
assert len(compute.instances) == 2
|
||||
# Verify instances have correct region tags
|
||||
uk_instance = next(i for i in compute.instances if i.id == "instance-uk")
|
||||
de_instance = next(i for i in compute.instances if i.id == "instance-de")
|
||||
assert uk_instance.region == "UK1"
|
||||
assert de_instance.region == "DE1"
|
||||
|
||||
def test_compute_list_instances_multi_region_partial_failure(self):
|
||||
"""Test that a failing region doesn't prevent other regions from being listed."""
|
||||
provider = set_mocked_openstack_provider()
|
||||
|
||||
mock_conn_ok = MagicMock()
|
||||
mock_conn_fail = MagicMock()
|
||||
|
||||
provider.regional_connections = {"UK1": mock_conn_ok, "DE1": mock_conn_fail}
|
||||
|
||||
mock_server = MagicMock()
|
||||
mock_server.id = "instance-uk"
|
||||
mock_server.name = "Instance UK"
|
||||
mock_server.status = "ACTIVE"
|
||||
mock_server.flavor = {"id": "flavor-1"}
|
||||
mock_server.security_groups = [{"name": "default"}]
|
||||
mock_server.is_locked = False
|
||||
mock_server.locked_reason = ""
|
||||
mock_server.key_name = ""
|
||||
mock_server.user_id = ""
|
||||
mock_server.access_ipv4 = ""
|
||||
mock_server.access_ipv6 = ""
|
||||
mock_server.public_v4 = ""
|
||||
mock_server.public_v6 = ""
|
||||
mock_server.private_v4 = "10.0.0.1"
|
||||
mock_server.private_v6 = ""
|
||||
mock_server.addresses = {}
|
||||
mock_server.has_config_drive = False
|
||||
mock_server.metadata = {}
|
||||
mock_server.user_data = ""
|
||||
mock_server.trusted_image_certificates = []
|
||||
|
||||
mock_conn_ok.compute.servers.return_value = [mock_server]
|
||||
mock_conn_fail.compute.servers.side_effect = openstack_exceptions.SDKException(
|
||||
"API error in DE1"
|
||||
)
|
||||
|
||||
compute = Compute(provider)
|
||||
|
||||
# Should have the instance from UK1, DE1 failure is logged but doesn't crash
|
||||
assert len(compute.instances) == 1
|
||||
assert compute.instances[0].id == "instance-uk"
|
||||
assert compute.instances[0].region == "UK1"
|
||||
|
||||
def test_compute_list_instances_multi_region_one_empty(self):
|
||||
"""Test multi-region where one region has instances and the other is empty."""
|
||||
provider = set_mocked_openstack_provider()
|
||||
|
||||
mock_conn_uk1 = MagicMock()
|
||||
mock_conn_de1 = MagicMock()
|
||||
|
||||
provider.regional_connections = {"UK1": mock_conn_uk1, "DE1": mock_conn_de1}
|
||||
|
||||
mock_server = MagicMock()
|
||||
mock_server.id = "instance-uk"
|
||||
mock_server.name = "Instance UK"
|
||||
mock_server.status = "ACTIVE"
|
||||
mock_server.flavor = {"id": "flavor-1"}
|
||||
mock_server.security_groups = [{"name": "default"}]
|
||||
mock_server.is_locked = False
|
||||
mock_server.locked_reason = ""
|
||||
mock_server.key_name = ""
|
||||
mock_server.user_id = ""
|
||||
mock_server.access_ipv4 = ""
|
||||
mock_server.access_ipv6 = ""
|
||||
mock_server.public_v4 = ""
|
||||
mock_server.public_v6 = ""
|
||||
mock_server.private_v4 = "10.0.0.1"
|
||||
mock_server.private_v6 = ""
|
||||
mock_server.addresses = {}
|
||||
mock_server.has_config_drive = False
|
||||
mock_server.metadata = {}
|
||||
mock_server.user_data = ""
|
||||
mock_server.trusted_image_certificates = []
|
||||
|
||||
mock_conn_uk1.compute.servers.return_value = [mock_server]
|
||||
mock_conn_de1.compute.servers.return_value = [] # Empty region
|
||||
|
||||
compute = Compute(provider)
|
||||
|
||||
assert len(compute.instances) == 1
|
||||
assert compute.instances[0].id == "instance-uk"
|
||||
assert compute.instances[0].region == "UK1"
|
||||
|
||||
Reference in New Issue
Block a user