From c476e5d60ea4ff2c4721b25bd69e83c29af39c02 Mon Sep 17 00:00:00 2001 From: Roman Skurikhin Date: Thu, 3 Sep 2020 18:36:22 +0300 Subject: [PATCH] Add cli commands for disk management --- CHANGELOG.D/1716.feature | 2 +- CLI.md | 125 +++++++++++++++++++++++++++- neuromation/cli/disks.py | 77 +++++++++++++++++ neuromation/cli/formatters/disks.py | 44 ++++++++++ neuromation/cli/main.py | 3 +- neuromation/cli/secrets.py | 2 +- tests/cli/formatters/test_disks.py | 28 +++++++ tests/e2e/test_e2e_disks.py | 29 +++++++ 8 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 neuromation/cli/disks.py create mode 100644 neuromation/cli/formatters/disks.py create mode 100644 tests/cli/formatters/test_disks.py create mode 100644 tests/e2e/test_e2e_disks.py diff --git a/CHANGELOG.D/1716.feature b/CHANGELOG.D/1716.feature index 54b2c87c3..746163361 100644 --- a/CHANGELOG.D/1716.feature +++ b/CHANGELOG.D/1716.feature @@ -1 +1 @@ -Added persistent disks management subsystem to api. \ No newline at end of file +Implemented disks management commands. \ No newline at end of file diff --git a/CLI.md b/CLI.md index d459116a3..a1195b23a 100644 --- a/CLI.md +++ b/CLI.md @@ -69,6 +69,11 @@ * [neuro secret ls](#neuro-secret-ls) * [neuro secret add](#neuro-secret-add) * [neuro secret rm](#neuro-secret-rm) + * [neuro disk](#neuro-disk) + * [neuro disk ls](#neuro-disk-ls) + * [neuro disk create](#neuro-disk-create) + * [neuro disk get](#neuro-disk-get) + * [neuro disk rm](#neuro-disk-rm) * [neuro help](#neuro-help) * [neuro run](#neuro-run) * [neuro ps](#neuro-ps) @@ -132,6 +137,7 @@ Name | Description| | _[neuro acl](#neuro-acl)_| Access Control List management | | _[neuro blob](#neuro-blob)_| Blob storage operations | | _[neuro secret](#neuro-secret)_| Operations with secrets | +| _[neuro disk](#neuro-disk)_| Operations with disks | **Commands:** @@ -1808,7 +1814,7 @@ Name | Description| |---|---| | _[neuro secret ls](#neuro-secret-ls)_| List secrets | | _[neuro secret add](#neuro-secret-add)_| Add secret KEY with data VALUE | -| _[neuro secret rm](#neuro-secret-rm)_| Add secret KEY | +| _[neuro secret rm](#neuro-secret-rm)_| Remove secret KEY | @@ -1862,7 +1868,7 @@ Name | Description| ### neuro secret rm -Add secret KEY. +Remove secret KEY. **Usage:** @@ -1879,6 +1885,121 @@ Name | Description| +## neuro disk + +Operations with disks. + +**Usage:** + +```bash +neuro disk [OPTIONS] COMMAND [ARGS]... +``` + +**Options:** + +Name | Description| +|----|------------| +|_--help_|Show this message and exit.| + + +**Commands:** + +|Usage|Description| +|---|---| +| _[neuro disk ls](#neuro-disk-ls)_| List disks | +| _[neuro disk create](#neuro-disk-create)_| Create disk with storage amount STORAGE | +| _[neuro disk get](#neuro-disk-get)_| Get disk DISK_ID | +| _[neuro disk rm](#neuro-disk-rm)_| Remove disk DISK_ID | + + + + +### neuro disk ls + +List disks. + +**Usage:** + +```bash +neuro disk ls [OPTIONS] +``` + +**Options:** + +Name | Description| +|----|------------| +|_--help_|Show this message and exit.| +|_\--full-uri_|Output full disk URI.| + + + + +### neuro disk create + +Create disk with storage amount STORAGE.
+ +**Usage:** + +```bash +neuro disk create [OPTIONS] STORAGE +``` + +**Examples:** + +```bash + +neuro disk create 10Gi +neuro disk create 500Mi + +``` + +**Options:** + +Name | Description| +|----|------------| +|_--help_|Show this message and exit.| + + + + +### neuro disk get + +Get disk DISK_ID. + +**Usage:** + +```bash +neuro disk get [OPTIONS] DISK_ID +``` + +**Options:** + +Name | Description| +|----|------------| +|_--help_|Show this message and exit.| + + + + +### neuro disk rm + +Remove disk DISK_ID. + +**Usage:** + +```bash +neuro disk rm [OPTIONS] DISK_ID +``` + +**Options:** + +Name | Description| +|----|------------| +|_--help_|Show this message and exit.| + + + + ## neuro help Get help on a command. diff --git a/neuromation/cli/disks.py b/neuromation/cli/disks.py new file mode 100644 index 000000000..2f99c2b43 --- /dev/null +++ b/neuromation/cli/disks.py @@ -0,0 +1,77 @@ +from .formatters.disks import DiskFormatter, DisksFormatter +from .formatters.utils import URIFormatter, uri_formatter +from .parse_utils import parse_memory +from .root import Root +from .utils import argument, command, group, option, pager_maybe + + +@group() +def disk() -> None: + """ + Operations with disks. + """ + + +@command() +@option("--full-uri", is_flag=True, help="Output full disk URI.") +async def ls(root: Root, full_uri: bool) -> None: + """ + List disks. + """ + + if full_uri: + uri_fmtr: URIFormatter = str + else: + uri_fmtr = uri_formatter( + username=root.client.username, cluster_name=root.client.cluster_name + ) + disks_fmtr = DisksFormatter(uri_fmtr) + + disks = [] + async for disk in root.client.disks.list(): + disks.append(disk) + + pager_maybe(disks_fmtr(disks), root.tty, root.terminal_size) + + +@command() +@argument("storage") +async def create(root: Root, storage: str) -> None: + """ + Create disk with storage amount STORAGE. + + Examples: + + neuro disk create 10Gi + neuro disk create 500Mi + """ + disk = await root.client.disks.create(parse_memory(storage)) + disk_fmtr = DiskFormatter(str) + pager_maybe(disk_fmtr(disk), root.tty, root.terminal_size) + + +@command() +@argument("disk_id") +async def get(root: Root, disk_id: str) -> None: + """ + Get disk DISK_ID. + """ + disk = await root.client.disks.get(disk_id) + disk_fmtr = DiskFormatter(str) + pager_maybe(disk_fmtr(disk), root.tty, root.terminal_size) + + +@command() +@argument("disk_id") +async def rm(root: Root, disk_id: str) -> None: + """ + Remove disk DISK_ID. + """ + + await root.client.disks.rm(disk_id) + + +disk.add_command(ls) +disk.add_command(create) +disk.add_command(get) +disk.add_command(rm) diff --git a/neuromation/cli/formatters/disks.py b/neuromation/cli/formatters/disks.py new file mode 100644 index 000000000..966d17f26 --- /dev/null +++ b/neuromation/cli/formatters/disks.py @@ -0,0 +1,44 @@ +from typing import Iterator, Sequence + +import click + +from neuromation.api import Disk +from neuromation.cli.formatters.ftable import table +from neuromation.cli.formatters.utils import URIFormatter + + +class DisksFormatter: + _table_header = [ + click.style("Id", bold=True), + click.style("Storage", bold=True), + click.style("Uri", bold=True), + ] + + def __init__(self, uri_formatter: URIFormatter): + self._uri_formatter = uri_formatter + + def _disk_to_table_row(self, disk: Disk) -> Sequence[str]: + if disk.storage >= 1024 ** 3: + storage_str = f"{disk.storage / (1024 ** 3):.2f}Gi" + elif disk.storage >= 1024 ** 2: + storage_str = f"{disk.storage / (1024 ** 2):.2f}Mi" + elif disk.storage >= 1024: + storage_str = f"{disk.storage / 1024:.2f}Ki" + else: + storage_str = str(disk.storage) + return [disk.id, storage_str, self._uri_formatter(disk.uri)] + + def __call__(self, disks: Sequence[Disk]) -> Iterator[str]: + disks_info = [ + self._table_header, + *(self._disk_to_table_row(disk) for disk in disks), + ] + return table(disks_info) + + +class DiskFormatter: + def __init__(self, uri_formatter: URIFormatter): + self._disks_formatter = DisksFormatter(uri_formatter) + + def __call__(self, disk: Disk) -> Iterator[str]: + return self._disks_formatter([disk]) diff --git a/neuromation/cli/main.py b/neuromation/cli/main.py index 1a38b5555..bc9dd3f9c 100644 --- a/neuromation/cli/main.py +++ b/neuromation/cli/main.py @@ -23,6 +23,7 @@ blob_storage, completion, config, + disks, image, job, project, @@ -483,8 +484,8 @@ def help(ctx: click.Context, command: Sequence[str]) -> None: cli.add_command(completion.completion) cli.add_command(share.acl) cli.add_command(blob_storage.blob_storage) -cli.add_command(blob_storage.blob_storage) cli.add_command(secrets.secret) +cli.add_command(disks.disk) cli.add_command(DeprecatedGroup(storage.storage, name="store", hidden=True)) diff --git a/neuromation/cli/secrets.py b/neuromation/cli/secrets.py index 307736ed3..575f2e359 100644 --- a/neuromation/cli/secrets.py +++ b/neuromation/cli/secrets.py @@ -47,7 +47,7 @@ async def add(root: Root, key: str, value: str) -> None: @argument("key") async def rm(root: Root, key: str) -> None: """ - Add secret KEY. + Remove secret KEY. """ await root.client.secrets.rm(key) diff --git a/tests/cli/formatters/test_disks.py b/tests/cli/formatters/test_disks.py new file mode 100644 index 000000000..f89c5a51a --- /dev/null +++ b/tests/cli/formatters/test_disks.py @@ -0,0 +1,28 @@ +import click + +from neuromation.api import Disk +from neuromation.cli.formatters.disks import DiskFormatter, DisksFormatter + + +def test_disk_formatter() -> None: + disk = Disk("disk", int(11.93 * (1024 ** 3)), "user", Disk.Status.READY, "cluster") + fmtr = DiskFormatter(str) + header_line, info_line = (click.unstyle(line).rstrip() for line in fmtr(disk)) + assert header_line.split() == ["Id", "Storage", "Uri"] + assert info_line.split() == ["disk", "11.93Gi", "disk://cluster/user/disk-id"] + + +def test_disks_formatter() -> None: + disks = [ + Disk("disk-1", 50 * (1024 ** 3), "user", Disk.Status.READY, "cluster"), + Disk("disk-2", 50 * (1024 ** 2), "user", Disk.Status.READY, "cluster"), + Disk("disk-3", 50 * (1024 ** 1), "user", Disk.Status.READY, "cluster"), + Disk("disk-4", 50, "user", Disk.Status.READY, "cluster"), + ] + fmtr = DisksFormatter(str) + header_line, *info_lines = (click.unstyle(line).rstrip() for line in fmtr(disks)) + assert header_line.split() == ["Id", "Storage", "Uri"] + assert info_lines[0].split() == ["disk-1", "50.00Gi", "disk://cluster/user/disk-1"] + assert info_lines[1].split() == ["disk-2", "50.00Mi", "disk://cluster/user/disk-2"] + assert info_lines[2].split() == ["disk-3", "50.00Ki", "disk://cluster/user/disk-3"] + assert info_lines[3].split() == ["disk-4", "50", "disk://cluster/user/disk-4"] diff --git a/tests/e2e/test_e2e_disks.py b/tests/e2e/test_e2e_disks.py new file mode 100644 index 000000000..7aa8ec8f3 --- /dev/null +++ b/tests/e2e/test_e2e_disks.py @@ -0,0 +1,29 @@ +import pytest + +from tests.e2e.conftest import Helper + + +@pytest.mark.e2e +def test_create_get_list_delete(helper: Helper) -> None: + cap = helper.run_cli(["disk", "ls"]) + assert cap.err == "" + + cap = helper.run_cli(["disk", "create", "2Gi"]) + assert cap.err == "" + disk_id, *_ = cap.out.splitlines()[1].split() + + cap = helper.run_cli(["disk", "ls"]) + assert cap.err == "" + assert disk_id in cap.out + + cap = helper.run_cli(["disk", "get", disk_id]) + assert cap.err == "" + assert disk_id in cap.out + assert "2Gi" in cap.out + + cap = helper.run_cli(["disk", "rm", disk_id]) + assert cap.err == "" + + cap = helper.run_cli(["disk", "ls"]) + assert cap.err == "" + assert disk_id not in cap.out