Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ The MCP server provides the following tools for interacting with the OpenShift A
* `cluster_id`: Cluster ID (string, required)
* `host_id`: Host ID (string, required)

### Infrastructure Environment
### ISO Download URL

* **infraenv_info** - Get detailed information about the assisted installer infra env with the given ID. Contains data like ISO download URL and infra env metadata.
* `infraenv_id`: Infrastructure environment ID (string, required)
* **cluster_iso_download_url** - Get ISO download URL(s) for a cluster. Returns ISO download URLs separated by newlines if multiple exist.
* `cluster_id`: Cluster ID (string, required)

### Host Management

Expand Down
73 changes: 49 additions & 24 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,41 +200,64 @@ async def host_events(cluster_id: str, host_id: str) -> str:


@mcp.tool()
async def infraenv_info(infraenv_id: str) -> str:
async def cluster_iso_download_url(cluster_id: str) -> str:
"""
Get detailed information about an infrastructure environment (InfraEnv).

An InfraEnv contains the configuration and resources needed to boot and discover
hosts for cluster installation, including the discovery ISO image and network
configuration.
Get ISO download URL(s) for a cluster.

Args:
infraenv_id (str): The unique identifier of the infrastructure environment.
cluster_id (str): The unique identifier of the cluster.

Returns:
str: A formatted string containing comprehensive InfraEnv information including:
- ISO download URL for host discovery
- Network configuration and proxy settings
- SSH public key for host access
- Associated cluster information
- Static network configuration if applicable
str: ISO download URL(s) separated by newlines if multiple URLs exist.
"""
log.info("Retrieving InfraEnv information for infraenv_id: %s", infraenv_id)
log.info("Retrieving InfraEnv ISO URLs for cluster_id: %s", cluster_id)
client = InventoryClient(get_access_token())
result = await client.get_infra_env(infraenv_id)
log.info("Successfully retrieved InfraEnv information for %s", infraenv_id)
return result.to_str()
infra_envs = await client.list_infra_envs(cluster_id)

if not infra_envs:
log.info("No infrastructure environments found for cluster %s", cluster_id)
return "No ISO download URLs found for this cluster."

log.info(
"Found %d infrastructure environments for cluster %s",
len(infra_envs),
cluster_id,
)

# Extract ISO URLs from each infra env
iso_urls = []
for infra_env in infra_envs:
iso_url = infra_env.get("download_url")
infra_env_id = infra_env.get("id", "unknown")

if iso_url:
iso_urls.append(iso_url)
else:
log.warning(
"No ISO download URL found for infra env %s",
infra_env_id,
)

if not iso_urls:
log.info(
"No ISO download URLs found in infrastructure environments for cluster %s",
cluster_id,
)
return "No ISO download URLs found for this cluster."

log.info("Returning %d ISO URLs for cluster %s", len(iso_urls), cluster_id)
return "\n".join(iso_urls)


@mcp.tool()
async def create_cluster(
name: str, version: str, base_domain: str, single_node: bool
) -> str:
"""
Create a new OpenShift cluster and associated infrastructure environment.
Create a new OpenShift cluster.

Creates both a cluster definition and an InfraEnv for host discovery. The cluster
can be configured for high availability (multi-node) or single-node deployment.
Creates a cluster definition. The cluster can be configured for high availability
(multi-node) or single-node deployment.

Args:
name (str): The name for the new cluster. Must be unique within your account.
Expand All @@ -247,9 +270,7 @@ async def create_cluster(
production high-availability clusters with multiple control plane nodes.

Returns:
str: A JSON string containing the created cluster and InfraEnv IDs:
- cluster_id (str): The unique identifier of the created cluster
- infraenv_id (str): The unique identifier of the created InfraEnv
str: A the created cluster's id
"""
log.info(
"Creating cluster: name=%s, version=%s, base_domain=%s, single_node=%s",
Expand All @@ -262,6 +283,10 @@ async def create_cluster(
cluster = await client.create_cluster(
name, version, single_node, base_dns_domain=base_domain, tags="chatbot"
)
if cluster.id is None:
log.error("Failed to create cluster %s: cluster ID is unset", name)
return f"Failed to create cluster {name}: cluster ID is unset"

log.info("Successfully created cluster %s with ID: %s", name, cluster.id)
infraenv = await client.create_infra_env(
name, cluster_id=cluster.id, openshift_version=cluster.openshift_version
Expand All @@ -271,7 +296,7 @@ async def create_cluster(
cluster.id,
infraenv.id,
)
return json.dumps({"cluster_id": cluster.id, "infraenv_id": infraenv.id})
return cluster.id


@mcp.tool()
Expand Down
37 changes: 37 additions & 0 deletions service_client/assisted_service_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,43 @@ async def get_infra_env(self, infra_env_id: str) -> models.InfraEnv:
)
raise

async def list_infra_envs(self, cluster_id: str) -> list[dict[str, Any]]:
"""
List infrastructure environments for a specific cluster.

Args:
cluster_id: The unique identifier of the cluster.

Returns:
list[dict[str, Any]]: A list of infrastructure environment dictionaries for the cluster.
"""
try:
log.info("Listing infrastructure environments for cluster %s", cluster_id)
result = await asyncio.to_thread(
self._installer_api().list_infra_envs, cluster_id=cluster_id
)
log.info(
"Successfully listed infrastructure environments for cluster %s",
cluster_id,
)
return cast(list[dict[str, Any]], result)
except ApiException as e:
log.error(
"API error while listing infrastructure environments for cluster %s: Status: %s, Reason: %s, Body: %s",
cluster_id,
e.status,
e.reason,
e.body,
)
raise
except Exception as e:
log.error(
"Unexpected error while listing infrastructure environments for cluster %s: %s",
cluster_id,
str(e),
)
raise

async def create_cluster(
self, name: str, version: str, single_node: bool, **cluster_params: Any
) -> models.Cluster:
Expand Down
39 changes: 39 additions & 0 deletions tests/test_assisted_service_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,45 @@ async def test_get_infra_env_success(self, client: InventoryClient) -> None:
assert result == mock_infra_env
mock_api.get_infra_env.assert_called_once_with(infra_env_id=infra_env_id)

@pytest.mark.asyncio
async def test_list_infra_envs_success(self, client: InventoryClient) -> None:
"""Test successful infrastructure environments listing for a cluster."""
cluster_id = "test-cluster-id"
mock_infra_env1 = Mock(spec=models.InfraEnv)
mock_infra_env1.id = "infra-env-1"
mock_infra_env2 = Mock(spec=models.InfraEnv)
mock_infra_env2.id = "infra-env-2"
mock_infra_envs = [mock_infra_env1, mock_infra_env2]

with patch.object(client, "_installer_api") as mock_installer_api:
mock_api = Mock()
mock_api.list_infra_envs.return_value = mock_infra_envs
mock_installer_api.return_value = mock_api

result = await client.list_infra_envs(cluster_id)

assert result == mock_infra_envs
assert len(result) == 2
mock_api.list_infra_envs.assert_called_once_with(cluster_id=cluster_id)

@pytest.mark.asyncio
async def test_list_infra_envs_api_exception(self, client: InventoryClient) -> None:
"""Test infrastructure environments listing API exception handling."""
cluster_id = "test-cluster-id"

with patch.object(client, "_installer_api") as mock_installer_api:
mock_api = Mock()
mock_api.list_infra_envs.side_effect = ApiException(
status=404, reason="Not Found"
)
mock_installer_api.return_value = mock_api

with pytest.raises(ApiException) as exc_info:
await client.list_infra_envs(cluster_id)

assert exc_info.value.status == 404
assert exc_info.value.reason == "Not Found"

@pytest.mark.asyncio
async def test_create_cluster_success(self, client: InventoryClient) -> None:
"""Test successful cluster creation."""
Expand Down
92 changes: 78 additions & 14 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,24 +339,92 @@ async def test_host_events_success(
)

@pytest.mark.asyncio
async def test_infraenv_info_success(
async def test_cluster_iso_download_url_success(
self,
mock_inventory_client: Mock,
mock_get_access_token: None, # pylint: disable=unused-argument
) -> None:
"""Test successful infraenv_info function."""
infraenv_id = "test-infraenv-id"
mock_infraenv = Mock()
mock_infraenv.to_str.return_value = "infraenv-info-string"
mock_inventory_client.get_infra_env.return_value = mock_infraenv
"""Test successful cluster_iso_download_url function with single infraenv."""
cluster_id = "test-cluster-id"
mock_infraenv = {
"name": "test-infraenv",
"id": "test-infraenv-id",
"cluster_id": cluster_id,
"openshift_version": "4.18.2",
"download_url": "https://api.openshift.com/api/assisted-install/v2/infra-envs/test-id/downloads/image",
}
mock_inventory_client.list_infra_envs.return_value = [mock_infraenv]

with patch.object(
server, "InventoryClient", return_value=mock_inventory_client
):
result = await server.infraenv_info(infraenv_id)
result = await server.cluster_iso_download_url(cluster_id)

assert result == "infraenv-info-string"
mock_inventory_client.get_infra_env.assert_called_once_with(infraenv_id)
expected_result = "https://api.openshift.com/api/assisted-install/v2/infra-envs/test-id/downloads/image"
assert result == expected_result
mock_inventory_client.list_infra_envs.assert_called_once_with(cluster_id)

@pytest.mark.asyncio
async def test_cluster_iso_download_url_multiple_infraenvs(
self,
mock_inventory_client: Mock,
mock_get_access_token: None, # pylint: disable=unused-argument
) -> None:
"""Test successful cluster_iso_download_url function with multiple infraenvs."""
cluster_id = "test-cluster-id"

# First infraenv
mock_infraenv1 = {
"name": "test-infraenv-1",
"id": "test-infraenv-id-1",
"cluster_id": cluster_id,
"openshift_version": "4.18.2",
"download_url": "https://api.openshift.com/api/assisted-install/v2/infra-envs/test-id-1/downloads/image",
}

# Second infraenv with different characteristics
mock_infraenv2 = {
"name": "test-infraenv-2",
"id": "test-infraenv-id-2",
"cluster_id": cluster_id,
"openshift_version": "4.18.2",
"download_url": "https://api.openshift.com/api/assisted-install/v2/infra-envs/test-id-2/downloads/image",
}

mock_inventory_client.list_infra_envs.return_value = [
mock_infraenv1,
mock_infraenv2,
]

with patch.object(
server, "InventoryClient", return_value=mock_inventory_client
):
result = await server.cluster_iso_download_url(cluster_id)

expected_result = (
"https://api.openshift.com/api/assisted-install/v2/infra-envs/test-id-1/downloads/image\n"
"https://api.openshift.com/api/assisted-install/v2/infra-envs/test-id-2/downloads/image"
)
assert result == expected_result
mock_inventory_client.list_infra_envs.assert_called_once_with(cluster_id)

@pytest.mark.asyncio
async def test_cluster_iso_download_url_no_infraenvs(
self,
mock_inventory_client: Mock,
mock_get_access_token: None, # pylint: disable=unused-argument
) -> None:
"""Test cluster_iso_download_url function when no infraenvs are found."""
cluster_id = "test-cluster-id"
mock_inventory_client.list_infra_envs.return_value = []

with patch.object(
server, "InventoryClient", return_value=mock_inventory_client
):
result = await server.cluster_iso_download_url(cluster_id)

assert result == "No ISO download URLs found for this cluster."
mock_inventory_client.list_infra_envs.assert_called_once_with(cluster_id)

@pytest.mark.asyncio
async def test_create_cluster_success(
Expand Down Expand Up @@ -385,11 +453,7 @@ async def test_create_cluster_success(
result = await server.create_cluster(
name, version, base_domain, single_node
)

expected_result = json.dumps(
{"cluster_id": "cluster-id", "infraenv_id": "infraenv-id"}
)
assert result == expected_result
assert result == mock_cluster.id

mock_inventory_client.create_cluster.assert_called_once_with(
name, version, single_node, base_dns_domain=base_domain, tags="chatbot"
Expand Down