Skip to content

Commit

Permalink
✨ feat: implements update and delete for virtual devices (#326)
Browse files Browse the repository at this point in the history
This commit improves the support for onboarding HWIL devices via device manifests. We implement the update_object and delete_object such that HWIL devices are re-onboarded or deleted via the rio apply and rio delete commands.

Take an example where the rapyuta.io device is present but the corresponding HWIL device pod was evicted, we will have to re-onboard it. On re-apply the manifest, the update_object handler will take care of running the onboarding script against on the said device. If in case the device was deleted as a part of some cleanup, then re-applying will ensure that the device is re-created and onboarded.

Similarly, rio delete device.yaml will ensure that both rapyuta.io and the HWIL device are cleaned up.

Wrike Ticket: https://www.wrike.com/open.htm?id=1425685033
  • Loading branch information
pallabpain authored Jul 2, 2024
1 parent dc9d26d commit 8a06bff
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 57 deletions.
68 changes: 49 additions & 19 deletions riocli/device/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@

from riocli.device.util import (
create_hwil_device,
delete_hwil_device,
execute_onboard_command,
find_device_guid,
update_device_labels
make_device_labels_from_hwil_device
)
from riocli.exceptions import DeviceNotFound
from riocli.jsonschema.validate import load_schema
from riocli.model import Model

Expand All @@ -32,10 +32,6 @@ def __init__(self, *args, **kwargs):
self.update(*args, **kwargs)

def find_object(self, client: Client) -> bool:
# For virtual devices, always return False to ensure re-onboarding is done even if the device already exists
if self.spec.get('virtual', {}).get('enabled', False):
return False

guid, obj = self.rc.find_depends({
"kind": "device",
"nameOrGUID": self.metadata.name,
Expand All @@ -51,29 +47,63 @@ def create_object(self, client: Client, **kwargs) -> v1Device:
device = client.create_device(self.to_v1())
return device

# Create the HWIL device.
hwil_response = create_hwil_device(self.spec, self.metadata)
try:
device_uuid = find_device_guid(client, self.metadata.name)
device = client.get_device(device_uuid)
except DeviceNotFound:
update_device_labels(self.metadata, hwil_response)
device = client.create_device(self.to_v1())
except Exception as e:
raise e

# Generate labels to store HWIL metadata in rapyuta.io device.
l = make_device_labels_from_hwil_device(hwil_response)
self.metadata.get('labels', {}).update(l)

# Create the rapyuta.io device.
device = client.create_device(self.to_v1())

# On-board the HWIL device using the onboard script.
onboard_script = device.onboard_script()
onboard_command = onboard_script.full_command()
try:
execute_onboard_command(hwil_response.id, onboard_command)
except Exception as e:
raise e
execute_onboard_command(hwil_response.id, onboard_command)

return device

def update_object(self, client: Client, obj: typing.Any) -> typing.Any:
pass
if not self.spec.get('virtual', {}).get('enabled', False):
return obj

device_uuid = find_device_guid(client, self.metadata.name)
device = client.get_device(device_uuid)

# Nothing to do if the device is already online or initializing.
if device['status'] in ('ONLINE', 'INITIALIZING'):
return device

device_labels = device.get('labels', {})
# Convert list to dict for easy access.
device_labels = {l['key']: l for l in device_labels}

# Otherwise, re-onboard the device.
hwil_response = create_hwil_device(self.spec, self.metadata)
labels = make_device_labels_from_hwil_device(hwil_response)

# Add or update labels in the device.
for k, v in labels.items():
if k in device_labels:
device_labels[k]['value'] = v
device.update_label(device_labels[k])
continue

device.add_label(k, v)

onboard_script = device.onboard_script()
onboard_command = onboard_script.full_command()
execute_onboard_command(hwil_response.id, onboard_command)

return device

def delete_object(self, client: Client, obj: typing.Any) -> typing.Any:
if self.spec.get('virtual', {}).get('enabled', False):
device_uuid = find_device_guid(client, self.metadata.name)
device = client.get_device(device_uuid)
delete_hwil_device(device)

obj.delete()

def to_v1(self) -> v1Device:
Expand Down
76 changes: 51 additions & 25 deletions riocli/device/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from riocli.constants import Colors
from riocli.exceptions import DeviceNotFound
from riocli.hwil.util import execute_command, find_device_id
from riocli.utils import is_valid_uuid, trim_suffix, trim_prefix
from riocli.utils import is_valid_uuid, trim_prefix, trim_suffix


def name_to_guid(f: typing.Callable) -> typing.Callable:
Expand Down Expand Up @@ -178,29 +178,58 @@ def is_remote_path(src, devices=[]):

def create_hwil_device(spec: dict, metadata: dict) -> Munch:
"""Create a new hardware-in-the-loop device."""
os = spec['virtual']['os']
codename = spec['virtual']['codename']
arch = spec['virtual']['arch']
labels = hwil_device_labels(spec.virtual.product, metadata.name)
device_name = f"{metadata['name']}-{spec['virtual']['product']}-{labels['user']}"
device_name = sanitize_hwil_device_name(device_name)
virtual = spec['virtual']
os = virtual['os']
codename = virtual['codename']
arch = virtual['arch']
product = virtual['product']
name = metadata['name']

labels = hwil_device_labels(product, name)
device_name = sanitize_hwil_device_name(f"{name}-{product}-{labels['user']}")

client = new_hwil_client()

try:
device_id = find_device_id(client, device_name)
return client.get_device(device_id)
except DeviceNotFound:
pass
pass # Do nothing and proceed.

try:
response = client.create_device(device_name, arch, os, codename, labels)
client.poll_till_device_ready(response.id, sleep_interval=5, retry_limit=3)
return response
except Exception as e:
raise e
response = client.create_device(device_name, arch, os, codename, labels)
client.poll_till_device_ready(response.id, sleep_interval=5, retry_limit=12)

if response.status == 'FAILED':
raise Exception('device has failed')

return response


def delete_hwil_device(device: Device) -> None:
"""Delete a hardware-in-the-loop device.
This is a helper method that deletes a HWIL device
associated with the rapyuta.io device.
"""
labels = device.get('labels', {})
if not labels:
raise DeviceNotFound(message='hwil device not found')

def execute_onboard_command(device_id: str, onboard_command: str) -> None:
device_id = None

for l in labels:
if l['key'] == 'hwil_device_id':
device_id = l['value']
break

if device_id is None:
raise DeviceNotFound(message='hwil device not found')

client = new_hwil_client()
client.delete_device(device_id)


def execute_onboard_command(device_id: int, onboard_command: str) -> None:
"""Execute the onboard command on a hardware-in-the-loop device."""
client = new_hwil_client()
try:
Expand All @@ -227,18 +256,15 @@ def hwil_device_labels(product_name, device_name) -> typing.Dict:
}


def update_device_labels(metadata, response) -> None:
device_labels = {
"hwil_device_id": str(response.id),
"hwil_device_name": response.name,
"arch": response.architecture,
"flavor": response.flavor,
"hwil_device_username": response.username,
def make_device_labels_from_hwil_device(d: Munch) -> dict:
return {
"hwil_device_id": str(d.id),
"hwil_device_name": d.name,
"arch": d.architecture,
"flavor": d.flavor,
"hwil_device_username": d.username,
}

existing_labels = metadata.get('labels', {})
existing_labels.update(device_labels)


def sanitize_hwil_device_name(name):
if len(name) == 0:
Expand Down
2 changes: 1 addition & 1 deletion riocli/hwil/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def find_device_id(client: Client, name: str) -> str:
raise DeviceNotFound(message="HWIL device not found")


def execute_command(client: Client, device_id: str, command: str) -> typing.Tuple[int, str, str]:
def execute_command(client: Client, device_id: int, command: str) -> typing.Tuple[int, str, str]:
"""Executes a command and waits for it to complete."""
try:
response = client.execute_command(device_id, command)
Expand Down
20 changes: 8 additions & 12 deletions riocli/hwilclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ def create_device(

data = json.loads(response.text)
if not response.ok:
err_msg = data.get('error')
raise Exception("hwil: {}".format(err_msg))
raise Exception("hwil: {}".format(response.text))

return munchify(data)

Expand All @@ -136,11 +135,12 @@ def get_device(self: Client, device_id: int) -> Munch:
url = f"{self._host}/device/{device_id}"
headers = self._get_auth_header()
response = RestClient(url).method(HttpMethod.GET).headers(headers).execute()

handle_server_errors(response)

data = json.loads(response.text)
if not response.ok:
err_msg = data.get('error')
raise Exception("hwil: {}".format(err_msg))
raise Exception("hwil: {}".format(response.text))

return munchify(data)

Expand All @@ -163,8 +163,7 @@ def execute_command(self: Client, device_id: int, command: str) -> Munch:

data = json.loads(response.text)
if not response.ok:
err_msg = data.get('error')
raise Exception("hwil: {}".format(err_msg))
raise Exception("hwil: {}".format(response.text))

return munchify(data)

Expand All @@ -177,8 +176,7 @@ def get_command(self: Client, command_uuid: str) -> Munch:

data = json.loads(response.text)
if not response.ok:
err_msg = data.get('error')
raise Exception("hwil: {}".format(err_msg))
raise Exception("hwil: {}".format(response.text))

return munchify(data)

Expand All @@ -194,8 +192,7 @@ def poll_till_device_ready(self: Client, device_id: int, sleep_interval: int, re

data = json.loads(response.text)
if not response.ok:
err_msg = data.get('error')
raise Exception("hwil: {}".format(err_msg))
raise Exception("hwil: {}".format(response.text))

device = munchify(data)
if device.status != 'IDLE':
Expand All @@ -216,8 +213,7 @@ def list_devices(self: Client):

data = json.loads(response.text)
if not response.ok:
err_msg = data.get('error')
raise Exception("hwil: {}".format(err_msg))
raise Exception("hwil: {}".format(response.text))

return munchify(data)

Expand Down

0 comments on commit 8a06bff

Please sign in to comment.