diff --git a/riocli/device/delete.py b/riocli/device/delete.py index 7ff31c25..cf801bb0 100644 --- a/riocli/device/delete.py +++ b/riocli/device/delete.py @@ -11,13 +11,22 @@ # 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. +from concurrent.futures import ThreadPoolExecutor +import functools +import re +from queue import Queue +import requests +from typing import List + import click from click_help_colors import HelpColorsCommand -from requests import Response +from rapyuta_io.clients.device import Device +from yaspin.api import Yaspin +from rapyuta_io import Client from riocli.config import new_client -from riocli.constants import Colors, Symbols -from riocli.device.util import name_to_guid +from riocli.constants import Symbols, Colors +from riocli.utils import tabulate_data from riocli.utils.spinner import with_spinner @@ -27,41 +36,137 @@ 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='deletes all devices') +@click.option('--workers', '-w', + help="number of parallel workers while running delete devices " + "command. 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]) - error = data.get('response', {}).get('error') + data, fg, statuses = [], Colors.GREEN, [] + success_count, failed_count = 0, 0 - if 'deployments' in error: - msg = 'Device has running deployments. Please de-provision them before deleting the device.' - raise Exception(msg) + for name, response in result: + if response.status_code and response.status_code < 400: + fg = Colors.GREEN + icon = Symbols.SUCCESS + success_count += 1 + else: + fg = Colors.RED + icon = Symbols.ERROR + failed_count +=1 + if response.status_code: + r = response.json() + error = r.get('response', {}).get('error') - raise Exception(error) + if 'deployments' in error: + msg = 'Device {0} has running deployments. Please de-provision them before deleting the device.'.format(name) + spinner.text = click.style(msg, Colors.YELLOW) + spinner.ok(click.style(Symbols.WARNING, Colors.YELLOW)) + + data.append([ + click.style(name, fg), + click.style(icon, fg) + ]) + + with spinner.hidden(): + tabulate_data(data, headers=['Name', 'Status']) + + spinner.write('') + if success_count: + spinner.text = click.style( + '{0} Devices(s) deleted successfully.'.format(success_count), Colors.GREEN) + spinner.ok(click.style(Symbols.SUCCESS, Colors.GREEN)) + if failed_count: + spinner.text = click.style( + '{0} Devices(s) deletion failed.'.format(failed_count), Colors.YELLOW) + spinner.ok(click.style(Symbols.WARNING, Colors.YELLOW)) + except Exception as e: + spinner.text = click.style( + 'Failed to delete device(s): {}'.format(e), Colors.RED) + spinner.red.fail(Symbols.ERROR) + raise SystemExit(1) from e + + +def fetch_devices( + client: Client, + device_name_or_regex: str, + delete_all: bool, +) -> List[Device]: + devices = client.get_all_devices() + result = [] + for device in devices: + if (delete_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 _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))