From acdde634181e428b804ec3298733d1aef37d9ac4 Mon Sep 17 00:00:00 2001 From: Dev Gupta <72784479+guptadev21@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:46:39 +0530 Subject: [PATCH] feat(device): implements report device command (#371) This commit implements the command to report a device and upload debug files from the device. You can also generate a public URL of the uploaded files that can be shared with support. Fixes AB#19657 --- .gitignore | 3 +- riocli/device/__init__.py | 2 + riocli/device/report.py | 103 ++++++++++++++++++++++++++++++++++++++ riocli/device/util.py | 45 +++++++++++++++-- 4 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 riocli/device/report.py diff --git a/.gitignore b/.gitignore index b0bc2512..768388ba 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ riocli/jsonschema/schemas/json/ *.class # Compiled Python bytecode -venv/ +venv*/ .venv/ build/ *.egg-info/ @@ -55,4 +55,3 @@ Thumbs.db .vscode/ .egg-info/ -dist/ diff --git a/riocli/device/__init__.py b/riocli/device/__init__.py index fc8ac999..19a8607a 100644 --- a/riocli/device/__init__.py +++ b/riocli/device/__init__.py @@ -25,6 +25,7 @@ from riocli.device.list import list_devices from riocli.device.migrate import migrate_project from riocli.device.onboard import device_onboard +from riocli.device.report import report_device from riocli.device.tools import tools from riocli.device.topic import device_topics from riocli.device.vpn import toggle_vpn @@ -54,6 +55,7 @@ def device(): device.add_command(inspect_device) device.add_command(list_deployments) device.add_command(list_devices) +device.add_command(report_device) device.add_command(tools) device.add_command(toggle_vpn) device.add_command(migrate_project) diff --git a/riocli/device/report.py b/riocli/device/report.py new file mode 100644 index 00000000..abcfad47 --- /dev/null +++ b/riocli/device/report.py @@ -0,0 +1,103 @@ +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import click +from click_help_colors import HelpColorsCommand + +from riocli.constants import Colors, Symbols +from riocli.device.util import name_to_guid, generate_shared_url, upload_debug_logs +from riocli.utils.spinner import with_spinner + + +@click.command( + "report", + cls=HelpColorsCommand, + help_headers_color=Colors.YELLOW, + help_options_color=Colors.GREEN, + hidden=True, +) +@click.option( + "--force", "-f", "--silent", "force", is_flag=True, help="Skip confirmation" +) +@click.option( + "--share", + "-s", + is_flag=True, + help="Generate a public URL for sharing", + default=False, +) +@click.option( + "--expiry", + "-e", + help="Expiry time for the shared URL in days", + default=7, + required=False, + show_default=True, +) +@click.argument("device-name", type=str) +@name_to_guid +@with_spinner(text="Upload debugging files...") +def report_device( + device_name: str, + device_guid: str, + force: bool, + share: bool, + expiry: int, + spinner=None, +) -> None: + """ + Uploads device debug logs and optionally generates a shareable URL with an expiry time. + + Usage Examples: + + Report a device with confirmation prompt. + + $ rio device report DEVICE_NAME + + Report a device without confirmation prompt. + + $ rio device report -f DEVICE_NAME + + Report a device and generate a public URL for sharing. + + $ rio device report -s DEVICE_NAME + + Report a device with a custom expiry time for the shared URL. + + $ rio device report -s -e 10 DEVICE_NAME + """ + if not force: + with spinner.hidden(): + click.confirm( + "Report Device?", + abort=True, + ) + try: + # Upload the debug logs + response = upload_debug_logs(device_guid=device_guid) + + if share: + spinner.text = click.style("Creating shared URL...", fg=Colors.YELLOW) + public_url = generate_shared_url( + device_guid, response["response"]["data"]["request_uuid"], expiry + ) + click.secho("\nURL: {}".format(public_url.url), fg=Colors.GREEN) + + spinner.text = click.style("Device reported successfully.", fg=Colors.GREEN) + spinner.green.ok(Symbols.SUCCESS) + + except Exception as e: + spinner.text = click.style( + "Failed to report device: {}".format(e), fg=Colors.RED + ) + spinner.red.fail(Symbols.ERROR) diff --git a/riocli/device/util.py b/riocli/device/util.py index 0dbc6d24..172447e8 100644 --- a/riocli/device/util.py +++ b/riocli/device/util.py @@ -16,14 +16,14 @@ import re import time import typing +from datetime import datetime, timedelta from pathlib import Path import click from munch import Munch from rapyuta_io import Client -from rapyuta_io.clients import LogUploads -from rapyuta_io.clients.device import Device -from rapyuta_io.clients.device import DeviceStatus +from rapyuta_io.clients import LogUploads, SharedURL +from rapyuta_io.clients.device import Device, DeviceStatus from rapyuta_io.utils import RestClient from rapyuta_io.utils.rest_client import HttpMethod @@ -33,6 +33,7 @@ from riocli.exceptions import DeviceNotFound from riocli.hwil.util import execute_command, find_device_id from riocli.utils import is_valid_uuid, trim_prefix, trim_suffix +from riocli.v2client.util import handle_server_errors def name_to_guid(f: typing.Callable) -> typing.Callable: @@ -74,6 +75,44 @@ def decorated(**kwargs: typing.Any): return decorated +def upload_debug_logs(device_guid: str) -> dict: + config = Configuration() + coreapi_host = config.data.get( + "core_api_host", "https://gaapiserver.apps.okd4v2.prod.rapyuta.io" + ) + + url = "{}/api/device-manager/v0/error_handler/upload_debug_logs/{}".format( + coreapi_host, device_guid + ) + + headers = config.get_auth_header() + response = ( + RestClient(url).method(HttpMethod.POST).headers(headers).execute(payload={}) + ) + + result = handle_server_errors(response) + + if result: + return result + + return response.json() + + +def generate_shared_url(device_guid: str, request_id: str, expiry: int, spinner=None): + try: + client = new_client() + device = client.get_device(device_id=device_guid) + expiry_time = datetime.now() + timedelta(days=expiry) + + # Create the shared URL + public_url = device.create_shared_url( + SharedURL(request_id, expiry_time=expiry_time) + ) + return public_url + except Exception as e: + raise Exception(f"Failed to create shared URL: {e}") + + def get_device_name(client: Client, guid: str) -> str: device = client.get_device(device_id=guid) return device.name