diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index f3fe85d..1010cf6 100644 --- a/src/openstack_mcp_server/tools/identity_tools.py +++ b/src/openstack_mcp_server/tools/identity_tools.py @@ -1,7 +1,7 @@ from fastmcp import FastMCP from .base import get_openstack_conn -from .response.identity import Domain, Region +from .response.identity import Domain, Project, Region class IdentityTools: @@ -26,6 +26,9 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.delete_domain) mcp.tool()(self.update_domain) + mcp.tool()(self.get_projects) + mcp.tool()(self.get_project) + def get_regions(self) -> list[Region]: """ Get the list of Identity regions. @@ -220,3 +223,49 @@ def update_domain( description=updated_domain.description, is_enabled=updated_domain.is_enabled, ) + + def get_projects(self) -> list[Project]: + """ + Get the list of Identity projects. + + :return: A list of Project objects representing the projects. + """ + conn = get_openstack_conn() + + project_list = [] + for project in conn.identity.projects(): + project_list.append( + Project( + id=project.id, + name=project.name, + description=project.description, + is_enabled=project.is_enabled, + domain_id=project.domain_id, + parent_id=project.parent_id, + ), + ) + + return project_list + + def get_project(self, name: str) -> Project: + """ + Get a project. + + :param name: The name of the project. + + :return: The Project object. + """ + conn = get_openstack_conn() + + project = conn.identity.find_project( + name_or_id=name, ignore_missing=False + ) + + return Project( + id=project.id, + name=project.name, + description=project.description, + is_enabled=project.is_enabled, + domain_id=project.domain_id, + parent_id=project.parent_id, + ) diff --git a/src/openstack_mcp_server/tools/response/identity.py b/src/openstack_mcp_server/tools/response/identity.py index 527ff4d..a86ee58 100644 --- a/src/openstack_mcp_server/tools/response/identity.py +++ b/src/openstack_mcp_server/tools/response/identity.py @@ -13,3 +13,12 @@ class Domain(BaseModel): name: str description: str | None = None is_enabled: bool | None = None + + +class Project(BaseModel): + id: str + name: str + description: str | None = None + is_enabled: bool | None = None + domain_id: str | None = None + parent_id: str | None = None diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index 47965bc..7245c90 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -6,7 +6,11 @@ from openstack import exceptions from openstack_mcp_server.tools.identity_tools import IdentityTools -from openstack_mcp_server.tools.response.identity import Domain, Region +from openstack_mcp_server.tools.response.identity import ( + Domain, + Project, + Region, +) class TestIdentityTools: @@ -715,3 +719,137 @@ def test_update_domain_with_empty_id( # Verify mock calls mock_conn.identity.update_domain.assert_called_once_with(domain="") + + def test_get_projects_success(self, mock_get_openstack_conn_identity): + """Test getting identity projects successfully.""" + mock_conn = mock_get_openstack_conn_identity + + # Create mock project objects + mock_project1 = Mock() + mock_project1.id = "project1111111111111111111111111" + mock_project1.name = "ProjectOne" + mock_project1.description = "Project One description" + mock_project1.is_enabled = True + mock_project1.domain_id = "domain1111111111111111111111111" + mock_project1.parent_id = "parentproject1111111111111111111" + + mock_project2 = Mock() + mock_project2.id = "project2222222222222222222222222" + mock_project2.name = "ProjectTwo" + mock_project2.description = "Project Two description" + mock_project2.is_enabled = False + mock_project2.domain_id = "domain22222222222222222222222222" + mock_project2.parent_id = "default" + + # Configure mock project.projects() + mock_conn.identity.projects.return_value = [ + mock_project1, + mock_project2, + ] + + # Test get_projects() + identity_tools = self.get_identity_tools() + result = identity_tools.get_projects() + + # Verify results + assert result == [ + Project( + id="project1111111111111111111111111", + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id="parentproject1111111111111111111", + ), + Project( + id="project2222222222222222222222222", + name="ProjectTwo", + description="Project Two description", + is_enabled=False, + domain_id="domain22222222222222222222222222", + parent_id="default", + ), + ] + + # Verify mock calls + mock_conn.identity.projects.assert_called_once() + + def test_get_projects_empty_list(self, mock_get_openstack_conn_identity): + """Test getting identity projects when there are no projects.""" + mock_conn = mock_get_openstack_conn_identity + + # Empty project list + mock_conn.identity.projects.return_value = [] + + # Test get_projects() + identity_tools = self.get_identity_tools() + result = identity_tools.get_projects() + + # Verify results + assert result == [] + + # Verify mock calls + mock_conn.identity.projects.assert_called_once() + + def test_get_project_success(self, mock_get_openstack_conn_identity): + """Test getting a identity project successfully.""" + mock_conn = mock_get_openstack_conn_identity + + # Create mock project object + mock_project = Mock() + mock_project.id = "project1111111111111111111111111" + mock_project.name = "ProjectOne" + mock_project.description = "Project One description" + mock_project.is_enabled = True + mock_project.domain_id = "domain1111111111111111111111111" + mock_project.parent_id = "parentproject1111111111111111111" + + # Configure mock project.find_project() + mock_conn.identity.find_project.return_value = mock_project + + # Test get_project() + identity_tools = self.get_identity_tools() + result = identity_tools.get_project(name="ProjectOne") + + # Verify results + assert result == Project( + id="project1111111111111111111111111", + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id="parentproject1111111111111111111", + ) + + # Verify mock calls + mock_conn.identity.find_project.assert_called_once_with( + name_or_id="ProjectOne", + ignore_missing=False, + ) + + def test_get_project_not_found(self, mock_get_openstack_conn_identity): + """Test getting a identity project that does not exist.""" + mock_conn = mock_get_openstack_conn_identity + + # Configure mock to raise NotFoundException + mock_conn.identity.find_project.side_effect = ( + exceptions.NotFoundException( + "Project 'ProjectOne' not found", + ) + ) + + # Test get_project() + identity_tools = self.get_identity_tools() + + # Verify exception is raised + with pytest.raises( + exceptions.NotFoundException, + match="Project 'ProjectOne' not found", + ): + identity_tools.get_project(name="ProjectOne") + + # Verify mock calls + mock_conn.identity.find_project.assert_called_once_with( + name_or_id="ProjectOne", + ignore_missing=False, + )