From 7340ef702784e342735d9fc919e36323dec0b34d Mon Sep 17 00:00:00 2001 From: S0okJu Date: Mon, 25 Aug 2025 17:17:49 +0900 Subject: [PATCH 1/8] feat(identity): Add get-project, get-projects tool(#58) - Add get-project, get-projects tool - Test code are updated and passing --- src/openstack_mcp_server/_version.py | 34 +++++ .../tools/identity_tools.py | 50 ++++++- .../tools/response/identity.py | 8 + tests/tools/test_identity_tools.py | 141 +++++++++++++++++- 4 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/openstack_mcp_server/_version.py diff --git a/src/openstack_mcp_server/_version.py b/src/openstack_mcp_server/_version.py new file mode 100644 index 0000000..c556c17 --- /dev/null +++ b/src/openstack_mcp_server/_version.py @@ -0,0 +1,34 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] +else: + VERSION_TUPLE = object + COMMIT_ID = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID + +__version__ = version = '0.1.dev38+g481dffd50.d20250825' +__version_tuple__ = version_tuple = (0, 1, 'dev38', 'g481dffd50.d20250825') + +__commit_id__ = commit_id = 'g481dffd50' diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index f3fe85d..7f1b6e4 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, Region, Project class IdentityTools: @@ -25,6 +25,9 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.create_domain) 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]: """ @@ -220,3 +223,48 @@ 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, + ) + \ No newline at end of file diff --git a/src/openstack_mcp_server/tools/response/identity.py b/src/openstack_mcp_server/tools/response/identity.py index 527ff4d..98a9fe9 100644 --- a/src/openstack_mcp_server/tools/response/identity.py +++ b/src/openstack_mcp_server/tools/response/identity.py @@ -13,3 +13,11 @@ 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 \ No newline at end of file diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index 47965bc..bd7f216 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -6,7 +6,7 @@ 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, Region, Project class TestIdentityTools: @@ -715,3 +715,142 @@ 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, + ) + + + + + + \ No newline at end of file From 1228a28e1093a48a2e668fb8036ef14f3dc60739 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Mon, 25 Aug 2025 17:35:49 +0900 Subject: [PATCH 2/8] chore(identity): ruff format(#58) --- .../tools/identity_tools.py | 11 +++-- .../tools/response/identity.py | 3 +- tests/tools/test_identity_tools.py | 47 +++++++++---------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index 7f1b6e4..60b206a 100644 --- a/src/openstack_mcp_server/tools/identity_tools.py +++ b/src/openstack_mcp_server/tools/identity_tools.py @@ -25,7 +25,7 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.create_domain) mcp.tool()(self.delete_domain) mcp.tool()(self.update_domain) - + mcp.tool()(self.get_projects) mcp.tool()(self.get_project) @@ -231,7 +231,7 @@ def get_projects(self) -> list[Project]: :return: A list of Project objects representing the projects. """ conn = get_openstack_conn() - + project_list = [] for project in conn.identity.projects(): project_list.append( @@ -246,7 +246,7 @@ def get_projects(self) -> list[Project]: ) return project_list - + def get_project(self, name: str) -> Project: """ Get a project. @@ -257,7 +257,9 @@ def get_project(self, name: str) -> Project: """ conn = get_openstack_conn() - project = conn.identity.find_project(name_or_id=name, ignore_missing=False) + project = conn.identity.find_project( + name_or_id=name, ignore_missing=False + ) return Project( id=project.id, @@ -267,4 +269,3 @@ def get_project(self, name: str) -> Project: domain_id=project.domain_id, parent_id=project.parent_id, ) - \ No newline at end of file diff --git a/src/openstack_mcp_server/tools/response/identity.py b/src/openstack_mcp_server/tools/response/identity.py index 98a9fe9..a86ee58 100644 --- a/src/openstack_mcp_server/tools/response/identity.py +++ b/src/openstack_mcp_server/tools/response/identity.py @@ -14,10 +14,11 @@ class Domain(BaseModel): 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 \ No newline at end of file + parent_id: str | None = None diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index bd7f216..576f033 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, Project +from openstack_mcp_server.tools.response.identity import ( + Domain, + Region, + Project, +) class TestIdentityTools: @@ -736,14 +740,17 @@ def test_get_projects_success(self, mock_get_openstack_conn_identity): 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] - + 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( @@ -763,29 +770,27 @@ def test_get_projects_success(self, mock_get_openstack_conn_identity): 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 @@ -805,7 +810,7 @@ def test_get_project_success(self, mock_get_openstack_conn_identity): # Test get_project() identity_tools = self.get_identity_tools() result = identity_tools.get_project(name="ProjectOne") - + # Verify results assert result == Project( id="project1111111111111111111111111", @@ -815,13 +820,13 @@ def test_get_project_success(self, mock_get_openstack_conn_identity): 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 @@ -835,22 +840,16 @@ def test_get_project_not_found(self, mock_get_openstack_conn_identity): # 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, ) - - - - - - \ No newline at end of file From 04f9b02d8024e40bbf02d95a65723e171f9b4b24 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Mon, 25 Aug 2025 17:40:28 +0900 Subject: [PATCH 3/8] chore(identity): ruff format(#58) --- src/openstack_mcp_server/tools/identity_tools.py | 2 +- tests/tools/test_identity_tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index 60b206a..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, Project +from .response.identity import Domain, Project, Region class IdentityTools: diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index 576f033..7245c90 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -8,8 +8,8 @@ from openstack_mcp_server.tools.identity_tools import IdentityTools from openstack_mcp_server.tools.response.identity import ( Domain, - Region, Project, + Region, ) From d997b58f2967910da2d1c1eec89a318463d92237 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Wed, 27 Aug 2025 11:25:28 +0900 Subject: [PATCH 4/8] fix(identity): Remove _version.py(#58) --- src/openstack_mcp_server/_version.py | 34 ---------------------------- 1 file changed, 34 deletions(-) delete mode 100644 src/openstack_mcp_server/_version.py diff --git a/src/openstack_mcp_server/_version.py b/src/openstack_mcp_server/_version.py deleted file mode 100644 index c556c17..0000000 --- a/src/openstack_mcp_server/_version.py +++ /dev/null @@ -1,34 +0,0 @@ -# file generated by setuptools-scm -# don't change, don't track in version control - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] - COMMIT_ID = Union[str, None] -else: - VERSION_TUPLE = object - COMMIT_ID = object - -version: str -__version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE -commit_id: COMMIT_ID -__commit_id__: COMMIT_ID - -__version__ = version = '0.1.dev38+g481dffd50.d20250825' -__version_tuple__ = version_tuple = (0, 1, 'dev38', 'g481dffd50.d20250825') - -__commit_id__ = commit_id = 'g481dffd50' From 823aa7d4efc3c5e8b22dc25adc70c0759ba77587 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Mon, 1 Sep 2025 17:09:26 +0900 Subject: [PATCH 5/8] feat(identity): Add create project tool(#58) - Add create-project tool - Tests are updated ans passing --- .../tools/identity_tools.py | 39 ++++ tests/tools/test_identity_tools.py | 189 ++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index 1010cf6..b639f8a 100644 --- a/src/openstack_mcp_server/tools/identity_tools.py +++ b/src/openstack_mcp_server/tools/identity_tools.py @@ -28,6 +28,7 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.get_projects) mcp.tool()(self.get_project) + mcp.tool()(self.create_project) def get_regions(self) -> list[Region]: """ @@ -269,3 +270,41 @@ def get_project(self, name: str) -> Project: domain_id=project.domain_id, parent_id=project.parent_id, ) + + def create_project( + self, + name: str, + description: str | None = None, + is_enabled: bool = True, + domain_id: str | None = None, + parent_id: str | None = None, + ) -> Project: + """ + Create a new project. + + :param name: The name of the project. + :param description: The description of the project. + :param is_enabled: Whether the project is enabled. + :param domain_id: The ID of the domain. + :param parent_id: The ID of the parent project. + + :return: The created Project object. + """ + conn = get_openstack_conn() + + project = conn.identity.create_project( + name=name, + description=description, + is_enabled=is_enabled, + domain_id=domain_id, + parent_id=parent_id, + ) + + 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/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index 7245c90..dd04547 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -853,3 +853,192 @@ def test_get_project_not_found(self, mock_get_openstack_conn_identity): name_or_id="ProjectOne", ignore_missing=False, ) + + def test_create_project_success_with_all_fields( + self, mock_get_openstack_conn_identity + ): + """Test creating 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.create_project() + mock_conn.identity.create_project.return_value = mock_project + + # Test create_project() + identity_tools = self.get_identity_tools() + result = identity_tools.create_project( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id="parentproject1111111111111111111", + ) + + # 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.create_project.assert_called_once_with( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id="parentproject1111111111111111111", + ) + + def test_create_project_parent_disabled_failed( + self, mock_get_openstack_conn_identity + ): + """Test creating a identity project with a parent project that is disabled.""" + mock_conn = mock_get_openstack_conn_identity + + # Configure mock to raise BadRequestException + mock_conn.identity.create_project.side_effect = exceptions.BadRequestException( + "cannot create a project in a branch containing a disabled project", + ) + + # Test create_project() + identity_tools = self.get_identity_tools() + + # Verify exception is raised + with pytest.raises( + exceptions.BadRequestException, + match="cannot create a project in a branch containing a disabled project", + ): + identity_tools.create_project( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id=None, + parent_id="parentproject1111111111111111111", + ) + + # Verify mock calls + mock_conn.identity.create_project.assert_called_once_with( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id=None, + parent_id="parentproject1111111111111111111", + ) + + def test_create_project_without_all_fields( + self, mock_get_openstack_conn_identity + ): + """Test creating a identity project without all fields.""" + mock_conn = mock_get_openstack_conn_identity + + mock_conn.identity.create_project.side_effect = ( + exceptions.BadRequestException( + "Field required", + ) + ) + + # Test create_project() + identity_tools = self.get_identity_tools() + + with pytest.raises( + exceptions.BadRequestException, + match="Field required", + ): + identity_tools.create_project( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id=None, + parent_id=None, + ) + + # Verify mock calls + mock_conn.identity.create_project.assert_called_once_with( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id=None, + parent_id=None, + ) + + def test_create_project_domain_not_found( + self, mock_get_openstack_conn_identity + ): + """Test creating a identity project with a domain that does not exist.""" + mock_conn = mock_get_openstack_conn_identity + + # Configure mock to raise BadRequestException + mock_conn.identity.create_project.side_effect = exceptions.BadRequestException( + "Domain 'domain1111111111111111111111111' not found. Please check the domain ID.", + ) + + # Test create_project() + identity_tools = self.get_identity_tools() + + with pytest.raises( + exceptions.BadRequestException, + match="Domain 'domain1111111111111111111111111' not found. Please check the domain ID.", + ): + identity_tools.create_project( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id=None, + ) + + # Verify mock calls + mock_conn.identity.create_project.assert_called_once_with( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id=None, + ) + + def test_create_project_parent_not_found( + self, mock_get_openstack_conn_identity + ): + """Test creating a identity project with a parent project that does not exist.""" + mock_conn = mock_get_openstack_conn_identity + + # Configure mock to raise BadRequestException + mock_conn.identity.create_project.side_effect = exceptions.BadRequestException( + "Parent project 'parentproject1111111111111111111' not found. Please check the parent project ID.", + ) + + # Test create_project() + identity_tools = self.get_identity_tools() + + with pytest.raises( + exceptions.BadRequestException, + match="Parent project 'parentproject1111111111111111111' not found. Please check the parent project ID.", + ): + identity_tools.create_project( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id=None, + parent_id="parentproject1111111111111111111", + ) + + # Verify mock calls + mock_conn.identity.create_project.assert_called_once_with( + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id=None, + parent_id="parentproject1111111111111111111", + ) From 9aec26231973b2177d10676b91bac631247e4f39 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Wed, 3 Sep 2025 00:44:05 +0900 Subject: [PATCH 6/8] fix(identity): Fix unnecessary test codes(#?58) --- tests/tools/test_identity_tools.py | 106 ----------------------------- 1 file changed, 106 deletions(-) diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index dd04547..ccb99e7 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -901,42 +901,6 @@ def test_create_project_success_with_all_fields( parent_id="parentproject1111111111111111111", ) - def test_create_project_parent_disabled_failed( - self, mock_get_openstack_conn_identity - ): - """Test creating a identity project with a parent project that is disabled.""" - mock_conn = mock_get_openstack_conn_identity - - # Configure mock to raise BadRequestException - mock_conn.identity.create_project.side_effect = exceptions.BadRequestException( - "cannot create a project in a branch containing a disabled project", - ) - - # Test create_project() - identity_tools = self.get_identity_tools() - - # Verify exception is raised - with pytest.raises( - exceptions.BadRequestException, - match="cannot create a project in a branch containing a disabled project", - ): - identity_tools.create_project( - name="ProjectOne", - description="Project One description", - is_enabled=True, - domain_id=None, - parent_id="parentproject1111111111111111111", - ) - - # Verify mock calls - mock_conn.identity.create_project.assert_called_once_with( - name="ProjectOne", - description="Project One description", - is_enabled=True, - domain_id=None, - parent_id="parentproject1111111111111111111", - ) - def test_create_project_without_all_fields( self, mock_get_openstack_conn_identity ): @@ -972,73 +936,3 @@ def test_create_project_without_all_fields( domain_id=None, parent_id=None, ) - - def test_create_project_domain_not_found( - self, mock_get_openstack_conn_identity - ): - """Test creating a identity project with a domain that does not exist.""" - mock_conn = mock_get_openstack_conn_identity - - # Configure mock to raise BadRequestException - mock_conn.identity.create_project.side_effect = exceptions.BadRequestException( - "Domain 'domain1111111111111111111111111' not found. Please check the domain ID.", - ) - - # Test create_project() - identity_tools = self.get_identity_tools() - - with pytest.raises( - exceptions.BadRequestException, - match="Domain 'domain1111111111111111111111111' not found. Please check the domain ID.", - ): - identity_tools.create_project( - name="ProjectOne", - description="Project One description", - is_enabled=True, - domain_id="domain1111111111111111111111111", - parent_id=None, - ) - - # Verify mock calls - mock_conn.identity.create_project.assert_called_once_with( - name="ProjectOne", - description="Project One description", - is_enabled=True, - domain_id="domain1111111111111111111111111", - parent_id=None, - ) - - def test_create_project_parent_not_found( - self, mock_get_openstack_conn_identity - ): - """Test creating a identity project with a parent project that does not exist.""" - mock_conn = mock_get_openstack_conn_identity - - # Configure mock to raise BadRequestException - mock_conn.identity.create_project.side_effect = exceptions.BadRequestException( - "Parent project 'parentproject1111111111111111111' not found. Please check the parent project ID.", - ) - - # Test create_project() - identity_tools = self.get_identity_tools() - - with pytest.raises( - exceptions.BadRequestException, - match="Parent project 'parentproject1111111111111111111' not found. Please check the parent project ID.", - ): - identity_tools.create_project( - name="ProjectOne", - description="Project One description", - is_enabled=True, - domain_id=None, - parent_id="parentproject1111111111111111111", - ) - - # Verify mock calls - mock_conn.identity.create_project.assert_called_once_with( - name="ProjectOne", - description="Project One description", - is_enabled=True, - domain_id=None, - parent_id="parentproject1111111111111111111", - ) From 8fd051d9ab2c2197fd591b137ea9a4e0a0a906b1 Mon Sep 17 00:00:00 2001 From: S0okJu Date: Thu, 4 Sep 2025 17:43:46 +0900 Subject: [PATCH 7/8] feat(identity): Add delete project tool(#58) --- .../tools/identity_tools.py | 11 +++++ tests/tools/test_identity_tools.py | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index b639f8a..fe08e25 100644 --- a/src/openstack_mcp_server/tools/identity_tools.py +++ b/src/openstack_mcp_server/tools/identity_tools.py @@ -29,6 +29,7 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.get_projects) mcp.tool()(self.get_project) mcp.tool()(self.create_project) + mcp.tool()(self.delete_project) def get_regions(self) -> list[Region]: """ @@ -308,3 +309,13 @@ def create_project( domain_id=project.domain_id, parent_id=project.parent_id, ) + + def delete_project(self, id: str) -> None: + """ + Delete a project. + + :param name: The name of the project. + """ + conn = get_openstack_conn() + conn.identity.delete_project(project=id, ignore_missing=False) + return None diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index ccb99e7..94fa649 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -936,3 +936,50 @@ def test_create_project_without_all_fields( domain_id=None, parent_id=None, ) + + def test_delete_project_success(self, mock_get_openstack_conn_identity): + """Test deleting a identity project successfully.""" + mock_conn = mock_get_openstack_conn_identity + + # Test delete_project() + identity_tools = self.get_identity_tools() + result = identity_tools.delete_project( + id="project1111111111111111111111111" + ) + + # Verify results + assert result is None + + # Verify mock calls + mock_conn.identity.delete_project.assert_called_once_with( + project="project1111111111111111111111111", + ignore_missing=False, + ) + + def test_delete_project_not_found(self, mock_get_openstack_conn_identity): + """Test deleting a identity project that does not exist.""" + mock_conn = mock_get_openstack_conn_identity + + # Configure mock to raise NotFoundException + mock_conn.identity.delete_project.side_effect = ( + exceptions.NotFoundException( + "Project 'project1111111111111111111111111' not found", + ) + ) + + # Test delete_project() + identity_tools = self.get_identity_tools() + + with pytest.raises( + exceptions.NotFoundException, + match="Project 'project1111111111111111111111111' not found", + ): + identity_tools.delete_project( + id="project1111111111111111111111111" + ) + + # Verify mock calls + mock_conn.identity.delete_project.assert_called_once_with( + project="project1111111111111111111111111", + ignore_missing=False, + ) From e100f4d5180cb589dcb857ce909cc46db616088e Mon Sep 17 00:00:00 2001 From: S0okJu Date: Mon, 15 Sep 2025 20:27:23 +0900 Subject: [PATCH 8/8] feat(identity): Add update project tool(#58) --- .../tools/identity_tools.py | 47 +++++++++++++ tests/tools/test_identity_tools.py | 70 +++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py index fe08e25..5432a29 100644 --- a/src/openstack_mcp_server/tools/identity_tools.py +++ b/src/openstack_mcp_server/tools/identity_tools.py @@ -30,6 +30,7 @@ def register_tools(self, mcp: FastMCP): mcp.tool()(self.get_project) mcp.tool()(self.create_project) mcp.tool()(self.delete_project) + mcp.tool()(self.update_project) def get_regions(self) -> list[Region]: """ @@ -319,3 +320,49 @@ def delete_project(self, id: str) -> None: conn = get_openstack_conn() conn.identity.delete_project(project=id, ignore_missing=False) return None + + def update_project( + self, + id: str, + name: str | None = None, + description: str | None = None, + is_enabled: bool | None = None, + domain_id: str | None = None, + parent_id: str | None = None, + ) -> Project: + """ + Update a project. + + :param id: The ID of the project. + :param name: The name of the project. + :param description: The description of the project. + :param is_enabled: Whether the project is enabled. + :param domain_id: The ID of the domain. + :param parent_id: The ID of the parent project. + + :return: The updated Project object. + """ + conn = get_openstack_conn() + + args = {} + if name is not None: + args["name"] = name + if description is not None: + args["description"] = description + if is_enabled is not None: + args["is_enabled"] = is_enabled + if domain_id is not None: + args["domain_id"] = domain_id + if parent_id is not None: + args["parent_id"] = parent_id + + updated_project = conn.identity.update_project(project=id, **args) + + return Project( + id=updated_project.id, + name=updated_project.name, + description=updated_project.description, + is_enabled=updated_project.is_enabled, + domain_id=updated_project.domain_id, + parent_id=updated_project.parent_id, + ) diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py index 94fa649..b78e7ce 100644 --- a/tests/tools/test_identity_tools.py +++ b/tests/tools/test_identity_tools.py @@ -983,3 +983,73 @@ def test_delete_project_not_found(self, mock_get_openstack_conn_identity): project="project1111111111111111111111111", ignore_missing=False, ) + + def test_update_project_success(self, mock_get_openstack_conn_identity): + """Test updating 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.update_project() + mock_conn.identity.update_project.return_value = mock_project + + # Test update_project() + identity_tools = self.get_identity_tools() + result = identity_tools.update_project( + id="project1111111111111111111111111", + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id="parentproject1111111111111111111", + ) + + # 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.update_project.assert_called_once_with( + project="project1111111111111111111111111", + name="ProjectOne", + description="Project One description", + is_enabled=True, + domain_id="domain1111111111111111111111111", + parent_id="parentproject1111111111111111111", + ) + + def test_update_project_empty_id(self, mock_get_openstack_conn_identity): + """Test updating a identity project with an empty ID.""" + mock_conn = mock_get_openstack_conn_identity + + # Configure mock to raise BadRequestException + mock_conn.identity.update_project.side_effect = ( + exceptions.BadRequestException( + "Field required", + ) + ) + + # Test update_project() + identity_tools = self.get_identity_tools() + + with pytest.raises( + exceptions.BadRequestException, + match="Field required", + ): + identity_tools.update_project(id="") + + # Verify mock calls + mock_conn.identity.update_project.assert_called_once_with(project="")