Skip to content

Commit f5384de

Browse files
S0okJuhalucinor
authored andcommitted
feat: Add get-project, get-projects tool (#63)
1 parent 5f45d0e commit f5384de

File tree

3 files changed

+198
-2
lines changed

3 files changed

+198
-2
lines changed

src/openstack_mcp_server/tools/identity_tools.py

Lines changed: 50 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, Project, Region
55

66

77
class IdentityTools:
@@ -26,6 +26,9 @@ def register_tools(self, mcp: FastMCP):
2626
mcp.tool()(self.delete_domain)
2727
mcp.tool()(self.update_domain)
2828

29+
mcp.tool()(self.get_projects)
30+
mcp.tool()(self.get_project)
31+
2932
def get_regions(self) -> list[Region]:
3033
"""
3134
Get the list of Identity regions.
@@ -220,3 +223,49 @@ 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(
261+
name_or_id=name, ignore_missing=False
262+
)
263+
264+
return Project(
265+
id=project.id,
266+
name=project.name,
267+
description=project.description,
268+
is_enabled=project.is_enabled,
269+
domain_id=project.domain_id,
270+
parent_id=project.parent_id,
271+
)

src/openstack_mcp_server/tools/response/identity.py

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

tests/tools/test_identity_tools.py

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
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 (
10+
Domain,
11+
Project,
12+
Region,
13+
)
1014

1115

1216
class TestIdentityTools:
@@ -715,3 +719,137 @@ def test_update_domain_with_empty_id(
715719

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

0 commit comments

Comments
 (0)