Skip to content

Commit

Permalink
feat(device): updates device delete command to delete multiple devices (
Browse files Browse the repository at this point in the history
#217)

This commit updates device delete command that enables users
to conveniently delete existing devices by providing device
name or regex that can delete multiple devices

Usage: python -m rio device delete [OPTIONS] [DEVICE_NAME_OR_REGEX]

  Deletes one more devices

Options:
  -f, --force, --silent  Skip confirmation
  -a, --delete-all       Deletes all devices
  -w, --workers INTEGER  number of parallel workers while running delete device command. defaults to 10.
  --help                 Show this message and exit.
  • Loading branch information
RomilShah authored Nov 9, 2023
1 parent a24ae37 commit 1a35403
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 27 deletions.
151 changes: 124 additions & 27 deletions riocli/device/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@
# 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 functools
from concurrent.futures import ThreadPoolExecutor
from queue import Queue

import click
import requests
from click_help_colors import HelpColorsCommand
from requests import Response
from rapyuta_io import Client
from rapyuta_io.clients.device import Device
from yaspin.api import Yaspin

from riocli.config import new_client
from riocli.constants import Colors, Symbols
from riocli.device.util import name_to_guid
from riocli.device.util import fetch_devices
from riocli.constants import Symbols, Colors
from riocli.utils import tabulate_data
from riocli.utils.spinner import with_spinner


Expand All @@ -27,41 +35,130 @@
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.option('--force', '-f', 'force', is_flag=True, help='Skip confirmation')
@click.argument('device-name', type=str)
@name_to_guid
@click.option('--force', '-f', '--silent', is_flag=True, default=False,
help='Skip confirmation')
@click.option('--delete-all', '-a', is_flag=True, default=False,
help='Delete all devices')
@click.option('--workers', '-w',
help="Number of parallel workers for deleting devices. Defaults to 10.", type=int, default=10)
@click.argument('device-name-or-regex', type=str, default="")
@with_spinner(text='Deleting device...')
def delete_device(device_name: str, device_guid: str, force: bool, spinner=None):
def delete_device(
force: bool,
delete_all: bool,
workers: int,
device_name_or_regex: str,
spinner: Yaspin = None,
) -> None:
"""
Deletes a device
Deletes one more devices
"""
with spinner.hidden():
if not force:
click.confirm(
'Deleting device {} ({})'.format(
device_name, device_guid), abort=True)
client = new_client()
if not (device_name_or_regex or delete_all):
spinner.text = 'Nothing to delete'
spinner.green.ok(Symbols.SUCCESS)
return

try:
client = new_client(with_project=True)
handle_device_delete_error(client.delete_device(device_id=device_guid))
spinner.text = click.style('Device deleted successfully', fg=Colors.GREEN)
spinner.green.ok(Symbols.SUCCESS)
devices = fetch_devices(
client, device_name_or_regex, delete_all)
except Exception as e:
spinner.text = click.style('Failed to delete device: {}'.format(e), fg=Colors.RED)
spinner.text = click.style(
'Failed to delete device(s): {}'.format(e), Colors.RED)
spinner.red.fail(Symbols.ERROR)
raise SystemExit(1) from e


def handle_device_delete_error(response: Response):
if response.status_code < 400:
if not devices:
spinner.text = "No devices to delete"
spinner.ok(Symbols.SUCCESS)
return

data = response.json()
headers = ['Name', 'Device ID', 'Status']
data = [[d.name, d.uuid, d.status] for d in devices]

with spinner.hidden():
tabulate_data(data, headers)

spinner.write('')

if not force:
with spinner.hidden():
click.confirm('Do you want to delete above device(s)?',
default=True, abort=True)
spinner.write('')

try:
result = Queue()
func = functools.partial(_delete_deivce, client, result)
with ThreadPoolExecutor(max_workers=workers) as executor:
executor.map(func, devices)

result = sorted(list(result.queue), key=lambda x: x[0])

data, fg, statuses = [], Colors.GREEN, []
success_count, failed_count = 0, 0

for name, response in result:
if response.status_code and response.status_code < 400:
fg = Colors.GREEN
icon = Symbols.SUCCESS
success_count += 1
msg = ''
else:
fg = Colors.RED
icon = Symbols.ERROR
failed_count += 1
msg = get_error_message(response, name)

data.append([
click.style(name, fg),
click.style(icon, fg),
click.style(msg, fg)
])

with spinner.hidden():
tabulate_data(data, headers=['Name', 'Status', 'Message'])

spinner.write('')

if failed_count == 0 and success_count == len(devices):
spinner_text = click.style('All devices deleted successfully.', Colors.GREEN)
spinner_char = click.style(Symbols.SUCCESS, Colors.GREEN)
elif success_count == 0 and failed_count == len(devices):
spinner_text = click.style('Failed to delete devices', Colors.YELLOW)
spinner_char = click.style(Symbols.WARNING, Colors.YELLOW)
else:
spinner_text = click.style(
'{}/{} devices deleted successfully'.format(success_count, len(devices)), Colors.YELLOW)
spinner_char = click.style(Symbols.WARNING, Colors.YELLOW)

spinner.text = spinner_text
spinner.ok(spinner_char)
except Exception as e:
spinner.text = click.style(
'Failed to delete devices: {}'.format(e), Colors.RED)
spinner.red.fail(Symbols.ERROR)
raise SystemExit(1) from e

def _delete_deivce(
client: Client,
result: Queue,
device: Device = None,
) -> None:
response = requests.models.Response()
try:
response = client.delete_device(device_id=device.uuid)
result.put((device["name"], response))
except Exception:
result.put((device["name"], response))


error = data.get('response', {}).get('error')
def get_error_message(response: requests.models.Response, name: str) -> str:
if response.status_code:
r = response.json()
error = r.get('response', {}).get('error')

if 'deployments' in error:
msg = 'Device has running deployments. Please de-provision them before deleting the device.'
raise Exception(msg)
if 'deployments' in error:
return 'Device {0} has running deployments.'.format(name)

raise Exception(error)
return ""
18 changes: 18 additions & 0 deletions riocli/device/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
import functools
import typing
from pathlib import Path
import re

import click
from rapyuta_io import Client
from rapyuta_io.clients import LogUploads
from rapyuta_io.clients.device import Device

from riocli.config import new_client
from riocli.constants import Colors
Expand Down Expand Up @@ -100,6 +102,22 @@ def decorated(**kwargs):

return decorated

def fetch_devices(
client: Client,
device_name_or_regex: str,
include_all: bool,
) -> typing.List[Device]:
devices = client.get_all_devices()
result = []
for device in devices:
if (include_all or device.name == device_name_or_regex or
(device_name_or_regex not in device.name and
re.search(device_name_or_regex, device.name)) or
device_name_or_regex == device.uuid):
result.append(device)

return result


def find_request_id(requests: typing.List[LogUploads], file_name: str) -> (str, str):
for request in requests:
Expand Down

0 comments on commit 1a35403

Please sign in to comment.