Skip to content

Commit 7b3a56e

Browse files
committed
feat(identity): Add create, delete, update domain(#31)
- Add create, delete, update domain tool - Test codes are updated and passing
1 parent e396261 commit 7b3a56e

File tree

2 files changed

+255
-13
lines changed

2 files changed

+255
-13
lines changed

src/openstack_mcp_server/tools/identity_tools.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def register_tools(self, mcp: FastMCP):
2222

2323
mcp.tool()(self.get_domains)
2424
mcp.tool()(self.get_domain)
25+
mcp.tool()(self.create_domain)
26+
mcp.tool()(self.delete_domain)
27+
mcp.tool()(self.update_domain)
2528

2629
def get_regions(self) -> list[Region]:
2730
"""
@@ -119,17 +122,68 @@ def get_domains(self) -> list[Domain]:
119122
)
120123
return domain_list
121124

122-
def get_domain(self, id: str) -> Domain:
125+
def get_domain(self, name: str) -> Domain:
123126
"""
124127
Get a domain.
125128
126-
:param id: The ID of the domain. (required)
129+
:param name: The name of the domain. (required)
127130
128131
:return: The Domain object.
129132
"""
130133
conn = get_openstack_conn()
131134

132-
domain = conn.identity.get_domain(domain=id)
135+
domain = conn.identity.find_domain(name_or_id=name)
133136

134137
return Domain(id=domain.id, name=domain.name, description=domain.description, is_enabled=domain.is_enabled)
135138

139+
def create_domain(self, name: str, description: str = "", is_enabled: bool = False) -> Domain:
140+
"""
141+
Create a new domain.
142+
143+
:param name: The name of the domain. (required)
144+
:param description: The description of the domain. (optional)
145+
:param is_enabled: Whether the domain is enabled. (optional)
146+
"""
147+
conn = get_openstack_conn()
148+
149+
domain = conn.identity.create_domain(name=name, description=description, enabled=is_enabled)
150+
151+
return Domain(id=domain.id, name=domain.name, description=domain.description, is_enabled=domain.is_enabled)
152+
153+
def delete_domain(self, name: str) -> None:
154+
"""
155+
Delete a domain.
156+
157+
:param name: The name of the domain. (required)
158+
"""
159+
conn = get_openstack_conn()
160+
161+
domain = conn.identity.find_domain(name_or_id=name)
162+
conn.identity.delete_domain(domain=domain, ignore_missing=False)
163+
164+
return None
165+
166+
def update_domain(self, name: str, description: str | None = None, is_enabled: bool | None = None) -> Domain:
167+
"""
168+
Update a domain.
169+
170+
:param name: The name of the domain. (required)
171+
:param description: The description of the domain. (optional)
172+
:param is_enabled: Whether the domain is enabled. (optional)
173+
"""
174+
conn = get_openstack_conn()
175+
176+
args = {}
177+
if name is not None:
178+
args["name"] = name
179+
if description is not None:
180+
args["description"] = description
181+
if is_enabled is not None:
182+
args["is_enabled"] = is_enabled
183+
184+
domain = conn.identity.find_domain(name_or_id=name)
185+
updated_domain = conn.identity.update_domain(domain=domain, **args)
186+
187+
return Domain(id=updated_domain.id, name=updated_domain.name, description=updated_domain.description, is_enabled=updated_domain.is_enabled)
188+
189+

tests/tools/test_identity_tools.py

Lines changed: 198 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest.mock import Mock
22

33
import pytest
4+
import pydantic
45

56
from openstack import exceptions
67

@@ -392,30 +393,30 @@ def test_get_domain_success(self, mock_get_openstack_conn_identity):
392393

393394
# Create mock domain object
394395
mock_domain = Mock()
395-
mock_domain.id = "domainone"
396-
mock_domain.name = "DomainOne"
397-
mock_domain.description = "Domain One description"
396+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
397+
mock_domain.name = "domainone"
398+
mock_domain.description = "domainone description"
398399
mock_domain.is_enabled = True
399400

400401
# Configure mock domain.get_domain()
401-
mock_conn.identity.get_domain.return_value = mock_domain
402+
mock_conn.identity.find_domain.return_value = mock_domain
402403

403404
# Test get_domain()
404405
identity_tools = self.get_identity_tools()
405-
result = identity_tools.get_domain(id="domainone")
406+
result = identity_tools.get_domain(name="domainone")
406407

407408
# Verify results
408-
assert result == Domain(id="domainone", name="DomainOne", description="Domain One description", is_enabled=True)
409+
assert result == Domain(id="d01a81393377480cbd75c0210442e687", name="domainone", description="domainone description", is_enabled=True)
409410

410411
# Verify mock calls
411-
mock_conn.identity.get_domain.assert_called_once_with(domain="domainone")
412+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="domainone")
412413

413414
def test_get_domain_not_found(self, mock_get_openstack_conn_identity):
414415
"""Test getting a identity domain that does not exist."""
415416
mock_conn = mock_get_openstack_conn_identity
416417

417418
# Configure mock to raise NotFoundException
418-
mock_conn.identity.get_domain.side_effect = exceptions.NotFoundException(
419+
mock_conn.identity.find_domain.side_effect = exceptions.NotFoundException(
419420
"Domain 'domainone' not found",
420421
)
421422

@@ -424,7 +425,194 @@ def test_get_domain_not_found(self, mock_get_openstack_conn_identity):
424425

425426
# Verify exception is raised
426427
with pytest.raises(exceptions.NotFoundException, match="Domain 'domainone' not found"):
427-
identity_tools.get_domain(id="domainone")
428+
identity_tools.get_domain(name="domainone")
428429

429430
# Verify mock calls
430-
mock_conn.identity.get_domain.assert_called_once_with(domain="domainone")
431+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="domainone")
432+
433+
def test_create_domain_success(self, mock_get_openstack_conn_identity):
434+
"""Test creating a identity domain successfully."""
435+
mock_conn = mock_get_openstack_conn_identity
436+
437+
# Create mock domain object
438+
mock_domain = Mock()
439+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
440+
mock_domain.name = "domainone"
441+
mock_domain.description = "domainone description"
442+
mock_domain.is_enabled = True
443+
444+
# Configure mock domain.create_domain()
445+
mock_conn.identity.create_domain.return_value = mock_domain
446+
447+
# Test create_domain()
448+
identity_tools = self.get_identity_tools()
449+
result = identity_tools.create_domain(name="domainone", description="domainone description", is_enabled=True)
450+
451+
# Verify results
452+
assert result == Domain(id="d01a81393377480cbd75c0210442e687", name="domainone", description="domainone description", is_enabled=True)
453+
454+
# Verify mock calls
455+
mock_conn.identity.create_domain.assert_called_once_with(name="domainone", description="domainone description", enabled=True)
456+
457+
def test_create_domain_without_name(self, mock_get_openstack_conn_identity):
458+
"""Test creating a identity domain without a name."""
459+
mock_conn = mock_get_openstack_conn_identity
460+
461+
# Test create_domain()
462+
identity_tools = self.get_identity_tools()
463+
464+
# Verify pydantic validation exception is raised
465+
with pytest.raises(pydantic.ValidationError):
466+
identity_tools.create_domain(name="", description="domainone description", is_enabled=False)
467+
468+
def test_create_domain_without_description(self, mock_get_openstack_conn_identity):
469+
"""Test creating a identity domain without a description."""
470+
mock_conn = mock_get_openstack_conn_identity
471+
472+
# Create mock domain object
473+
mock_domain = Mock()
474+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
475+
mock_domain.name = "domainone"
476+
mock_domain.description = ""
477+
mock_domain.is_enabled = False
478+
479+
# Configure mock domain.create_domain()
480+
mock_conn.identity.create_domain.return_value = mock_domain
481+
482+
# Test create_domain()
483+
identity_tools = self.get_identity_tools()
484+
result = identity_tools.create_domain(name="domainone")
485+
486+
# Verify results
487+
assert result == Domain(id="d01a81393377480cbd75c0210442e687", name="domainone", description="", is_enabled=False)
488+
489+
# Verify mock calls
490+
mock_conn.identity.create_domain.assert_called_once_with(name="domainone", description="", enabled=False)
491+
492+
493+
def test_delete_domain_success(self, mock_get_openstack_conn_identity):
494+
"""Test deleting a identity domain successfully."""
495+
mock_conn = mock_get_openstack_conn_identity
496+
497+
# mock
498+
mock_domain = Mock()
499+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
500+
mock_domain.name = "domainone"
501+
mock_domain.description = "domainone description"
502+
mock_domain.is_enabled = True
503+
504+
mock_conn.identity.find_domain.return_value = mock_domain
505+
506+
# Test delete_domain()
507+
identity_tools = self.get_identity_tools()
508+
result = identity_tools.delete_domain(name="domainone")
509+
510+
# Verify results
511+
assert result is None
512+
513+
# Verify mock calls
514+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="domainone")
515+
mock_conn.identity.delete_domain.assert_called_once_with(domain=mock_domain, ignore_missing=False)
516+
517+
518+
def test_delete_domain_not_found(self, mock_get_openstack_conn_identity):
519+
"""Test deleting a identity domain that does not exist."""
520+
mock_conn = mock_get_openstack_conn_identity
521+
522+
# Create mock domain object
523+
mock_domain = Mock()
524+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
525+
mock_domain.name = "domainone"
526+
mock_domain.description = "domainone description"
527+
mock_domain.is_enabled = True
528+
529+
mock_conn.identity.find_domain.return_value = mock_domain
530+
531+
# Configure mock to raise NotFoundException
532+
mock_conn.identity.delete_domain.side_effect = exceptions.NotFoundException(
533+
"Domain 'domainone' not found",
534+
)
535+
536+
# Test delete_domain()
537+
identity_tools = self.get_identity_tools()
538+
539+
# Verify exception is raised
540+
with pytest.raises(exceptions.NotFoundException, match="Domain 'domainone' not found"):
541+
identity_tools.delete_domain(name="domainone")
542+
543+
# Verify mock calls
544+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="domainone")
545+
mock_conn.identity.delete_domain.assert_called_once_with(domain=mock_domain, ignore_missing=False)
546+
547+
def test_update_domain_with_all_fields_success(self, mock_get_openstack_conn_identity):
548+
"""Test updating a identity domain successfully."""
549+
mock_conn = mock_get_openstack_conn_identity
550+
551+
# Create mock domain object
552+
mock_domain = Mock()
553+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
554+
mock_domain.name = "domainone"
555+
mock_domain.description = "domainone description"
556+
mock_domain.is_enabled = True
557+
558+
# Configure mock domain.find_domain() and update_domain()
559+
mock_conn.identity.find_domain.return_value = mock_domain
560+
mock_conn.identity.update_domain.return_value = mock_domain
561+
562+
# Test update_domain()
563+
identity_tools = self.get_identity_tools()
564+
result = identity_tools.update_domain(name="domainone", description="domainone description", is_enabled=True)
565+
566+
# Verify results
567+
assert result == Domain(id="d01a81393377480cbd75c0210442e687", name="domainone", description="domainone description", is_enabled=True)
568+
569+
# Verify mock calls
570+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="domainone")
571+
mock_conn.identity.update_domain.assert_called_once_with(domain=mock_domain, name="domainone", description="domainone description", is_enabled=True)
572+
573+
574+
def test_update_domain_with_empty_args(self, mock_get_openstack_conn_identity):
575+
"""Test updating a identity domain with empty arguments."""
576+
mock_conn = mock_get_openstack_conn_identity
577+
578+
# Create mock domain object
579+
mock_domain = Mock()
580+
mock_domain.id = "d01a81393377480cbd75c0210442e687"
581+
mock_domain.name = "domainone"
582+
mock_domain.description = "domainone description"
583+
mock_domain.is_enabled = True
584+
585+
# Configure mock domain.find_domain() and update_domain()
586+
mock_conn.identity.find_domain.return_value = mock_domain
587+
mock_conn.identity.update_domain.return_value = mock_domain
588+
589+
# Test update_domain()
590+
identity_tools = self.get_identity_tools()
591+
result = identity_tools.update_domain(name="domainone")
592+
593+
# Verify results
594+
assert result == Domain(id="d01a81393377480cbd75c0210442e687", name="domainone", description="domainone description", is_enabled=True)
595+
596+
# Verify mock calls
597+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="domainone")
598+
mock_conn.identity.update_domain.assert_called_once_with(domain=mock_domain, name="domainone")
599+
600+
def test_update_domain_with_empty_name(self, mock_get_openstack_conn_identity):
601+
"""Test updating a identity domain with an empty name."""
602+
mock_conn = mock_get_openstack_conn_identity
603+
604+
mock_conn.identity.find_domain.side_effect = exceptions.BadRequestException(
605+
"Field required",
606+
)
607+
608+
# Test update_domain()
609+
identity_tools = self.get_identity_tools()
610+
611+
# Verify exception is raised
612+
with pytest.raises(exceptions.BadRequestException, match="Field required"):
613+
identity_tools.update_domain(name="")
614+
615+
# Verify mock calls
616+
mock_conn.identity.find_domain.assert_called_once_with(name_or_id="")
617+
618+

0 commit comments

Comments
 (0)