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

Commit 2ececf2

Browse files
kyujin-choachimnol
andauthored
feat: dotfiles API and dotfiles CLI (#85)
* Implement functional API and CLI for dotfiles management * Update documentation * cli.admin.sessions: Fix a wrong reference to format_options Co-authored-by: Joongi Kim <[email protected]>
1 parent e5f632e commit 2ececf2

File tree

5 files changed

+281
-1
lines changed

5 files changed

+281
-1
lines changed

docs/cli/storage.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,18 @@ a normal directory.
9898
By reusing the same vfolder in subsequent sessions, you do not have to
9999
donwload the result and upload it as the input for next sessions, just
100100
keeping them in the storage.
101+
102+
103+
Creating default files for kernels
104+
----------------------------------
105+
106+
Backend.AI has a feature called 'dotfile', created to all the kernels
107+
user spawns. As you can guess, dotfile's path should start with ``.``.
108+
The following command creates dotfile named ``.aws/config``
109+
with permission `755`. This file will be created under ``/home/work``
110+
every time user spawns
111+
Backend.AI kernel.
112+
113+
.. code-block:: console
114+
115+
$ backend.ai dotfile create .aws/config < ~/.aws/config

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def _attach_command():
4747
from . import admin, config, app, files, logs, manager, proxy, ps, run # noqa
4848
from . import vfolder # noqa
4949
from . import session_template # noqa
50+
from . import dotfile # noqa
5051

5152

5253
_attach_command()

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

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import sys
2+
3+
import click
4+
from tabulate import tabulate
5+
6+
from . import AliasGroup, main
7+
from .pretty import print_info, print_warn, print_error
8+
from ..session import Session
9+
10+
11+
@main.group(cls=AliasGroup)
12+
def dotfile():
13+
'''Provides dotfile operations.'''
14+
15+
16+
@dotfile.command()
17+
@click.argument('path', metavar='PATH')
18+
@click.option('--perm', 'permission',
19+
help='Linux permission represented in octal number (e.g. 755) '
20+
'Defaults to 755 if not specified')
21+
@click.option('-f', '--file', 'dotfile_path',
22+
help='Path to dotfile to upload. '
23+
'If not specified, client will try to read file from STDIN. ')
24+
@click.option('-o', '--owner', '--owner-access-key', 'owner_access_key', metavar='ACCESS_KEY',
25+
help='Set the owner of the target session explicitly.')
26+
def create(path, permission, dotfile_path, owner_access_key):
27+
'''
28+
Store dotfile to Backend.AI Manager.
29+
Dotfiles will be automatically loaded when creating kernels.
30+
31+
PATH: Where dotfiles will be created when starting kernel
32+
'''
33+
34+
if dotfile_path:
35+
with open(dotfile_path, 'r') as fr:
36+
body = fr.read()
37+
else:
38+
body = ''
39+
for line in sys.stdin:
40+
body += (line + '\n')
41+
with Session() as session:
42+
try:
43+
if not permission:
44+
permission = '755'
45+
dotfile_ = session.Dotfile.create(body, path, permission,
46+
owner_access_key=owner_access_key)
47+
print_info(f'Dotfile {dotfile_.path} created and ready')
48+
except Exception as e:
49+
print_error(e)
50+
sys.exit(1)
51+
52+
53+
@dotfile.command()
54+
@click.argument('path', metavar='PATH')
55+
@click.option('-o', '--owner', '--owner-access-key', 'owner_access_key', metavar='ACCESS_KEY',
56+
help='Specify the owner of the target session explicitly.')
57+
def get(path, owner_access_key):
58+
'''
59+
Print dotfile content
60+
'''
61+
with Session() as session:
62+
try:
63+
dotfile_ = session.Dotfile(path, owner_access_key=owner_access_key)
64+
body = dotfile_.get()
65+
print(body['data'])
66+
except Exception as e:
67+
print_error(e)
68+
sys.exit(1)
69+
70+
71+
@dotfile.command()
72+
def list():
73+
'''
74+
List all availabe dotfiles by user.
75+
'''
76+
fields = [
77+
('Path', 'path', None),
78+
('Data', 'data', lambda v: v[:30].splitlines()[0]),
79+
('Permission', 'permission', None),
80+
]
81+
with Session() as session:
82+
try:
83+
resp = session.Dotfile.list_dotfiles()
84+
if not resp:
85+
print('There is no dotfiles created yet.')
86+
return
87+
rows = (
88+
tuple(
89+
item[key] if transform is None else transform(item[key])
90+
for _, key, transform in fields
91+
)
92+
for item in resp
93+
)
94+
hdrs = (display_name for display_name, _, _ in fields)
95+
print(tabulate(rows, hdrs))
96+
except Exception as e:
97+
print_error(e)
98+
sys.exit(1)
99+
100+
101+
@dotfile.command()
102+
@click.argument('path', metavar='PATH')
103+
@click.option('--perm', 'permission',
104+
help='Linux permission represented in octal number (e.g. 755) '
105+
'Defaults to 755 if not specified')
106+
@click.option('-f', '--file', 'dotfile_path',
107+
help='Path to dotfile to upload. '
108+
'If not specified, client will try to read file from STDIN. ')
109+
@click.option('-o', '--owner', '--owner-access-key', 'owner_access_key', metavar='ACCESS_KEY',
110+
help='Specify the owner of the target session explicitly.')
111+
def update(path, permission, dotfile_path, owner_access_key):
112+
'''
113+
Update dotfile stored in Backend.AI Manager.
114+
'''
115+
116+
if dotfile_path:
117+
with open(dotfile_path, 'r') as fr:
118+
body = fr.read()
119+
else:
120+
body = ''
121+
for line in sys.stdin:
122+
body += (line + '\n')
123+
with Session() as session:
124+
try:
125+
if not permission:
126+
permission = '755'
127+
dotfile_ = session.Dotfile(path, owner_access_key=owner_access_key)
128+
dotfile_.update(body, permission)
129+
print_info(f'Dotfile {dotfile_.path} updated')
130+
except Exception as e:
131+
print_error(e)
132+
sys.exit(1)
133+
134+
135+
@dotfile.command()
136+
@click.argument('path', metavar='PATH')
137+
@click.option('-f', '--force', type=bool, is_flag=True,
138+
help='Delete dotfile without confirmation.')
139+
@click.option('-o', '--owner', '--owner-access-key', 'owner_access_key', metavar='ACCESS_KEY',
140+
help='Specify the owner of the target session explicitly.')
141+
def delete(path, force, owner_access_key):
142+
'''
143+
Delete dotfile from Backend.AI Manager.
144+
'''
145+
with Session() as session:
146+
dotfile_ = session.Dotfile(path, owner_access_key=owner_access_key)
147+
if not force:
148+
print_warn('Are you sure? (y/[n])')
149+
result = input()
150+
if result.strip() != 'y':
151+
print_info('Aborting.')
152+
exit()
153+
try:
154+
dotfile_.delete()
155+
print_info(f'Dotfile {dotfile_.path} deleted')
156+
except Exception as e:
157+
print_error(e)
158+
sys.exit(1)

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from typing import List, Mapping
2+
3+
from .base import api_function
4+
from ..request import Request
5+
6+
7+
__all__ = (
8+
'Dotfile',
9+
)
10+
11+
12+
class Dotfile:
13+
14+
session = None
15+
@api_function
16+
@classmethod
17+
async def create(cls,
18+
data: str,
19+
path: str,
20+
permission: str,
21+
owner_access_key: str = None,
22+
) -> 'Dotfile':
23+
rqst = Request(cls.session,
24+
'POST', '/user-config/dotfiles')
25+
body = {
26+
'data': data,
27+
'path': path,
28+
'permission': permission
29+
}
30+
rqst.set_json(body)
31+
async with rqst.fetch() as resp:
32+
if resp.status == 200:
33+
await resp.json()
34+
return cls(path, owner_access_key=owner_access_key)
35+
36+
@api_function
37+
@classmethod
38+
async def list_dotfiles(cls) -> 'List[Mapping[str, str]]':
39+
rqst = Request(cls.session,
40+
'GET', '/user-config/dotfiles')
41+
async with rqst.fetch() as resp:
42+
if resp.status == 200:
43+
return await resp.json()
44+
45+
def __init__(self, path: str, owner_access_key: str = None):
46+
self.path = path
47+
self.owner_access_key = owner_access_key
48+
49+
@api_function
50+
async def get(self) -> str:
51+
params = {'path': self.path}
52+
if self.owner_access_key:
53+
params['owner_access_key'] = self.owner_access_key
54+
rqst = Request(self.session,
55+
'GET', f'/user-config/dotfiles',
56+
params=params)
57+
async with rqst.fetch() as resp:
58+
if resp.status == 200:
59+
return await resp.json()
60+
61+
@api_function
62+
async def update(self, data: str, permission: str):
63+
body = {
64+
'data': data,
65+
'path': self.path,
66+
'permission': permission
67+
}
68+
if self.owner_access_key:
69+
body['owner_access_key'] = self.owner_access_key
70+
rqst = Request(self.session,
71+
'PATCH', f'/user-config/dotfiles')
72+
rqst.set_json(body)
73+
74+
async with rqst.fetch() as resp:
75+
return await resp.json()
76+
77+
@api_function
78+
async def delete(self):
79+
params = {'path': self.path}
80+
if self.owner_access_key:
81+
params['owner_access_key'] = self.owner_access_key
82+
rqst = Request(self.session,
83+
'DELETE', f'/user-config/dotfiles',
84+
params=params)
85+
86+
async with rqst.fetch() as resp:
87+
return await resp.json()

src/ai/backend/client/session.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class BaseSession(metaclass=abc.ABCMeta):
115115
'Domain', 'Group', 'Auth', 'User', 'KeyPair',
116116
'EtcdConfig',
117117
'Resource', 'KeypairResourcePolicy',
118-
'VFolder',
118+
'VFolder', 'Dotfile'
119119
)
120120

121121
aiohttp_session: aiohttp.ClientSession
@@ -191,6 +191,7 @@ async def _create_aiohttp_session() -> aiohttp.ClientSession:
191191
from .func.session_template import SessionTemplate
192192
from .func.user import User
193193
from .func.vfolder import VFolder
194+
from .func.dotfile import Dotfile
194195

195196
self.System = type('System', (BaseFunction, ), {
196197
**System.__dict__,
@@ -354,6 +355,15 @@ async def _create_aiohttp_session() -> aiohttp.ClientSession:
354355
bound to this session.
355356
'''
356357

358+
self.Dotfile = type('Dotfile', (BaseFunction, ), {
359+
**Dotfile.__dict__,
360+
'session': self,
361+
})
362+
'''
363+
The :class:`~ai.backend.client.dotfile.Dotfile` function proxy
364+
bound to this session.
365+
'''
366+
357367
def close(self):
358368
'''
359369
Terminates the session. It schedules the ``close()`` coroutine
@@ -423,6 +433,7 @@ def __init__(self, *, config: APIConfig = None):
423433
from .func.session_template import SessionTemplate
424434
from .func.user import User
425435
from .func.vfolder import VFolder
436+
from .func.dotfile import Dotfile
426437

427438
self.System = type('System', (BaseFunction, ), {
428439
**System.__dict__,
@@ -576,6 +587,14 @@ def __init__(self, *, config: APIConfig = None):
576587
The :class:`~ai.backend.client.vfolder.VFolder` function proxy
577588
bound to this session.
578589
'''
590+
self.Dotfile = type('Dotfile', (BaseFunction, ), {
591+
**Dotfile.__dict__,
592+
'session': self,
593+
})
594+
'''
595+
The :class:`~ai.backend.client.dotfile.Dotfile` function proxy
596+
bound to this session.
597+
'''
579598

580599
async def close(self):
581600
if self._closed:

0 commit comments

Comments
 (0)