diff --git a/demo-notebooks/guided-demos/ipywidgets.ipynb b/demo-notebooks/guided-demos/ipywidgets.ipynb new file mode 100644 index 00000000..cd0fa1e5 --- /dev/null +++ b/demo-notebooks/guided-demos/ipywidgets.ipynb @@ -0,0 +1,392 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8d4a42f6", + "metadata": {}, + "source": [ + "In this notebook, we will go through the basics of using the SDK to:\n", + " - Spin up a Ray cluster with our desired resources\n", + " - View the status and specs of our Ray cluster\n", + " - Take down the Ray cluster when finished" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "301094f1", + "metadata": {}, + "outputs": [], + "source": [ + "%pip uninstall codeflare-sdk -y\n", + "%pip install ../../dist/codeflare_sdk-0.0.0.dev0-py3-none-any.whl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b55bc3ea-4ce3-49bf-bb1f-e209de8ca47a", + "metadata": {}, + "outputs": [], + "source": [ + "# Import pieces from codeflare-sdk\n", + "from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication, list_cluster_details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "614daa0c", + "metadata": {}, + "outputs": [], + "source": [ + "# Create authentication object for user permissions\n", + "# IF unused, SDK will automatically check for default kubeconfig, then in-cluster config\n", + "# KubeConfigFileAuthentication can also be used to specify kubeconfig path manually\n", + "auth = TokenAuthentication(\n", + " token = \"XXXXX\",\n", + " server = \"XXXXX\",\n", + " skip_tls=False\n", + ")\n", + "auth.login()" + ] + }, + { + "cell_type": "markdown", + "id": "bc27f84c", + "metadata": {}, + "source": [ + "Here, we want to define our cluster by specifying the resources we require for our batch workload. Below, we define our cluster object (which generates a corresponding RayCluster).\n", + "\n", + "NOTE: We must specify the `image` which will be used in our RayCluster, we recommend you bring your own image which suits your purposes. \n", + "The example here is a community image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f4bc870-091f-4e11-9642-cba145710159", + "metadata": {}, + "outputs": [], + "source": [ + "# Create and configure our cluster object\n", + "# The SDK will try to find the name of your default local queue based on the annotation \"kueue.x-k8s.io/default-queue\": \"true\" unless you specify the local queue manually below\n", + "cluster = Cluster(ClusterConfiguration(\n", + " name='raytest1', \n", + " namespace='default', # Update to your namespace\n", + " head_gpus=0, # For GPU enabled workloads set the head_gpus and num_gpus\n", + " num_gpus=0,\n", + " num_workers=1,\n", + " min_cpus=1,\n", + " max_cpus=1,\n", + " min_memory=2,\n", + " max_memory=2,\n", + " image=\"quay.io/rhoai/ray:2.23.0-py39-cu121\",\n", + " write_to_file=False, # When enabled Ray Cluster yaml files are written to /HOME/.codeflare/resources \n", + " # local_queue=\"local-queue-name\" # Specify the local queue manually\n", + "))" + ] + }, + { + "cell_type": "markdown", + "id": "12eef53c", + "metadata": {}, + "source": [ + "Next, we want to bring our cluster up, so we call the `up()` function below to submit our Ray Cluster onto the queue, and begin the process of obtaining our resource cluster." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0884bbc-c224-4ca0-98a0-02dfa09c2200", + "metadata": {}, + "outputs": [], + "source": [ + "# Bring up the cluster\n", + "cluster.up()" + ] + }, + { + "cell_type": "markdown", + "id": "657ebdfb", + "metadata": {}, + "source": [ + "Now, we want to check on the status of our resource cluster, and wait until it is finally ready for use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df71c1ed", + "metadata": {}, + "outputs": [], + "source": [ + "def format_status(status):\n", + " if status == \"Ready\":\n", + " return 'Ready ✓'\n", + " elif status == \"Suspended\":\n", + " return 'Suspended ~'\n", + " elif status == \"Starting\":\n", + " return 'Starting ⌛'\n", + " elif status == \"Failed\":\n", + " return 'Failed ✗'\n", + " else:\n", + " return status\n", + "\n", + "import ipywidgets as widgets\n", + "import pandas as pd\n", + "from IPython.display import display, HTML\n", + "data = {\n", + " \"name\": [\"RayTest1\", \"RayTest2\", \"RayTest3\", \"RayTest4\"],\n", + " \"namespace\": [\"default\", \"usernamespace\", \"usernamespace\", \"usernamespace\"],\n", + " \"head_gpu\": [0, 1, 2, 0],\n", + " \"worker_gpu\": [2, 0, 1, 0],\n", + " \"min_memory\": [2, 4, 4, 2],\n", + " \"max_memory\": [2, 4, 8, 4],\n", + " \"min_cpu\": [1, 2, 4, 2],\n", + " \"max_cpu\": [1, 4, 8, 2],\n", + " \"status\": [\"Ready\", \"Starting\", \"Suspended\", \"Failed\"]\n", + "}\n", + "df = pd.DataFrame(data)\n", + "\n", + "# format to add icons\n", + "df['status'] = df['status'].apply(format_status)\n", + "\n", + "my_output = widgets.Output()\n", + "my_output\n", + "classification_widget = widgets.ToggleButtons(\n", + " options=['RayTest1', \"RayTest2\", \"RayTest3\", \"RayTest4\"],\n", + " description='Select an existing cluster:',\n", + ")\n", + "\n", + "def on_click(change):\n", + " new_value = change[\"new\"]\n", + " my_output.clear_output()\n", + " with my_output:\n", + " display(HTML(df[df[\"name\"]==new_value][[\"name\", \"namespace\", \"head_gpu\", \"worker_gpu\", \"min_memory\", \"max_memory\", \"min_cpu\", \"max_cpu\", \"status\"]].to_html(escape=False, index=False, border=2)))\n", + "\n", + "classification_widget.observe(on_click, names=\"value\")\n", + "display(widgets.VBox([classification_widget, my_output]))\n", + "\n", + "\n", + "list_jobs_button = widgets.Button(\n", + " description='View Jobs',\n", + " icon='suitcase'\n", + " )\n", + "delete_button = widgets.Button(\n", + " description='Delete Cluster',\n", + " icon='trash'\n", + " )\n", + "ray_dashboard_button = widgets.Button(\n", + " description='Open Ray Dashboard',\n", + " icon='dashboard',\n", + " layout=widgets.Layout(width='auto'),\n", + " )\n", + "view_yaml_button = widgets.Button(\n", + " description='View YAML',\n", + " icon='file'\n", + " )\n", + "display(widgets.HBox([delete_button, list_jobs_button, view_yaml_button, ray_dashboard_button]))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24e612ff", + "metadata": {}, + "outputs": [], + "source": [ + "list_cluster_details()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9dda874b", + "metadata": {}, + "outputs": [], + "source": [ + "def format_status(status):\n", + " if status == \"Ready\":\n", + " return 'Ready ✓'\n", + " elif status == \"Suspended\":\n", + " return 'Suspended ~'\n", + " elif status == \"Starting\":\n", + " return 'Starting ⌛'\n", + " elif status == \"Failed\":\n", + " return 'Failed ✗'\n", + " else:\n", + " return status\n", + "\n", + "import ipywidgets as widgets\n", + "import pandas as pd\n", + "from IPython.display import display, HTML\n", + "data = {\n", + " \"name\": [\"RayTest1\", \"RayTest2\", \"RayTest3\", \"RayTest4\"],\n", + " \"namespace\": [\"default\", \"usernamespace\", \"usernamespace\", \"usernamespace\"],\n", + " \"head_gpu\": [0, 1, 2, 0],\n", + " \"worker_gpu\": [2, 0, 1, 0],\n", + " \"min_memory\": [2, 4, 4, 2],\n", + " \"max_memory\": [2, 4, 8, 4],\n", + " \"min_cpu\": [1, 2, 4, 2],\n", + " \"max_cpu\": [1, 4, 8, 2],\n", + " \"status\": [\"Ready\", \"Starting\", \"Suspended\", \"Failed\"],\n", + " \"pods\": [\n", + " [{\"pod\": \"head\", \"name\": \"head-raytest1\", \"status\": \"Ready\"}, {\"pod\": \"worker\", \"name\": \"worker-raytest1-a\", \"status\": \"Ready\"}, {\"pod\": \"worker\", \"name\": \"worker-raytest1-b\", \"status\": \"Ready\"}],\n", + " [{\"pod\": \"head\", \"name\": \"head-raytest2\", \"status\": \"Ready\"}, {\"pod\": \"worker\", \"name\": \"worker-raytest2a\", \"status\": \"Starting\"}],\n", + " [{\"pod\": \"head\", \"name\": \"head-raytest3\", \"status\": \"Suspended\"}, {\"pod\": \"worker\", \"name\": \"worker-raytest3a\", \"status\": \"Suspended\"}],\n", + " [{\"pod\": \"head\", \"name\": \"head-raytest4\", \"status\": \"Failed\"}, {\"pod\": \"worker\", \"name\": \"worker-raytest4a\", \"status\": \"Failed\"}]\n", + " ]\n", + "}\n", + "df = pd.DataFrame(data)\n", + "\n", + "# format to add icons\n", + "df['status'] = df['status'].apply(format_status)\n", + "\n", + "my_output = widgets.Output()\n", + "my_output\n", + "classification_widget = widgets.ToggleButtons(\n", + " options=['RayTest1', \"RayTest2\", \"RayTest3\", \"RayTest4\"],\n", + " description='Select an existing cluster:',\n", + ")\n", + "\n", + "def on_click(change):\n", + " new_value = change[\"new\"]\n", + " my_output.clear_output()\n", + " with my_output:\n", + " selected_data = df[df[\"name\"] == new_value]\n", + " main_table = selected_data[[\"name\", \"namespace\", \"head_gpu\", \"worker_gpu\", \"min_memory\", \"max_memory\", \"min_cpu\", \"max_cpu\", \"status\"]].to_html(escape=False, index=False)\n", + " pod_rows = \"\"\n", + " for pod in selected_data[\"pods\"].values[0]:\n", + " pod_rows += f'{pod[\"pod\"]}{pod[\"name\"]}{format_status(pod[\"status\"])}'\n", + " pods_table = f'
{pod_rows}
PodNameStatus
'\n", + " display(HTML(f'
{main_table}{pods_table}
'))\n", + "\n", + "classification_widget.observe(on_click, names=\"value\")\n", + "display(widgets.VBox([classification_widget, my_output]))\n", + "\n", + "\n", + "list_jobs_button = widgets.Button(\n", + " description='View Jobs',\n", + " icon='suitcase'\n", + " )\n", + "delete_button = widgets.Button(\n", + " description='Delete Cluster',\n", + " icon='trash'\n", + " )\n", + "ray_dashboard_button = widgets.Button(\n", + " description='Open Ray Dashboard',\n", + " icon='dashboard',\n", + " layout=widgets.Layout(width='auto'),\n", + " )\n", + "view_yaml_button = widgets.Button(\n", + " description='View YAML',\n", + " icon='file'\n", + " )\n", + "display(widgets.HBox([delete_button, list_jobs_button, view_yaml_button, ray_dashboard_button]))" + ] + }, + { + "cell_type": "markdown", + "id": "b3a55fe4", + "metadata": {}, + "source": [ + "Let's quickly verify that the specs of the cluster are as expected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f1ab7ff", + "metadata": {}, + "outputs": [], + "source": [ + "with my_output:\n", + " display(cluster.details())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fd45bc5-03c0-4ae5-9ec5-dd1c30f1a084", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import HTML, display\n", + "import ipywidgets as widgets\n", + "\n", + "def on_click(change):\n", + " new_value = change[\"new\"]\n", + " my_output.clear_output()\n", + " with my_output:\n", + " display(HTML(f'
{df[df[\"name\"]==new_value][[\"name\", \"namespace\", \"head_gpu\", \"worker_gpu\", \"min_memory\", \"max_memory\", \"min_cpu\", \"max_cpu\", \"status\"]].to_html(escape=False, index=False)}
'))\n", + "\n", + "classification_widget.observe(on_click, names=\"value\")\n", + "display(widgets.VBox([classification_widget, my_output], layout=widgets.Layout(border='2px solid black')))\n", + "\n", + "list_jobs_button = widgets.Button(description='View Jobs', icon='suitcase')\n", + "delete_button = widgets.Button(description='Delete Cluster', icon='trash')\n", + "ray_dashboard_button = widgets.Button(description='Open Ray Dashboard', icon='dashboard', layout=widgets.Layout(width='auto'))\n", + "view_yaml_button = widgets.Button(description='View YAML', icon='file')\n", + "buttons_container = widgets.HBox([delete_button, list_jobs_button, view_yaml_button, ray_dashboard_button], layout=widgets.Layout(border='2px solid black'))\n", + "\n", + "display(buttons_container)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5af8cd32", + "metadata": {}, + "source": [ + "Finally, we bring our resource cluster down and release/terminate the associated resources, bringing everything back to the way it was before our cluster was brought up." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f36db0f-31f6-4373-9503-dc3c1c4c3f57", + "metadata": {}, + "outputs": [], + "source": [ + "cluster.down()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d41b90e", + "metadata": {}, + "outputs": [], + "source": [ + "auth.logout()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + }, + "vscode": { + "interpreter": { + "hash": "f9f85f796d01129d0dd105a088854619f454435301f6ffec2fea96ecbd9be4ac" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/codeflare_sdk/__init__.py b/src/codeflare_sdk/__init__.py index 0390a3d2..5002ee0c 100644 --- a/src/codeflare_sdk/__init__.py +++ b/src/codeflare_sdk/__init__.py @@ -14,6 +14,8 @@ get_cluster, list_all_queued, list_all_clusters, + display_cluster_radios, + list_cluster_details, ) from .job import RayJobClient diff --git a/src/codeflare_sdk/cluster/__init__.py b/src/codeflare_sdk/cluster/__init__.py index 0b1849e5..84bee330 100644 --- a/src/codeflare_sdk/cluster/__init__.py +++ b/src/codeflare_sdk/cluster/__init__.py @@ -19,6 +19,8 @@ get_cluster, list_all_queued, list_all_clusters, + display_cluster_radios, + list_cluster_details, ) from .awload import AWManager diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index f0f50eb3..bf82f7fb 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -67,11 +67,27 @@ def __init__(self, config: ClusterConfiguration): based off of the configured resources to represent the desired cluster request. """ + self.cluster_up_down_buttons() self.config = config self.app_wrapper_yaml = self.create_app_wrapper() self._job_submission_client = None self.app_wrapper_name = self.config.name + import ipywidgets as widgets + from IPython.display import display, clear_output + def cluster_up_down_buttons(self): + delete_button = widgets.Button( + description='Cluster Down', + icon='trash', + ) + up_button = widgets.Button( + description='Cluster Up', + icon='play', + ) + # Display the buttons in an HBox + display(widgets.HBox([delete_button, up_button])) + + @property def _client_headers(self): k8_client = api_config_handler() or client.ApiClient() @@ -582,6 +598,117 @@ def get_current_namespace(): # pragma: no cover except KeyError: return None +import ipywidgets as widgets +from IPython.display import display, clear_output + + +def format_status(status): + if status == "Ready": + return 'Ready ✓' + elif status == "Suspended": + return 'Suspended ~' + elif status == "Starting": + return 'Starting ⌛' + elif status == "Failed": + return 'Failed ✗' + else: + return status + +def list_cluster_details(): + import ipywidgets as widgets + import pandas as pd + from IPython.display import display, HTML + data = { + "name": ["RayTest1", "RayTest2", "RayTest3", "RayTest4"], + "namespace": ["default", "usernamespace", "usernamespace", "usernamespace"], + "head_gpu": [0, 1, 2, 0], + "worker_gpu": [2, 0, 1, 0], + "min_memory": [2, 4, 4, 2], + "max_memory": [2, 4, 8, 4], + "min_cpu": [1, 2, 4, 2], + "max_cpu": [1, 4, 8, 2], + "status": ["Ready", "Starting", "Suspended", "Failed"] + } + df = pd.DataFrame(data) + + # format to add icons + df['status'] = df['status'].apply(format_status) + + my_output = widgets.Output() + my_output + classification_widget = widgets.ToggleButtons( + options=['RayTest1', "RayTest2", "RayTest3", "RayTest4"], + description='Select an existing cluster:', + ) + + def on_click(change): + new_value = change["new"] + my_output.clear_output() + with my_output: + display(HTML(df[df["name"]==new_value][["name", "namespace", "head_gpu", "worker_gpu", "min_memory", "max_memory", "min_cpu", "max_cpu", "status"]].to_html(escape=False, index=False, border=2))) + + classification_widget.observe(on_click, names="value") + display(widgets.VBox([classification_widget, my_output])) + + + list_jobs_button = widgets.Button( + description='View Jobs', + icon='suitcase' + ) + delete_button = widgets.Button( + description='Delete Cluster', + icon='trash' + ) + ray_dashboard_button = widgets.Button( + description='Open Ray Dashboard', + icon='dashboard', + layout=widgets.Layout(width='auto'), + ) + view_yaml_button = widgets.Button( + description='View YAML', + icon='file' + ) + display(widgets.HBox([delete_button, list_jobs_button, view_yaml_button, ray_dashboard_button])) + + +def display_cluster_radios(): + create_cluster_widgets() + cluster_radios, delete_buttons = create_cluster_widgets() + radio_buttons = widgets.RadioButtons( + options=cluster_radios, + description='Clusters:', + disabled=False + ) + display(radio_buttons) + for button in delete_buttons: + display(button) + +def create_cluster_widgets(): + clusters = {"name": "ray1", "namespace": "default"}, {"name": "ray2", "namespace": "default"} + cluster_radios = [] + delete_buttons = [] + + # Function to handle delete button click + def on_delete_clicked(b): + index = int(b.tooltip) + del clusters[index] + clear_output() + display_cluster_radios() + + for index, cluster in enumerate(clusters): + delete_button = widgets.Button( + description='Delete', + tooltip=str(index), + icon='trash' + ) + delete_button.on_click(on_delete_clicked) + cluster_radios.append( + (f'{cluster["name"]} ({cluster["namespace"]})', cluster["name"]) + ) + delete_buttons.append(delete_button) + + return cluster_radios, delete_buttons + def get_cluster( cluster_name: str,