Skip to content

Commit 823aa7d

Browse files
committed
feat(identity): Add create project tool(#58)
- Add create-project tool - Tests are updated ans passing
1 parent d997b58 commit 823aa7d

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed

src/openstack_mcp_server/tools/identity_tools.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def register_tools(self, mcp: FastMCP):
2828

2929
mcp.tool()(self.get_projects)
3030
mcp.tool()(self.get_project)
31+
mcp.tool()(self.create_project)
3132

3233
def get_regions(self) -> list[Region]:
3334
"""
@@ -269,3 +270,41 @@ def get_project(self, name: str) -> Project:
269270
domain_id=project.domain_id,
270271
parent_id=project.parent_id,
271272
)
273+
274+
def create_project(
275+
self,
276+
name: str,
277+
description: str | None = None,
278+
is_enabled: bool = True,
279+
domain_id: str | None = None,
280+
parent_id: str | None = None,
281+
) -> Project:
282+
"""
283+
Create a new project.
284+
285+
:param name: The name of the project.
286+
:param description: The description of the project.
287+
:param is_enabled: Whether the project is enabled.
288+
:param domain_id: The ID of the domain.
289+
:param parent_id: The ID of the parent project.
290+
291+
:return: The created Project object.
292+
"""
293+
conn = get_openstack_conn()
294+
295+
project = conn.identity.create_project(
296+
name=name,
297+
description=description,
298+
is_enabled=is_enabled,
299+
domain_id=domain_id,
300+
parent_id=parent_id,
301+
)
302+
303+
return Project(
304+
id=project.id,
305+
name=project.name,
306+
description=project.description,
307+
is_enabled=project.is_enabled,
308+
domain_id=project.domain_id,
309+
parent_id=project.parent_id,
310+
)

tests/tools/test_identity_tools.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,3 +853,192 @@ def test_get_project_not_found(self, mock_get_openstack_conn_identity):
853853
name_or_id="ProjectOne",
854854
ignore_missing=False,
855855
)
856+
857+
def test_create_project_success_with_all_fields(
858+
self, mock_get_openstack_conn_identity
859+
):
860+
"""Test creating a identity project successfully."""
861+
mock_conn = mock_get_openstack_conn_identity
862+
863+
# Create mock project object
864+
mock_project = Mock()
865+
mock_project.id = "project1111111111111111111111111"
866+
mock_project.name = "ProjectOne"
867+
mock_project.description = "Project One description"
868+
mock_project.is_enabled = True
869+
mock_project.domain_id = "domain1111111111111111111111111"
870+
mock_project.parent_id = "parentproject1111111111111111111"
871+
872+
# Configure mock project.create_project()
873+
mock_conn.identity.create_project.return_value = mock_project
874+
875+
# Test create_project()
876+
identity_tools = self.get_identity_tools()
877+
result = identity_tools.create_project(
878+
name="ProjectOne",
879+
description="Project One description",
880+
is_enabled=True,
881+
domain_id="domain1111111111111111111111111",
882+
parent_id="parentproject1111111111111111111",
883+
)
884+
885+
# Verify results
886+
assert result == Project(
887+
id="project1111111111111111111111111",
888+
name="ProjectOne",
889+
description="Project One description",
890+
is_enabled=True,
891+
domain_id="domain1111111111111111111111111",
892+
parent_id="parentproject1111111111111111111",
893+
)
894+
895+
# Verify mock calls
896+
mock_conn.identity.create_project.assert_called_once_with(
897+
name="ProjectOne",
898+
description="Project One description",
899+
is_enabled=True,
900+
domain_id="domain1111111111111111111111111",
901+
parent_id="parentproject1111111111111111111",
902+
)
903+
904+
def test_create_project_parent_disabled_failed(
905+
self, mock_get_openstack_conn_identity
906+
):
907+
"""Test creating a identity project with a parent project that is disabled."""
908+
mock_conn = mock_get_openstack_conn_identity
909+
910+
# Configure mock to raise BadRequestException
911+
mock_conn.identity.create_project.side_effect = exceptions.BadRequestException(
912+
"cannot create a project in a branch containing a disabled project",
913+
)
914+
915+
# Test create_project()
916+
identity_tools = self.get_identity_tools()
917+
918+
# Verify exception is raised
919+
with pytest.raises(
920+
exceptions.BadRequestException,
921+
match="cannot create a project in a branch containing a disabled project",
922+
):
923+
identity_tools.create_project(
924+
name="ProjectOne",
925+
description="Project One description",
926+
is_enabled=True,
927+
domain_id=None,
928+
parent_id="parentproject1111111111111111111",
929+
)
930+
931+
# Verify mock calls
932+
mock_conn.identity.create_project.assert_called_once_with(
933+
name="ProjectOne",
934+
description="Project One description",
935+
is_enabled=True,
936+
domain_id=None,
937+
parent_id="parentproject1111111111111111111",
938+
)
939+
940+
def test_create_project_without_all_fields(
941+
self, mock_get_openstack_conn_identity
942+
):
943+
"""Test creating a identity project without all fields."""
944+
mock_conn = mock_get_openstack_conn_identity
945+
946+
mock_conn.identity.create_project.side_effect = (
947+
exceptions.BadRequestException(
948+
"Field required",
949+
)
950+
)
951+
952+
# Test create_project()
953+
identity_tools = self.get_identity_tools()
954+
955+
with pytest.raises(
956+
exceptions.BadRequestException,
957+
match="Field required",
958+
):
959+
identity_tools.create_project(
960+
name="ProjectOne",
961+
description="Project One description",
962+
is_enabled=True,
963+
domain_id=None,
964+
parent_id=None,
965+
)
966+
967+
# Verify mock calls
968+
mock_conn.identity.create_project.assert_called_once_with(
969+
name="ProjectOne",
970+
description="Project One description",
971+
is_enabled=True,
972+
domain_id=None,
973+
parent_id=None,
974+
)
975+
976+
def test_create_project_domain_not_found(
977+
self, mock_get_openstack_conn_identity
978+
):
979+
"""Test creating a identity project with a domain that does not exist."""
980+
mock_conn = mock_get_openstack_conn_identity
981+
982+
# Configure mock to raise BadRequestException
983+
mock_conn.identity.create_project.side_effect = exceptions.BadRequestException(
984+
"Domain 'domain1111111111111111111111111' not found. Please check the domain ID.",
985+
)
986+
987+
# Test create_project()
988+
identity_tools = self.get_identity_tools()
989+
990+
with pytest.raises(
991+
exceptions.BadRequestException,
992+
match="Domain 'domain1111111111111111111111111' not found. Please check the domain ID.",
993+
):
994+
identity_tools.create_project(
995+
name="ProjectOne",
996+
description="Project One description",
997+
is_enabled=True,
998+
domain_id="domain1111111111111111111111111",
999+
parent_id=None,
1000+
)
1001+
1002+
# Verify mock calls
1003+
mock_conn.identity.create_project.assert_called_once_with(
1004+
name="ProjectOne",
1005+
description="Project One description",
1006+
is_enabled=True,
1007+
domain_id="domain1111111111111111111111111",
1008+
parent_id=None,
1009+
)
1010+
1011+
def test_create_project_parent_not_found(
1012+
self, mock_get_openstack_conn_identity
1013+
):
1014+
"""Test creating a identity project with a parent project that does not exist."""
1015+
mock_conn = mock_get_openstack_conn_identity
1016+
1017+
# Configure mock to raise BadRequestException
1018+
mock_conn.identity.create_project.side_effect = exceptions.BadRequestException(
1019+
"Parent project 'parentproject1111111111111111111' not found. Please check the parent project ID.",
1020+
)
1021+
1022+
# Test create_project()
1023+
identity_tools = self.get_identity_tools()
1024+
1025+
with pytest.raises(
1026+
exceptions.BadRequestException,
1027+
match="Parent project 'parentproject1111111111111111111' not found. Please check the parent project ID.",
1028+
):
1029+
identity_tools.create_project(
1030+
name="ProjectOne",
1031+
description="Project One description",
1032+
is_enabled=True,
1033+
domain_id=None,
1034+
parent_id="parentproject1111111111111111111",
1035+
)
1036+
1037+
# Verify mock calls
1038+
mock_conn.identity.create_project.assert_called_once_with(
1039+
name="ProjectOne",
1040+
description="Project One description",
1041+
is_enabled=True,
1042+
domain_id=None,
1043+
parent_id="parentproject1111111111111111111",
1044+
)

0 commit comments

Comments
 (0)