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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ jupyter labextension list

When installing from a published wheel, Node.js and `jlpm` are not required.

## 🔐 Access Tokens on EGI Notebooks

When APRICOTLab runs inside EGI Notebooks, the deployment wizard tries to obtain an EGI access token automatically and fills the access token field for you.

You can still edit that field manually before deploying. This is useful when a target site does not support the automatically generated token or requires a different token for that specific site.

## ✨ IPython Magics for Infrastructure Management

The extension provides a set of custom IPython magic commands for interacting with deployed infrastructures:
Expand Down Expand Up @@ -147,7 +153,7 @@ cd apricotlab
pip install -e .

# Link the extension to JupyterLab
jupyter labextension develop . --overwrite
jupyter-builder develop . --overwrite

# Build the extension
jlpm build
Expand Down Expand Up @@ -178,7 +184,7 @@ jupyter lab build --minimize=False
pip uninstall apricot
```

You should also remove the symlink created with `jupyter labextension develop`. Run:
You should also remove the symlink created with `jupyter-builder develop`. Run:

```bash
jupyter labextension list
Expand Down
190 changes: 93 additions & 97 deletions apricot_magics/apricot_magics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import os
import json
import sys
import shutil
import re

IM_ENDPOINT = "https://im.egi.eu/im"

Expand All @@ -22,38 +24,53 @@ def __init__(self, shell):
self.load_paths()

data = self.load_json(self.inf_list_path)
access_token = data.get("access_token")
access_token = data.get("access_token", "")
refresh_token = data.get("refresh_token", "")
self.client = None

if access_token != "":
if access_token:
auth = f"""
type = InfrastructureManager; token = {access_token}
"""
else:
refresh_token = data["refresh_token"]
self.client = IMClient.init_client(IM_ENDPOINT, auth)
elif refresh_token:
self.generate_new_access_token(refresh_token)

self.client = IMClient.init_client(IM_ENDPOINT, auth)
self.initialize_im_client()

########################
# Auxiliar functions #
########################

def load_paths(self):
# Get the absolute path to the current file (apricot_magics.py)
current_dir = Path(__file__).parent
state_dir = Path.home() / "apricotlab_state"
state_dir.mkdir(exist_ok=True)

for legacy_dir in (
Path.cwd() / "apricotlab_state",
Path.cwd().parent / "apricotlab_state",
):
if legacy_dir.exists() and legacy_dir.resolve() != state_dir.resolve():
for legacy_file in legacy_dir.iterdir():
target = state_dir / legacy_file.name
if legacy_file.is_file() and not target.exists():
shutil.copy2(legacy_file, target)

self.inf_list_path = state_dir / "infrastructuresList.json"
self.deployed_template_path = state_dir / "deployed-template.yaml"
self.authfile_path = state_dir / "authfile"

# Construct the path to the 'resources' folder relative to 'apricot_magics/'
resources_dir = current_dir.parent / "resources"

self.inf_list_path = resources_dir / "infrastructuresList.json"
self.deployed_template_path = resources_dir / "deployed-template.yaml"
self.authfile_path = resources_dir / "authfile"

# Check if the files exist
if not self.inf_list_path.exists():
raise FileNotFoundError(f"File not found: {self.inf_list_path}")
self.inf_list_path.write_text(
json.dumps({"refresh_token": "", "infrastructures": []}, indent=4)
)

if not self.deployed_template_path.exists():
raise FileNotFoundError(f"File not found: {self.deployed_template_path}")
self.deployed_template_path.touch()

if not self.authfile_path.exists():
self.authfile_path.write_text(
"id = im; type = InfrastructureManager; token = <token>\n"
)

def load_json(self, path):
"""Load a JSON file and handle errors."""
Expand Down Expand Up @@ -422,101 +439,80 @@ def apricot_ls(self, line):
)
)

@line_magic
def apricot_info(self, line):
if not line:
return "Usage: `%apricot_info infrastructure-id`\n"

inf_id = line.split()[0]

def get_raw_infrastructure_info(self, inf_id):
try:
self.initialize_im_client()
success, inf_info = self.client.getinfo(inf_id)
return list(inf_info)

except Exception as e:
print(f"Error: {e}")
return None

@line_magic
def apricot_radl(self, line):
if not line:
return "Usage: `%apricot_radl infrastructure-id`\n"

inf_id = line.split()[0]
inf_info_items = self.get_raw_infrastructure_info(inf_id)

if inf_info_items is None:
return "Failed"

for item in inf_info:
for item in inf_info_items:
print(*item, sep="\n")

# @line_magic
# def apricot_vmls(self, line):
@line_magic
def apricot_info(self, line):
if not line:
print("Usage: `%apricot_vmls infrastructure-id`\n")
return "Fail"
return "Usage: `%apricot_info infrastructure-id`\n"

inf_id = line.split()[0]
inf_info_items = self.get_raw_infrastructure_info(inf_id)

if inf_info_items is None:
return "Failed"

vm_info_list = []

try:
self.initialize_im_client()
success, inf_info = self.client.getinfo(inf_id)
def extract_property(output, names, operators=("=", ">=")):
for name in names:
for operator in operators:
pattern = rf"{re.escape(name)}\s*{re.escape(operator)}\s*'([^']*)'"
match = re.search(pattern, output)
if match:
return match.group(1).split()[0]

except Exception as e:
print(f"Error: {e}")
return "Failed"
pattern = rf"{re.escape(name)}\s*{re.escape(operator)}\s*([^\s\n]+)"
match = re.search(pattern, output)
if match:
return match.group(1).strip("'")

return "N/A"

for item in inf_info:
for item in inf_info_items:
vm_id = item[0]
(
net_interface_ip,
provider_type,
disk_size,
cpu_count,
memory_size,
gpu_count,
) = (None, None, None, None, None, None)

output_string = item[
2
] # The third element contains the VM details as a string
for line in output_string.split("\n"):
if "net_interface.0.ip =" in line:
net_interface_ip = (
line.split("= ")[1].strip().replace("'", "").split(" ")[0]
)
if "provider.type =" in line:
provider_type = (
line.split("= ")[1].strip().replace("'", "").split(" ")[0]
)
if "disk.0.size >=" in line:
disk_size = line.split(">= ")[1].strip().strip("'").split(" ")[0]
if "cpu.count =" in line:
cpu_count = line.split("= ")[1].strip().strip("'").split(" ")[0]
if "memory.size =" in line:
memory_size = line.split("= ")[1].strip().strip("'").split(" ")[0]
if "gpu.count >=" in line:
gpu_count = line.split(">= ")[1].strip().strip("'").split(" ")[0]

start_time = time.time()
while not all(
(
output_string = item[2] if len(item) > 2 else ""

vm_info_list.append(
[
vm_id,
net_interface_ip,
provider_type,
disk_size,
cpu_count,
memory_size,
memory_size,
)
): # Ensure valid values
if time.time() - start_time > 4:
break
# time.sleep(1)

# if all((vm_id, net_interface_ip, provider_type, disk_size, cpu_count, memory_size, gpu_count)):
vm_info_list.append(
[
vm_id,
net_interface_ip,
provider_type,
disk_size,
cpu_count,
memory_size,
gpu_count,
]
)
extract_property(
output_string,
["net_interface.1.ip", "net_interface.0.ip", "node_ip"],
),
extract_property(output_string, ["provider.type"]),
extract_property(output_string, ["disk.0.size"]),
extract_property(output_string, ["cpu.count"]),
extract_property(output_string, ["memory.size"]),
extract_property(output_string, ["gpu.count"]),
]
)

if not vm_info_list:
print("No VM information found.")
return

# Print table
print(
Expand Down Expand Up @@ -644,7 +640,7 @@ def apricot(self, code, cell=None):
for line in lines:
if len(line) > 0:
result = self.apricot(line.strip())
if result != "Done":
if result not in ("Done", None):
print("Execution stopped")
return f"Fail on line: '{line.strip()}'"
return "Done"
Expand Down Expand Up @@ -709,7 +705,7 @@ def apricot(self, code, cell=None):

self.cleanup_files("key.pem")

return "Done"
return None

elif word1 == "list":
return self.apricot_ls("")
Expand Down Expand Up @@ -738,4 +734,4 @@ def load_ipython_extension(ipython):
can be loaded via `%load_ext module.path` or be configured to be
autoloaded by IPython at startup time.
"""
ipython.register_magics(Apricot_Magics)
ipython.register_magics(Apricot_Magics)
26 changes: 23 additions & 3 deletions apricot_tutorial.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"source": [
"## 🔐 Authorization File\n",
"\n",
"Before diving in, you’ll need an **authorization file** to authenticate with cloud providers. This file is located at `resources/authfile`: \n",
"Before diving in, you’ll need an **authorization file** to authenticate with cloud providers. This file is located at `apricotlab_state/authfile`: \n",
"\n",
"- 👉 It's **automatically filled** if you are using predefined recipes used in **Deployment Menu**.\n",
"\n",
Expand Down Expand Up @@ -143,7 +143,7 @@
"id": "f3954ffd",
"metadata": {},
"source": [
"💡 **Tip**: If you've already saved this token before, (so it is saved in `resources/infrastructuresList.json`) — you can run the command without the variable. "
"💡 **Tip**: If you've already saved this token before, (so it is saved in `apricotlab_state/infrastructuresList.json`) — you can run the command without the variable. "
]
},
{
Expand Down Expand Up @@ -348,7 +348,9 @@
"metadata": {},
"source": [
"## 🧠 Get Information About the Infrastructure Nodes\n",
"Explore detailed information about your infrastructure nodes:"
"Explore detailed information about your infrastructure nodes.\n",
"\n",
"Use `%apricot_info` to show a compact table with the VM ID, public IP, provider, disk, CPU, memory and GPU values.\n"
]
},
{
Expand All @@ -365,6 +367,24 @@
"%apricot_info $infrastructure_id"
]
},
{
"cell_type": "markdown",
"id": "2678f0d8",
"metadata": {},
"source": [
"If you need to inspect the raw infrastructure description returned by the IM, use `%apricot_radl`. This prints the RADL/raw information directly, which is useful for debugging or checking provider-specific fields.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a36d8b8c",
"metadata": {},
"outputs": [],
"source": [
"%apricot_radl $infrastructure_id"
]
},
{
"cell_type": "markdown",
"id": "2e80eb5f",
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"files": [
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
"src/**/*.{ts,tsx}"
"src/**/*.{ts,tsx}",
"resources/**/*"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand All @@ -31,8 +32,9 @@
"scripts": {
"build": "jlpm build:lib && jlpm build:labextension:dev",
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
"build:labextension": "jupyter labextension build .",
"build:labextension:dev": "jupyter labextension build --development True .",
"build:labextension": "jupyter labextension build . && jlpm copy:resources",
"build:labextension:dev": "jupyter labextension build --development True . && jlpm copy:resources",
"copy:resources": "node -e \"const fs=require('fs');fs.rmSync('apricot/labextension/resources',{recursive:true,force:true});fs.cpSync('resources','apricot/labextension/resources',{recursive:true});\"",
"build:lib": "tsc --sourceMap",
"build:lib:prod": "tsc",
"clean": "jlpm clean:lib",
Expand Down
1 change: 0 additions & 1 deletion resources/authfile

This file was deleted.

Empty file removed resources/deployed-template.yaml
Empty file.
4 changes: 0 additions & 4 deletions resources/infrastructuresList.json

This file was deleted.

2 changes: 1 addition & 1 deletion slurm-experiment.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"- Use the **custom recipe** below in **TOSCA** format:\n",
"> 🔍 Change both _image_ values with the valid cloud provider image you want to use.\n",
"\n",
"> ✍️ You will need to fill your authfile in `resources/authfile` with your IM and cloud credentials if you use the *magic commands* to deploy the cluster."
"> ✍️ You will need to fill your authfile in `apricotlab_state/authfile` with your IM and cloud credentials if you use the *magic commands* to deploy the cluster."
]
},
{
Expand Down
Loading
Loading