-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathconfig.py
339 lines (269 loc) · 11.7 KB
/
config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
"""Performs various configuration directories related commands."""
# Pycharm says this isn't available in python 3.9+. It is wrong.
import grp
import sys
import os
import shutil
import stat
import uuid
from pathlib import Path
from typing import Union
import yc_yaml as yaml
from pavilion import config
from pavilion import errors
from pavilion.output import fprint, draw_table
from pavilion.utils import get_login
from .base_classes import sub_cmd, Command
class ConfigCmdError(errors.PavilionError):
"""Raised for errors when creating configs."""
class ConfigCommand(Command):
"""Command plugin to perform various configuration directory tasks."""
def __init__(self):
super().__init__(
name='config',
description="Create or modify Pavilion configuration directories.",
short_help="Perform various configuration tasks.",
sub_commands=True)
def _setup_arguments(self, parser):
subparsers = parser.add_subparsers(
dest="sub_cmd",
help="Action to perform"
)
create_p = subparsers.add_parser(
'create',
help="Create a new configuration dir, sub-folders, and config file.",
description="Creates a new configuration directory at the given location with "
"the given label, and adds it to the root Pavilion config.")
create_p.add_argument(
'--working_dir',
help='Set the given path as the working directory for this config dir.')
create_p.add_argument(
'--group',
help="Set the group of the created directory to this group name, and set the "
"group sticky bit to the config dir and it's working dir (if given).")
create_p.add_argument(
'label',
help="The label for this config directory, to uniquely identify run tests.")
create_p.add_argument(
'path', type=Path,
help="Where to create the config directory.")
setup_p = subparsers.add_parser(
'setup',
help="Setup a root pavilion config directory, including a new pavilion.yaml file.",
description="As per 'create', except ignore any normally found pavilion.yaml and "
"create a new one in the the given location alongside the other created "
"files. Does not create a 'config.y"
)
setup_p.add_argument(
'--group',
help="Set the group of the created directory to this group name, and set the "
"group sticky bit to the config dir and it's working directory.")
setup_p.add_argument(
'path', type=Path,
help="Where to create the config directory.")
setup_p.add_argument(
'working_dir', type=Path,
help='Set the given path as general default working directory.')
add_p = subparsers.add_parser(
'add',
help="Add the given path as a config directory in the root pavilion.yaml.",
description="Add the given path as a config directory in the root pavilion.yaml.")
add_p.add_argument("path", type=Path,
help="Path to the config directory to add. It must exist.")
remove_p = subparsers.add_parser(
'remove',
help="Remove the given config dir path from the root pavilion.yaml",
description="Remove the config config dir path from the root pavilion.yaml. The "
"directory itself is not removed.")
remove_p.add_argument("config", help="Path or config label to remove.")
subparsers.add_parser(
'list',
help="List the paths to the config directories, their labels, and working_dirs.")
def run(self, pav_cfg, args):
"""Run the config command's chosen sub-command."""
pav_cfg_file = pav_cfg.pav_cfg_file
# This path is needed by (almost) all sub commands.
if pav_cfg_file is None and args.sub_cmd != 'setup':
fprint(sys.stderr,
"Main Pavilion config file path is missing. This would generally happen "
"when loading a pavilion.yaml manually (not through 'find_pavilion_config)")
return 1
return self._run_sub_command(pav_cfg, args)
@sub_cmd()
def _create_cmd(self, pav_cfg: config.PavConfig, args):
label = args.label
path: Path = args.path
path = path.resolve()
if args.group is not None:
try:
group = self.get_group(args.group)
except ConfigCmdError as err:
fprint(self.errfile, err)
return 1
else:
group = None
try:
self.create_config_dir(pav_cfg, path, label, group, args.working_dir)
except ConfigCmdError as err:
fprint(self.errfile, err)
return 1
return 0
@sub_cmd()
def _setup_cmd(self, pav_cfg: config.PavConfig, args):
"""Similar to the 'config create' command, but with the expectation that this will
be the primary pavilion config location."""
# The provided Pavilion config is ignored - we're creating a new one.
_ = pav_cfg
path: Path = args.path.resolve()
if args.group is not None:
try:
group = self.get_group(args.group)
except ConfigCmdError as err:
fprint(self.errfile, err)
return 1
else:
group = None
pav_cfg: config.PavConfig = config.PavilionConfigLoader().load_empty()
pav_cfg.working_dir = args.working_dir
pav_cfg.pav_cfg_file = path/'pavilion.yaml'
try:
self.create_config_dir(pav_cfg, path, 'main', group, working_dir=args.working_dir)
except ConfigCmdError as err:
fprint(self.errfile, err)
return self.write_pav_cfg(pav_cfg)
@staticmethod
def get_group(group_name) -> Union[grp.struct_group, None]:
"""Check the supplied group and return a group struct object.
:raises ValueError: On invalid groups names.
"""
user = get_login()
try:
group = grp.getgrnam(group_name)
except KeyError:
raise ConfigCmdError("Group '{}' does not exist.".format(group_name))
if user not in group.gr_mem:
raise ConfigCmdError("Current user '{}' is not in group '{}'."
.format(user, group_name))
return group
def create_config_dir(self, pav_cfg: config.PavConfig, path: Path,
label: str, group: Union[None, grp.struct_group],
working_dir: Path = None):
"""Create a standard Pavilion configuration directory at 'path',
saving a config.yaml with the given label."""
config_data = {
'label': label,
}
if not path.parent.exists():
raise ConfigCmdError("Parent directory '{}' does not exist.".format(path.parent))
if label in pav_cfg.configs:
raise ConfigCmdError("Given label '{}' already exists in the pav config."
.format(label))
try:
path.mkdir(parents=True, exist_ok=True)
except OSError as err:
raise ConfigCmdError("Could not create specified directory", err)
perms = 0o775
if group is not None:
# Mask out 'other' access.
perms = perms & 0o770
# Add the group sticky bit.
perms = perms | stat.S_ISGID
try:
os.chown(path, -1, group.gr_gid)
except OSError as err:
shutil.rmtree(path)
raise ConfigCmdError("Could not set config dir group to '{}'"
.format(group.gr_name), err)
try:
path.chmod(perms)
except OSError as err:
shutil.rmtree(path)
raise ConfigCmdError("Could not set permissions on config dir '{}'"
.format(path), err)
if working_dir is not None:
config_data['working_dir'] = str(working_dir)
if group is not None:
config_data['group'] = group.gr_name
config_file_path = path/'config.yaml'
try:
with (path/'config.yaml').open('w') as config_file:
yaml.dump(config_data, config_file)
except OSError as err:
shutil.rmtree(path)
raise ConfigCmdError("Error writing config file at '{}'"
.format(config_file_path), err)
for subdir in ('hosts', 'modes', 'os', 'plugins', 'collections', 'suites'):
subdir = path/subdir
try:
subdir.mkdir(exist_ok=True)
except OSError as err:
shutil.rmtree(path)
raise ConfigCmdError("Could not make config subdir '{}'".format(subdir), err)
# The working dir will be created automatically when Pavilion next runs.
pav_cfg.config_dirs.append(path)
return self.write_pav_cfg(pav_cfg)
@sub_cmd()
def _add_cmd(self, pav_cfg, args):
path: Path = args.path
path = path.resolve()
if not path.exists():
fprint(self.errfile, "Config path '{}' does not exist.".format(path))
return 1
pav_cfg['config_dirs'].append(path)
return self.write_pav_cfg(pav_cfg)
@staticmethod
def write_pav_cfg(pav_cfg):
"""Add the given config path (which should already exist) to the pavilion.yaml file."""
loader = config.PavilionConfigLoader()
pav_cfg_file = pav_cfg.pav_cfg_file
tmp_suffix = uuid.uuid4().hex[:10]
pav_cfg_file_tmp = pav_cfg_file.with_suffix(pav_cfg_file.suffix + '.' + tmp_suffix)
try:
with pav_cfg_file_tmp.open('w') as tmp_file:
loader.dump(tmp_file, values=pav_cfg)
pav_cfg_file_tmp.rename(pav_cfg_file)
except OSError as err:
fprint(sys.stderr,
"Failed to write pav config file at '{}'".format(pav_cfg_file), err)
if pav_cfg_file_tmp.exists():
try:
pav_cfg_file_tmp.unlink()
except OSError:
pass
return 1
return 0
@sub_cmd('rm')
def _remove_cmd(self, pav_cfg: config.PavConfig, args):
"""Remove the given config path from the pavilion config."""
if args.config in pav_cfg.configs:
path = pav_cfg.configs[args.config].path
else:
path: Path = Path(args.config).resolve()
resolved_dirs = {}
for config_dir in pav_cfg.config_dirs:
resolved_dirs[config_dir.resolve()] = config_dir
if path not in resolved_dirs:
fprint(self.errfile,
"Couldn't remove config dir '{}'. It was not in the list of known "
"configuration directories.".format(args.config))
fprint(self.errfile, "Known dirs:")
for conf_dir in pav_cfg.config_dirs:
fprint(self.errfile, ' {}'.format(conf_dir))
return 1
found_dir = resolved_dirs[path]
pav_cfg.config_dirs.remove(found_dir)
self.write_pav_cfg(pav_cfg)
@sub_cmd('ls')
def _list_cmd(self, pav_cfg: config.PavConfig, args):
_ = args
rows = []
for label, cfg in pav_cfg.configs.items():
cfg_data = {}
cfg_data.update(cfg)
cfg_data['label'] = label
rows.append(cfg_data)
draw_table(
outfile=self.outfile,
fields=['label', 'path', 'working_dir'],
rows=rows,
)