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

Commit 1f351cc

Browse files
committed
Add etcd commands for admin (#84)
* Add a set of commands under "backend.ai admin etcd", just like the manager CLI.
1 parent 5d132ca commit 1f351cc

File tree

4 files changed

+190
-5
lines changed

4 files changed

+190
-5
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def admin():
1010

1111
def _attach_command():
1212
from . import ( # noqa
13-
agents, domains, groups, images, keypairs, resources, resource_policies,
13+
agents, domains, etcd, groups, images, keypairs, resources, resource_policies,
1414
scaling_groups, sessions, users, vfolders
1515
)
1616

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import json
2+
import sys
3+
4+
import click
5+
6+
from . import admin
7+
from ..pretty import print_pretty, print_error, print_fail
8+
from ...session import Session
9+
10+
11+
@admin.group(invoke_without_command=False)
12+
@click.pass_context
13+
def etcd(ctx):
14+
'''
15+
List and manage ETCD configurations.
16+
(admin privilege required)
17+
'''
18+
if ctx.invoked_subcommand is not None:
19+
return
20+
21+
22+
@etcd.command()
23+
@click.argument('key', type=str, metavar='KEY')
24+
@click.option('-p', '--prefix', is_flag=True, default=False,
25+
help='Get all keys prefixed with the given key.')
26+
def get(key, prefix):
27+
'''
28+
Get a ETCD value(s).
29+
30+
KEY: Name of ETCD key.
31+
'''
32+
with Session() as session:
33+
try:
34+
data = session.EtcdConfig.get(key, prefix)
35+
except Exception as e:
36+
print_error(e)
37+
sys.exit(1)
38+
data = json.dumps(data, indent=2) if data else 'null'
39+
print_pretty(data)
40+
41+
42+
@etcd.command()
43+
@click.argument('key', type=str, metavar='KEY')
44+
@click.argument('value', type=str, metavar='VALUE')
45+
def set(key, value):
46+
'''
47+
Set new key and value on ETCD.
48+
49+
KEY: Name of ETCD key.
50+
VALUE: Value to set.
51+
'''
52+
with Session() as session:
53+
try:
54+
value = json.loads(value)
55+
print_pretty('Value converted to a dictionary.')
56+
except json.JSONDecodeError:
57+
pass
58+
try:
59+
data = session.EtcdConfig.set(key, value)
60+
except Exception as e:
61+
print_error(e)
62+
sys.exit(1)
63+
if data.get('result', False) != 'ok':
64+
print_fail('Unable to set key/value.')
65+
else:
66+
print_pretty('Successfully set key/value.')
67+
68+
69+
@etcd.command()
70+
@click.argument('key', type=str, metavar='KEY')
71+
@click.option('-p', '--prefix', is_flag=True, default=False,
72+
help='Delete all keys prefixed with the given key.')
73+
def delete(key, prefix):
74+
'''
75+
Delete key(s) from ETCD.
76+
77+
KEY: Name of ETCD key.
78+
'''
79+
with Session() as session:
80+
try:
81+
data = session.EtcdConfig.delete(key, prefix)
82+
except Exception as e:
83+
print_error(e)
84+
sys.exit(1)
85+
if data.get('result', False) != 'ok':
86+
print_fail('Unable to delete key/value.')
87+
else:
88+
print_pretty('Successfully deleted key/value.')

src/ai/backend/client/etcd.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from .base import api_function
2+
from .request import Request
3+
4+
__all__ = (
5+
'EtcdConfig',
6+
)
7+
8+
9+
class EtcdConfig:
10+
'''
11+
Provides a way to get or set ETCD configurations.
12+
13+
.. note::
14+
15+
All methods in this function class require your API access key to
16+
have the *superadmin* privilege.
17+
'''
18+
19+
session = None
20+
'''The client session instance that this function class is bound to.'''
21+
22+
@api_function
23+
@classmethod
24+
async def get(cls, key: str, prefix: bool = False) -> dict:
25+
'''
26+
Get configuration from ETCD with given key.
27+
28+
:param key: Name of the key to fetch.
29+
:param prefix: get all keys prefixed with the give key.
30+
'''
31+
rqst = Request(cls.session, 'POST', '/config/get')
32+
rqst.set_json({
33+
'key': key,
34+
'prefix': prefix,
35+
})
36+
async with rqst.fetch() as resp:
37+
data = await resp.json()
38+
return data.get('result', None)
39+
40+
@api_function
41+
@classmethod
42+
async def set(cls, key: str, value: str) -> dict:
43+
'''
44+
Set configuration into ETCD with given key and value.
45+
46+
:param key: Name of the key to set.
47+
:param value: Value to set.
48+
'''
49+
rqst = Request(cls.session, 'POST', '/config/set')
50+
rqst.set_json({
51+
'key': key,
52+
'value': value,
53+
})
54+
async with rqst.fetch() as resp:
55+
data = await resp.json()
56+
return data
57+
58+
@api_function
59+
@classmethod
60+
async def delete(cls, key: str, prefix: bool = False) -> dict:
61+
'''
62+
Delete configuration from ETCD with given key.
63+
64+
:param key: Name of the key to delete.
65+
:param prefix: delete all keys prefixed with the give key.
66+
'''
67+
rqst = Request(cls.session, 'POST', '/config/delete')
68+
rqst.set_json({
69+
'key': key,
70+
'prefix': prefix,
71+
})
72+
async with rqst.fetch() as resp:
73+
data = await resp.json()
74+
return data

src/ai/backend/client/session.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ class BaseSession(metaclass=abc.ABCMeta):
7272

7373
__slots__ = (
7474
'_config', '_closed', 'aiohttp_session',
75-
'Admin', 'Agent', 'AgentWathcer', 'Auth', 'Domain', 'Group', 'ScalingGroup',
76-
'Admin', 'Agent', 'AgentWatcher', 'Domain', 'Group', 'ScalingGroup',
77-
'Image', 'Kernel', 'KeyPair', 'Manager', 'Resource',
78-
'KeypairResourcePolicy', 'User', 'VFolder',
75+
'Manager', 'Admin',
76+
'Agent', 'AgentWatcher', 'ScalingGroup',
77+
'Image', 'Kernel',
78+
'Domain', 'Group', 'Auth', 'User', 'KeyPair',
79+
'EtcdConfig',
80+
'Resource', 'KeypairResourcePolicy',
81+
'VFolder',
7982
)
8083

8184
def __init__(self, *, config: APIConfig = None):
@@ -134,6 +137,7 @@ async def _create_aiohttp_session():
134137
from .admin import Admin
135138
from .agent import Agent, AgentWatcher
136139
from .auth import Auth
140+
from .etcd import EtcdConfig
137141
from .domain import Domain
138142
from .group import Group
139143
from .image import Image
@@ -182,6 +186,15 @@ async def _create_aiohttp_session():
182186
bound to this session.
183187
'''
184188

189+
self.EtcdConfig = type('EtcdConfig', (BaseFunction, ), {
190+
**EtcdConfig.__dict__,
191+
'session': self,
192+
})
193+
'''
194+
The :class:`~ai.backend.client.EtcdConfig` function proxy
195+
bound to this session.
196+
'''
197+
185198
self.Domain = type('Domain', (BaseFunction, ), {
186199
**Domain.__dict__,
187200
'session': self,
@@ -335,6 +348,7 @@ def __init__(self, *, config: APIConfig = None):
335348
from .admin import Admin
336349
from .agent import Agent, AgentWatcher
337350
from .auth import Auth
351+
from .etcd import EtcdConfig
338352
from .group import Group
339353
from .image import Image
340354
from .kernel import Kernel
@@ -382,6 +396,15 @@ def __init__(self, *, config: APIConfig = None):
382396
bound to this session.
383397
'''
384398

399+
self.EtcdConfig = type('EtcdConfig', (BaseFunction, ), {
400+
**EtcdConfig.__dict__,
401+
'session': self,
402+
})
403+
'''
404+
The :class:`~ai.backend.client.EtcdConfig` function proxy
405+
bound to this session.
406+
'''
407+
385408
self.Group = type('Group', (BaseFunction, ), {
386409
**Group.__dict__,
387410
'session': self,

0 commit comments

Comments
 (0)