diff --git a/README.md b/README.md index 4ca38fd..8376155 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/server.py b/server.py index efccd48..053c40d 100644 --- a/server.py +++ b/server.py @@ -200,30 +200,53 @@ 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() @@ -231,10 +254,10 @@ 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. @@ -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", @@ -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 @@ -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() diff --git a/service_client/assisted_service_api.py b/service_client/assisted_service_api.py index 6cf682e..4c56f90 100644 --- a/service_client/assisted_service_api.py +++ b/service_client/assisted_service_api.py @@ -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: diff --git a/tests/test_assisted_service_api.py b/tests/test_assisted_service_api.py index 7c55972..af549a2 100644 --- a/tests/test_assisted_service_api.py +++ b/tests/test_assisted_service_api.py @@ -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.""" diff --git a/tests/test_server.py b/tests/test_server.py index c61d6cb..07a6e38 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -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( @@ -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"