Skip to content

Commit 7340ef7

Browse files
committed
feat(identity): Add get-project, get-projects tool(#58)
- Add get-project, get-projects tool - Test code are updated and passing
1 parent 481dffd commit 7340ef7

File tree

4 files changed

+231
-2
lines changed

4 files changed

+231
-2
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# file generated by setuptools-scm
2+
# don't change, don't track in version control
3+
4+
__all__ = [
5+
"__version__",
6+
"__version_tuple__",
7+
"version",
8+
"version_tuple",
9+
"__commit_id__",
10+
"commit_id",
11+
]
12+
13+
TYPE_CHECKING = False
14+
if TYPE_CHECKING:
15+
from typing import Tuple
16+
from typing import Union
17+
18+
VERSION_TUPLE = Tuple[Union[int, str], ...]
19+
COMMIT_ID = Union[str, None]
20+
else:
21+
VERSION_TUPLE = object
22+
COMMIT_ID = object
23+
24+
version: str
25+
__version__: str
26+
__version_tuple__: VERSION_TUPLE
27+
version_tuple: VERSION_TUPLE
28+
commit_id: COMMIT_ID
29+
__commit_id__: COMMIT_ID
30+
31+
__version__ = version = '0.1.dev38+g481dffd50.d20250825'
32+
__version_tuple__ = version_tuple = (0, 1, 'dev38', 'g481dffd50.d20250825')
33+
34+
__commit_id__ = commit_id = 'g481dffd50'

src/openstack_mcp_server/tools/identity_tools.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastmcp import FastMCP
22

33
from .base import get_openstack_conn
4-
from .response.identity import Domain, Region
4+
from .response.identity import Domain, Region, Project
55

66

77
class IdentityTools:
@@ -25,6 +25,9 @@ def register_tools(self, mcp: FastMCP):
2525
mcp.tool()(self.create_domain)
2626
mcp.tool()(self.delete_domain)
2727
mcp.tool()(self.update_domain)
28+
29+
mcp.tool()(self.get_projects)
30+
mcp.tool()(self.get_project)
2831

2932
def get_regions(self) -> list[Region]:
3033
"""
@@ -220,3 +223,48 @@ def update_domain(
220223
description=updated_domain.description,
221224
is_enabled=updated_domain.is_enabled,
222225
)
226+
227+
def get_projects(self) -> list[Project]:
228+
"""
229+
Get the list of Identity projects.
230+
231+
:return: A list of Project objects representing the projects.
232+
"""
233+
conn = get_openstack_conn()
234+
235+
project_list = []
236+
for project in conn.identity.projects():
237+
project_list.append(
238+
Project(
239+
id=project.id,
240+
name=project.name,
241+
description=project.description,
242+
is_enabled=project.is_enabled,
243+
domain_id=project.domain_id,
244+
parent_id=project.parent_id,
245+
),
246+
)
247+
248+
return project_list
249+
250+
def get_project(self, name: str) -> Project:
251+
"""
252+
Get a project.
253+
254+
:param name: The name of the project.
255+
256+
:return: The Project object.
257+
"""
258+
conn = get_openstack_conn()
259+
260+
project = conn.identity.find_project(name_or_id=name, ignore_missing=False)
261+
262+
return Project(
263+
id=project.id,
264+
name=project.name,
265+
description=project.description,
266+
is_enabled=project.is_enabled,
267+
domain_id=project.domain_id,
268+
parent_id=project.parent_id,
269+
)
270+

src/openstack_mcp_server/tools/response/identity.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ class Domain(BaseModel):
1313
name: str
1414
description: str | None = None
1515
is_enabled: bool | None = None
16+
17+
class Project(BaseModel):
18+
id: str
19+
name: str
20+
description: str | None = None
21+
is_enabled: bool | None = None
22+
domain_id: str | None = None
23+
parent_id: str | None = None

tests/tools/test_identity_tools.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from openstack import exceptions
77

88
from openstack_mcp_server.tools.identity_tools import IdentityTools
9-
from openstack_mcp_server.tools.response.identity import Domain, Region
9+
from openstack_mcp_server.tools.response.identity import Domain, Region, Project
1010

1111

1212
class TestIdentityTools:
@@ -715,3 +715,142 @@ def test_update_domain_with_empty_id(
715715

716716
# Verify mock calls
717717
mock_conn.identity.update_domain.assert_called_once_with(domain="")
718+
719+
def test_get_projects_success(self, mock_get_openstack_conn_identity):
720+
"""Test getting identity projects successfully."""
721+
mock_conn = mock_get_openstack_conn_identity
722+
723+
# Create mock project objects
724+
mock_project1 = Mock()
725+
mock_project1.id = "project1111111111111111111111111"
726+
mock_project1.name = "ProjectOne"
727+
mock_project1.description = "Project One description"
728+
mock_project1.is_enabled = True
729+
mock_project1.domain_id = "domain1111111111111111111111111"
730+
mock_project1.parent_id = "parentproject1111111111111111111"
731+
732+
mock_project2 = Mock()
733+
mock_project2.id = "project2222222222222222222222222"
734+
mock_project2.name = "ProjectTwo"
735+
mock_project2.description = "Project Two description"
736+
mock_project2.is_enabled = False
737+
mock_project2.domain_id = "domain22222222222222222222222222"
738+
mock_project2.parent_id = "default"
739+
740+
# Configure mock project.projects()
741+
mock_conn.identity.projects.return_value = [mock_project1, mock_project2]
742+
743+
# Test get_projects()
744+
identity_tools = self.get_identity_tools()
745+
result = identity_tools.get_projects()
746+
747+
# Verify results
748+
assert result == [
749+
Project(
750+
id="project1111111111111111111111111",
751+
name="ProjectOne",
752+
description="Project One description",
753+
is_enabled=True,
754+
domain_id="domain1111111111111111111111111",
755+
parent_id="parentproject1111111111111111111",
756+
),
757+
Project(
758+
id="project2222222222222222222222222",
759+
name="ProjectTwo",
760+
description="Project Two description",
761+
is_enabled=False,
762+
domain_id="domain22222222222222222222222222",
763+
parent_id="default",
764+
),
765+
]
766+
767+
# Verify mock calls
768+
mock_conn.identity.projects.assert_called_once()
769+
770+
771+
772+
def test_get_projects_empty_list(self, mock_get_openstack_conn_identity):
773+
"""Test getting identity projects when there are no projects."""
774+
mock_conn = mock_get_openstack_conn_identity
775+
776+
# Empty project list
777+
mock_conn.identity.projects.return_value = []
778+
779+
# Test get_projects()
780+
identity_tools = self.get_identity_tools()
781+
result = identity_tools.get_projects()
782+
783+
# Verify results
784+
assert result == []
785+
786+
# Verify mock calls
787+
mock_conn.identity.projects.assert_called_once()
788+
789+
def test_get_project_success(self, mock_get_openstack_conn_identity):
790+
"""Test getting a identity project successfully."""
791+
mock_conn = mock_get_openstack_conn_identity
792+
793+
# Create mock project object
794+
mock_project = Mock()
795+
mock_project.id = "project1111111111111111111111111"
796+
mock_project.name = "ProjectOne"
797+
mock_project.description = "Project One description"
798+
mock_project.is_enabled = True
799+
mock_project.domain_id = "domain1111111111111111111111111"
800+
mock_project.parent_id = "parentproject1111111111111111111"
801+
802+
# Configure mock project.find_project()
803+
mock_conn.identity.find_project.return_value = mock_project
804+
805+
# Test get_project()
806+
identity_tools = self.get_identity_tools()
807+
result = identity_tools.get_project(name="ProjectOne")
808+
809+
# Verify results
810+
assert result == Project(
811+
id="project1111111111111111111111111",
812+
name="ProjectOne",
813+
description="Project One description",
814+
is_enabled=True,
815+
domain_id="domain1111111111111111111111111",
816+
parent_id="parentproject1111111111111111111",
817+
)
818+
819+
# Verify mock calls
820+
mock_conn.identity.find_project.assert_called_once_with(
821+
name_or_id="ProjectOne",
822+
ignore_missing=False,
823+
)
824+
825+
def test_get_project_not_found(self, mock_get_openstack_conn_identity):
826+
"""Test getting a identity project that does not exist."""
827+
mock_conn = mock_get_openstack_conn_identity
828+
829+
# Configure mock to raise NotFoundException
830+
mock_conn.identity.find_project.side_effect = (
831+
exceptions.NotFoundException(
832+
"Project 'ProjectOne' not found",
833+
)
834+
)
835+
836+
# Test get_project()
837+
identity_tools = self.get_identity_tools()
838+
839+
# Verify exception is raised
840+
with pytest.raises(
841+
exceptions.NotFoundException,
842+
match="Project 'ProjectOne' not found",
843+
):
844+
identity_tools.get_project(name="ProjectOne")
845+
846+
# Verify mock calls
847+
mock_conn.identity.find_project.assert_called_once_with(
848+
name_or_id="ProjectOne",
849+
ignore_missing=False,
850+
)
851+
852+
853+
854+
855+
856+

0 commit comments

Comments
 (0)