Skip to content
This repository was archived by the owner on Sep 22, 2023. It is now read-only.

Commit 549ed20

Browse files
authored
Support hardware metadata queries (#152)
* feat: Include agent hardware metadata query in "admin agent" command * feat: Translate empty/null values using cli.utils.format_nested_dict() and cli.utils.format_value() * feat: Add "admin storage", "admin storage-list" commands * feat: Include performance metric in "admin storage" result
1 parent bce17d2 commit 549ed20

File tree

7 files changed

+220
-3
lines changed

7 files changed

+220
-3
lines changed

changes/152.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support for hardware metadata queries for super-admins
2+
- Add `admin storages` and `admin storage {vfolder-host}` commands which provides the hardware metadata and performance metric
3+
- Include `hardware_metadata` field in the `Agent` graph object type.

src/ai/backend/client/cli/admin/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ def admin():
1010

1111
def _attach_command():
1212
from . import ( # noqa
13-
agents, domains, etcd, groups, images, keypairs, resources, resource_policies,
14-
scaling_groups, sessions, users, vfolders,
13+
agents,
14+
domains,
15+
etcd,
16+
groups,
17+
images,
18+
keypairs,
1519
license,
20+
resources,
21+
resource_policies,
22+
scaling_groups,
23+
sessions,
24+
storage,
25+
users,
26+
vfolders,
1627
)
1728

1829

src/ai/backend/client/cli/admin/agents.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
echo_via_pager,
1414
tabulate_items,
1515
)
16+
from ..utils import format_nested_dicts
1617
from ...exceptions import NoItems
1718

1819

@@ -85,6 +86,7 @@ def agent(agent_id):
8586
('CPU Usage (%)', 'cpu_cur_pct'),
8687
('Total slots', 'available_slots'),
8788
('Occupied slots', 'occupied_slots'),
89+
('Hardware Metadata', 'hardware_metadata'),
8890
('Live Stat', 'live_stat'),
8991
]
9092
if is_legacy_server():
@@ -104,6 +106,8 @@ def agent(agent_id):
104106
if key in resp:
105107
if key == 'live_stat' and resp[key] is not None:
106108
rows.append((name, format_stats(json.loads(resp[key]))))
109+
elif key == 'hardware_metadata':
110+
rows.append((name, format_nested_dicts(json.loads(resp[key]))))
107111
else:
108112
rows.append((name, resp[key]))
109113
print(tabulate(rows, headers=('Field', 'Value')))
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import json
2+
import sys
3+
4+
import click
5+
from tabulate import tabulate
6+
7+
from . import admin
8+
from ...session import Session
9+
from ..pretty import print_error
10+
from ..pagination import (
11+
get_preferred_page_size,
12+
echo_via_pager,
13+
tabulate_items,
14+
)
15+
from ..utils import format_nested_dicts, format_value
16+
from ...exceptions import NoItems
17+
18+
19+
@admin.command()
20+
@click.argument('vfolder_host')
21+
def storage(vfolder_host):
22+
"""
23+
Show the information about the given storage volume.
24+
(super-admin privilege required)
25+
"""
26+
fields = [
27+
('ID', 'id'),
28+
('Backend', 'backend'),
29+
('Capabilities', 'capabilities'),
30+
('Path', 'path'),
31+
('FS prefix', 'fsprefix'),
32+
('Hardware Metadata', 'hardware_metadata'),
33+
('Perf. Metric', 'performance_metric'),
34+
]
35+
with Session() as session:
36+
try:
37+
resp = session.Storage.detail(
38+
vfolder_host=vfolder_host,
39+
fields=(item[1] for item in fields))
40+
except Exception as e:
41+
print_error(e)
42+
sys.exit(1)
43+
rows = []
44+
for name, key in fields:
45+
if key in resp:
46+
if key in ('hardware_metadata', 'performance_metric'):
47+
rows.append((name, format_nested_dicts(json.loads(resp[key]))))
48+
else:
49+
rows.append((name, format_value(resp[key])))
50+
print(tabulate(rows, headers=('Field', 'Value')))
51+
52+
53+
@admin.command()
54+
def storage_list():
55+
"""
56+
List storage volumes.
57+
(super-admin privilege required)
58+
"""
59+
fields = [
60+
('ID', 'id'),
61+
('Backend', 'backend'),
62+
('Capabilities', 'capabilities'),
63+
]
64+
try:
65+
with Session() as session:
66+
page_size = get_preferred_page_size()
67+
try:
68+
items = session.Storage.paginated_list(
69+
fields=[f[1] for f in fields],
70+
page_size=page_size,
71+
)
72+
echo_via_pager(
73+
tabulate_items(items, fields)
74+
)
75+
except NoItems:
76+
print("There are no storage volumes.")
77+
except Exception as e:
78+
print_error(e)
79+
sys.exit(1)

src/ai/backend/client/cli/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import re
2+
import textwrap
3+
from typing import Any, Mapping
24

35
import click
46

@@ -37,3 +39,34 @@ def convert(self, value, param, ctx):
3739
if m is None:
3840
self.fail(f"{value!r} is not a valid byte-size expression", param, ctx)
3941
return value
42+
43+
44+
def format_nested_dicts(value: Mapping[str, Mapping[str, Any]]) -> str:
45+
"""
46+
Format a mapping from string keys to sub-mappings.
47+
"""
48+
rows = []
49+
if not value:
50+
rows.append("(empty)")
51+
for outer_key, outer_value in value.items():
52+
if isinstance(outer_value, dict):
53+
if outer_value:
54+
rows.append(f"+ {outer_key}")
55+
inner_rows = format_nested_dicts(outer_value)
56+
rows.append(textwrap.indent(inner_rows, prefix=" "))
57+
else:
58+
rows.append(f"+ {outer_key}: (empty)")
59+
else:
60+
if outer_value is None:
61+
rows.append(f"- {outer_key}: (null)")
62+
else:
63+
rows.append(f"- {outer_key}: {outer_value}")
64+
return "\n".join(rows)
65+
66+
67+
def format_value(value: Any) -> str:
68+
if value is None:
69+
return "(null)"
70+
if isinstance(value, (dict, list, set)) and not value:
71+
return "(empty)"
72+
return str(value)

src/ai/backend/client/func/storage.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import textwrap
2+
from typing import (
3+
AsyncIterator,
4+
Sequence,
5+
)
6+
7+
from .base import api_function, BaseFunction
8+
from ..request import Request
9+
from ..pagination import generate_paginated_results
10+
11+
__all__ = (
12+
'Storage',
13+
)
14+
15+
_default_list_fields = (
16+
'id',
17+
'backend',
18+
'capabilities',
19+
)
20+
21+
_default_detail_fields = (
22+
'id',
23+
'backend',
24+
'path',
25+
'fsprefix',
26+
'capabilities',
27+
'hardware_metadata',
28+
)
29+
30+
31+
class Storage(BaseFunction):
32+
"""
33+
Provides a shortcut of :func:`Admin.query()
34+
<ai.backend.client.admin.Admin.query>` that fetches various straoge volume
35+
information keyed by vfolder hosts.
36+
37+
.. note::
38+
39+
All methods in this function class require your API access key to
40+
have the *super-admin* privilege.
41+
"""
42+
43+
@api_function
44+
@classmethod
45+
async def paginated_list(
46+
cls,
47+
status: str = 'ALIVE',
48+
*,
49+
fields: Sequence[str] = _default_list_fields,
50+
page_size: int = 20,
51+
) -> AsyncIterator[dict]:
52+
"""
53+
Lists the keypairs.
54+
You need an admin privilege for this operation.
55+
"""
56+
async for item in generate_paginated_results(
57+
'storage_volume_list',
58+
{},
59+
fields,
60+
page_size=page_size,
61+
):
62+
yield item
63+
64+
@api_function
65+
@classmethod
66+
async def detail(
67+
cls,
68+
vfolder_host: str,
69+
fields: Sequence[str] = _default_detail_fields,
70+
) -> Sequence[dict]:
71+
query = textwrap.dedent("""\
72+
query($vfolder_host: String!) {
73+
storage_volume(id: $vfolder_host) {$fields}
74+
}
75+
""")
76+
query = query.replace('$fields', ' '.join(fields))
77+
variables = {'vfolder_host': vfolder_host}
78+
rqst = Request('POST', '/admin/graphql')
79+
rqst.set_json({
80+
'query': query,
81+
'variables': variables,
82+
})
83+
async with rqst.fetch() as resp:
84+
data = await resp.json()
85+
return data['storage_volume']

src/ai/backend/client/session.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ class BaseSession(metaclass=abc.ABCMeta):
243243
'_config', '_closed', '_context_token', '_proxy_mode',
244244
'aiohttp_session', 'api_version',
245245
'System', 'Manager', 'Admin',
246-
'Agent', 'AgentWatcher', 'ScalingGroup',
246+
'Agent', 'AgentWatcher', 'ScalingGroup', 'Storage',
247247
'Image', 'ComputeSession', 'SessionTemplate',
248248
'Domain', 'Group', 'Auth', 'User', 'KeyPair',
249249
'BackgroundTask',
@@ -273,6 +273,7 @@ def __init__(
273273
from .func.system import System
274274
from .func.admin import Admin
275275
from .func.agent import Agent, AgentWatcher
276+
from .func.storage import Storage
276277
from .func.auth import Auth
277278
from .func.bgtask import BackgroundTask
278279
from .func.domain import Domain
@@ -295,6 +296,7 @@ def __init__(
295296
self.Admin = Admin
296297
self.Agent = Agent
297298
self.AgentWatcher = AgentWatcher
299+
self.Storage = Storage
298300
self.Auth = Auth
299301
self.BackgroundTask = BackgroundTask
300302
self.EtcdConfig = EtcdConfig

0 commit comments

Comments
 (0)