Skip to content

Commit 1da7bf6

Browse files
committed
[CHORE] Add VNIC tools
Signed-off-by: will.shope <[email protected]>
1 parent d7e0fc6 commit 1da7bf6

File tree

11 files changed

+411
-35
lines changed

11 files changed

+411
-35
lines changed

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
"""
66

77
__project__ = "oracle.oci-compute-mcp-server"
8-
__version__ = "1.0.2"
8+
__version__ = "1.1.0"

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/models.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,3 +781,71 @@ def map_response(resp: oci.response.Response) -> Response | None:
781781

782782

783783
# endregion
784+
785+
# region VnicAttachment
786+
787+
788+
class VnicAttachment(BaseModel):
789+
"""
790+
Pydantic model mirroring the fields of oci.core.models.VnicAttachment.
791+
"""
792+
793+
availability_domain: Optional[str] = Field(
794+
None, description="The availability domain of the instance. Example: `Uocm:PHX-AD-1`"
795+
)
796+
compartment_id: Optional[str] = Field(
797+
None, description="The OCID of the compartment the VNIC attachment is in, which is the same compartment the instance is in."
798+
)
799+
display_name: Optional[str] = Field(
800+
None, description="A user-friendly name. Does not have to be unique, and it's changeable. Avoid entering confidential information."
801+
)
802+
id: Optional[str] = Field(
803+
None, description="The OCID of the VNIC attachment."
804+
)
805+
instance_id: Optional[str] = Field(
806+
None, description="The OCID of the instance."
807+
)
808+
lifecycle_state: Optional[Literal["ATTACHING", "ATTACHED", "DETACHING", "DETACHED", "UNKNOWN_ENUM_VALUE"]] = Field(
809+
None, description="The current state of the VNIC attachment."
810+
)
811+
nic_index: Optional[int] = Field(
812+
None, description="Which physical network interface card (NIC) the VNIC uses. Certain bare metal instance shapes have two active physical NICs (0 and 1). If you add a secondary VNIC to one of these instances, you can specify which NIC the VNIC will use. For more information, see Virtual Network Interface Cards (VNICs)."
813+
)
814+
subnet_id: Optional[str] = Field(
815+
None, description="The OCID of the subnet to create the VNIC in."
816+
)
817+
vlan_id: Optional[str] = Field(
818+
None, description="The OCID of the VLAN to create the VNIC in. Creating the VNIC in a VLAN (instead of a subnet) is possible only if you are an Oracle Cloud VMware Solution customer. See Vlan. An error is returned if the instance already has a VNIC attached to it from this VLAN."
819+
)
820+
time_created: Optional[datetime] = Field(
821+
None, description="The date and time the VNIC attachment was created, in the format defined by RFC3339. Example: `2016-08-25T21:10:29.600Z`"
822+
)
823+
vlan_tag: Optional[int] = Field(
824+
None, description="The Oracle-assigned VLAN tag of the attached VNIC. Available after the attachment process is complete. However, if the VNIC belongs to a VLAN as part of the Oracle Cloud VMware Solution, the `vlanTag` value is instead the value of the `vlanTag` attribute for the VLAN. See Vlan. Example: `0`"
825+
)
826+
vnic_id: Optional[str] = Field(
827+
None, description="The OCID of the VNIC. Available after the attachment process is complete."
828+
)
829+
830+
831+
def map_vnic_attachment(va: oci.core.models.VnicAttachment) -> VnicAttachment:
832+
"""
833+
Convert an oci.core.models.VnicAttachment to oracle.oci_compute_mcp_server.models.VnicAttachment.
834+
"""
835+
return VnicAttachment(
836+
availability_domain=getattr(va, "availability_domain", None),
837+
compartment_id=getattr(va, "compartment_id", None),
838+
display_name=getattr(va, "display_name", None),
839+
id=getattr(va, "id", None),
840+
instance_id=getattr(va, "instance_id", None),
841+
lifecycle_state=getattr(va, "lifecycle_state", None),
842+
nic_index=getattr(va, "nic_index", None),
843+
subnet_id=getattr(va, "subnet_id", None),
844+
vlan_id=getattr(va, "vlan_id", None),
845+
time_created=getattr(va, "time_created", None),
846+
vlan_tag=getattr(va, "vlan_tag", None),
847+
vnic_id=getattr(va, "vnic_id", None),
848+
)
849+
850+
851+
# endregion

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
from oracle.oci_compute_mcp_server.models import (
2020
Image,
2121
Instance,
22+
VnicAttachment,
2223
Response,
2324
map_image,
2425
map_instance,
26+
map_vnic_attachment,
2527
map_response,
2628
)
2729
from pydantic import Field
@@ -88,7 +90,7 @@ def list_instances(
8890
"limit": limit,
8991
}
9092

91-
if lifecycle_state is not None:
93+
if lifecycle_state:
9294
kwargs["lifecycle_state"] = lifecycle_state
9395

9496
response = client.list_instances(**kwargs)
@@ -272,6 +274,11 @@ def list_images(
272274
operating_system: Optional[str] = Field(
273275
None, description="The operating system to filter with"
274276
),
277+
limit: Optional[int] = Field(
278+
None,
279+
description="The maximum amount of resources to return. If None, there is no limit.",
280+
ge=1,
281+
),
275282
) -> list[Image]:
276283
images: list[Image] = []
277284

@@ -282,8 +289,14 @@ def list_images(
282289
has_next_page = True
283290
next_page: str = None
284291

285-
while has_next_page:
286-
response = client.list_images(compartment_id=compartment_id, page=next_page)
292+
while has_next_page and (limit is None or len(images) < limit):
293+
kwargs = {
294+
"compartment_id": compartment_id,
295+
"page": next_page,
296+
"limit": limit,
297+
}
298+
299+
response = client.list_images(**kwargs)
287300
has_next_page = response.has_next_page
288301
next_page = response.next_page if hasattr(response, "next_page") else None
289302

@@ -345,6 +358,79 @@ def instance_action(
345358
raise e
346359

347360

361+
@mcp.tool(
362+
description="List vnic attachments in a given compartment and/or on a given instance. "
363+
)
364+
def list_vnic_attachments(
365+
compartment_id: str = Field(
366+
...,
367+
description="The OCID of the compartment. "
368+
"If an instance_id is passed in, but no compartment_id is passed in,"
369+
"then the compartment OCID of the instance may be used as a default."
370+
),
371+
instance_id: Optional[str] = Field(
372+
None,
373+
description="The OCID of the instance"
374+
),
375+
limit: Optional[int] = Field(
376+
None,
377+
description="The maximum amount of resources to return. If None, there is no limit.",
378+
ge=1,
379+
),
380+
) -> list[VnicAttachment]:
381+
vnic_attachments: list[VnicAttachment] = []
382+
383+
try:
384+
client = get_compute_client()
385+
386+
response: oci.response.Response = None
387+
has_next_page = True
388+
next_page: str = None
389+
390+
while has_next_page and (limit is None or len(vnic_attachments) < limit):
391+
kwargs = {
392+
"compartment_id": compartment_id,
393+
"page": next_page,
394+
"limit": limit,
395+
}
396+
397+
if instance_id:
398+
kwargs["instance_id"] = instance_id
399+
400+
response = client.list_vnic_attachments(**kwargs)
401+
has_next_page = response.has_next_page
402+
next_page = response.next_page if hasattr(response, "next_page") else None
403+
404+
data: list[oci.core.models.VnicAttachment] = response.data
405+
406+
for d in data:
407+
vnic_attachments.append(map_vnic_attachment(d))
408+
409+
logger.info(f"Found {len(vnic_attachments)} Vnic Attachments")
410+
return vnic_attachments
411+
412+
except Exception as e:
413+
logger.error(f"Error in list_vnic_attachments tool: {str(e)}")
414+
raise e
415+
416+
417+
@mcp.tool(description="Get Vnic Attachment with a given OCID")
418+
def get_vnic_attachment(
419+
vnic_attachment_id: str = Field(..., description="The OCID of the vnic attachment")
420+
) -> VnicAttachment:
421+
try:
422+
client = get_compute_client()
423+
424+
response: oci.response.Response = client.get_vnic_attachment(vnic_attachment_id=vnic_attachment_id)
425+
data: oci.core.models.VnicAttachment = response.data
426+
logger.info("Found Vnic Attachment")
427+
return map_vnic_attachment(data)
428+
429+
except Exception as e:
430+
logger.error(f"Error in get_vnic_attachment tool: {str(e)}")
431+
raise e
432+
433+
348434
def main() -> None:
349435
mcp.run()
350436

src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/tests/test_compute_tools.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,55 @@ async def test_instance_action(self, mock_get_client):
227227

228228
assert result["id"] == "instance1"
229229
assert result["lifecycle_state"] == "STOPPING"
230+
231+
@pytest.mark.asyncio
232+
@patch("oracle.oci_compute_mcp_server.server.get_compute_client")
233+
async def test_list_vnic_attachments(self, mock_get_client):
234+
mock_client = MagicMock()
235+
mock_get_client.return_value = mock_client
236+
237+
mock_list_response = create_autospec(oci.response.Response)
238+
mock_list_response.data = [
239+
oci.core.models.VnicAttachment(
240+
id="vnicattachment1",
241+
display_name="VNIC attachment 1",
242+
lifecycle_state="ATTACHED"
243+
)
244+
]
245+
mock_list_response.has_next_page = False
246+
mock_list_response.next_page = None
247+
mock_client.list_vnic_attachments.return_value = mock_list_response
248+
249+
async with Client(mcp) as client:
250+
call_tool_result = await client.call_tool(
251+
"list_vnic_attachments",
252+
{
253+
"compartment_id": "test_compartment",
254+
},
255+
)
256+
result = call_tool_result.structured_content["result"]
257+
258+
assert len(result) == 1
259+
assert result[0]["id"] == "vnicattachment1"
260+
261+
@pytest.mark.asyncio
262+
@patch("oracle.oci_compute_mcp_server.server.get_compute_client")
263+
async def test_get_vnic_attachment(self, mock_get_client):
264+
mock_client = MagicMock()
265+
mock_get_client.return_value = mock_client
266+
267+
mock_get_response = create_autospec(oci.response.Response)
268+
mock_get_response.data = oci.core.models.VnicAttachment(
269+
id="vnicattachment1",
270+
display_name="VNIC attachment 1",
271+
lifecycle_state="ATTACHED"
272+
)
273+
mock_client.get_vnic_attachment.return_value = mock_get_response
274+
275+
async with Client(mcp) as client:
276+
call_tool_result = await client.call_tool(
277+
"get_vnic_attachment", {"vnic_attachment_id": "vnicattachment1"}
278+
)
279+
result = call_tool_result.structured_content
280+
281+
assert result["id"] == "vnicattachment1"

src/oci-compute-mcp-server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "oracle.oci-compute-mcp-server"
3-
version = "1.0.2"
3+
version = "1.1.0"
44
description = "OCI Compute Service MCP server"
55
readme = "README.md"
66
requires-python = ">=3.13"

0 commit comments

Comments
 (0)