root / tools / cfgshell @ f4ad2ef0
History | View | Annotate | Download (9 kB)
1 |
#!/usr/bin/python |
---|---|
2 |
# |
3 |
|
4 |
# Copyright (C) 2006, 2007 Google Inc. |
5 |
# |
6 |
# This program is free software; you can redistribute it and/or modify |
7 |
# it under the terms of the GNU General Public License as published by |
8 |
# the Free Software Foundation; either version 2 of the License, or |
9 |
# (at your option) any later version. |
10 |
# |
11 |
# This program is distributed in the hope that it will be useful, but |
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 |
# General Public License for more details. |
15 |
# |
16 |
# You should have received a copy of the GNU General Public License |
17 |
# along with this program; if not, write to the Free Software |
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
19 |
# 02110-1301, USA. |
20 |
|
21 |
|
22 |
"""Tool to do manual changes to the config file. |
23 |
|
24 |
""" |
25 |
|
26 |
# functions in this module need to have a given name structure, so: |
27 |
# pylint: disable-msg=C0103 |
28 |
|
29 |
|
30 |
import optparse |
31 |
import cmd |
32 |
|
33 |
try: |
34 |
import readline |
35 |
_wd = readline.get_completer_delims() |
36 |
_wd = _wd.replace("-", "") |
37 |
readline.set_completer_delims(_wd) |
38 |
del _wd |
39 |
except ImportError: |
40 |
pass |
41 |
|
42 |
from ganeti import errors |
43 |
from ganeti import config |
44 |
from ganeti import objects |
45 |
|
46 |
|
47 |
class ConfigShell(cmd.Cmd): |
48 |
"""Command tool for editing the config file. |
49 |
|
50 |
Note that although we don't do saves after remove, the current |
51 |
ConfigWriter code does that; so we can't prevent someone from |
52 |
actually breaking the config with this tool. It's the users' |
53 |
responsibility to know what they're doing. |
54 |
|
55 |
""" |
56 |
prompt = "(/) " |
57 |
|
58 |
def __init__(self, cfg_file=None): |
59 |
"""Constructor for the ConfigShell object. |
60 |
|
61 |
The optional cfg_file argument will be used to load a config file |
62 |
at startup. |
63 |
|
64 |
""" |
65 |
cmd.Cmd.__init__(self) |
66 |
self.cfg = None |
67 |
self.parents = [] |
68 |
self.path = [] |
69 |
if cfg_file: |
70 |
self.do_load(cfg_file) |
71 |
self.postcmd(False, "") |
72 |
|
73 |
def emptyline(self): |
74 |
"""Empty line handling. |
75 |
|
76 |
Note that the default will re-run the last command. We don't want |
77 |
that, and just ignore the empty line. |
78 |
|
79 |
""" |
80 |
return False |
81 |
|
82 |
@staticmethod |
83 |
def _get_entries(obj): |
84 |
"""Computes the list of subdirs and files in the given object. |
85 |
|
86 |
This, depending on the passed object entry, look at each logical |
87 |
child of the object and decides if it's a container or a simple |
88 |
object. Based on this, it computes the list of subdir and files. |
89 |
|
90 |
""" |
91 |
dirs = [] |
92 |
entries = [] |
93 |
if isinstance(obj, objects.ConfigObject): |
94 |
for name in obj.__slots__: |
95 |
child = getattr(obj, name, None) |
96 |
if isinstance(child, (list, dict, tuple, objects.ConfigObject)): |
97 |
dirs.append(name) |
98 |
else: |
99 |
entries.append(name) |
100 |
elif isinstance(obj, (list, tuple)): |
101 |
for idx, child in enumerate(obj): |
102 |
if isinstance(child, (list, dict, tuple, objects.ConfigObject)): |
103 |
dirs.append(str(idx)) |
104 |
else: |
105 |
entries.append(str(idx)) |
106 |
elif isinstance(obj, dict): |
107 |
dirs = obj.keys() |
108 |
|
109 |
return dirs, entries |
110 |
|
111 |
def precmd(self, line): |
112 |
"""Precmd hook to prevent commands in invalid states. |
113 |
|
114 |
This will prevent everything except load and quit when no |
115 |
configuration is loaded. |
116 |
|
117 |
""" |
118 |
if line.startswith("load") or line == 'EOF' or line == "quit": |
119 |
return line |
120 |
if not self.parents or self.cfg is None: |
121 |
print "No config data loaded" |
122 |
return "" |
123 |
return line |
124 |
|
125 |
def postcmd(self, stop, line): |
126 |
"""Postcmd hook to update the prompt. |
127 |
|
128 |
We show the current location in the prompt and this function is |
129 |
used to update it; this is only needed after cd and load, but we |
130 |
update it anyway. |
131 |
|
132 |
""" |
133 |
if self.cfg is None: |
134 |
self.prompt = "(#no config) " |
135 |
else: |
136 |
self.prompt = "(/%s) " % ("/".join(self.path),) |
137 |
return stop |
138 |
|
139 |
def do_load(self, line): |
140 |
"""Load function. |
141 |
|
142 |
Syntax: load [/path/to/config/file] |
143 |
|
144 |
This will load a new configuration, discarding any existing data |
145 |
(if any). If no argument has been passed, it will use the default |
146 |
config file location. |
147 |
|
148 |
""" |
149 |
if line: |
150 |
arg = line |
151 |
else: |
152 |
arg = None |
153 |
try: |
154 |
self.cfg = config.ConfigWriter(cfg_file=arg, offline=True) |
155 |
self.parents = [self.cfg._config_data] # pylint: disable-msg=W0212 |
156 |
self.path = [] |
157 |
except errors.ConfigurationError, err: |
158 |
print "Error: %s" % str(err) |
159 |
return False |
160 |
|
161 |
def do_ls(self, line): |
162 |
"""List the current entry. |
163 |
|
164 |
This will show directories with a slash appended and files |
165 |
normally. |
166 |
|
167 |
""" |
168 |
dirs, entries = self._get_entries(self.parents[-1]) |
169 |
for i in dirs: |
170 |
print i + "/" |
171 |
for i in entries: |
172 |
print i |
173 |
return False |
174 |
|
175 |
def complete_cd(self, text, line, begidx, endidx): |
176 |
"""Completion function for the cd command. |
177 |
|
178 |
""" |
179 |
pointer = self.parents[-1] |
180 |
dirs, _ = self._get_entries(pointer) |
181 |
matches = [str(name) for name in dirs if name.startswith(text)] |
182 |
return matches |
183 |
|
184 |
def do_cd(self, line): |
185 |
"""Changes the current path. |
186 |
|
187 |
Valid arguments: either .., /, "" (no argument) or a child of the current |
188 |
object. |
189 |
|
190 |
""" |
191 |
if line == "..": |
192 |
if self.path: |
193 |
self.path.pop() |
194 |
self.parents.pop() |
195 |
return False |
196 |
else: |
197 |
print "Already at top level" |
198 |
return False |
199 |
elif len(line) == 0 or line == "/": |
200 |
self.parents = self.parents[0:1] |
201 |
self.path = [] |
202 |
return False |
203 |
|
204 |
pointer = self.parents[-1] |
205 |
dirs, _ = self._get_entries(pointer) |
206 |
|
207 |
if line not in dirs: |
208 |
print "No such child" |
209 |
return False |
210 |
if isinstance(pointer, (dict, list, tuple)): |
211 |
if isinstance(pointer, (list, tuple)): |
212 |
line = int(line) |
213 |
new_obj = pointer[line] |
214 |
else: |
215 |
new_obj = getattr(pointer, line) |
216 |
self.parents.append(new_obj) |
217 |
self.path.append(str(line)) |
218 |
return False |
219 |
|
220 |
def do_pwd(self, line): |
221 |
"""Shows the current path. |
222 |
|
223 |
This duplicates the prompt functionality, but it's reasonable to |
224 |
have. |
225 |
|
226 |
""" |
227 |
print "/" + "/".join(self.path) |
228 |
return False |
229 |
|
230 |
def complete_cat(self, text, line, begidx, endidx): |
231 |
"""Completion for the cat command. |
232 |
|
233 |
""" |
234 |
pointer = self.parents[-1] |
235 |
_, entries = self._get_entries(pointer) |
236 |
matches = [name for name in entries if name.startswith(text)] |
237 |
return matches |
238 |
|
239 |
def do_cat(self, line): |
240 |
"""Shows the contents of the given file. |
241 |
|
242 |
This will display the contents of the given file, which must be a |
243 |
child of the current path (as shows by `ls`). |
244 |
|
245 |
""" |
246 |
pointer = self.parents[-1] |
247 |
_, entries = self._get_entries(pointer) |
248 |
if line not in entries: |
249 |
print "No such entry" |
250 |
return False |
251 |
|
252 |
if isinstance(pointer, (dict, list, tuple)): |
253 |
if isinstance(pointer, (list, tuple)): |
254 |
line = int(line) |
255 |
val = pointer[line] |
256 |
else: |
257 |
val = getattr(pointer, line) |
258 |
print val |
259 |
return False |
260 |
|
261 |
def do_verify(self, line): |
262 |
"""Verify the configuration. |
263 |
|
264 |
This verifies the contents of the configuration file (and not the |
265 |
in-memory data, as every modify operation automatically saves the |
266 |
file). |
267 |
|
268 |
""" |
269 |
vdata = self.cfg.VerifyConfig() |
270 |
if vdata: |
271 |
print "Validation failed. Errors:" |
272 |
for text in vdata: |
273 |
print text |
274 |
return False |
275 |
|
276 |
def do_save(self, line): |
277 |
"""Saves the configuration data. |
278 |
|
279 |
Note that is redundant (all modify operations automatically save |
280 |
the data), but it is good to use it as in the future that could |
281 |
change. |
282 |
|
283 |
""" |
284 |
if self.cfg.VerifyConfig(): |
285 |
print "Config data does not validate, refusing to save." |
286 |
return False |
287 |
self.cfg._WriteConfig() # pylint: disable-msg=W0212 |
288 |
|
289 |
def do_rm(self, line): |
290 |
"""Removes an instance or a node. |
291 |
|
292 |
This function works only on instances or nodes. You must be in |
293 |
either `/nodes` or `/instances` and give a valid argument. |
294 |
|
295 |
""" |
296 |
pointer = self.parents[-1] |
297 |
data = self.cfg._config_data # pylint: disable-msg=W0212 |
298 |
if pointer not in (data.instances, data.nodes): |
299 |
print "Can only delete instances and nodes" |
300 |
return False |
301 |
if pointer == data.instances: |
302 |
if line in data.instances: |
303 |
self.cfg.RemoveInstance(line) |
304 |
else: |
305 |
print "Invalid instance name" |
306 |
else: |
307 |
if line in data.nodes: |
308 |
self.cfg.RemoveNode(line) |
309 |
else: |
310 |
print "Invalid node name" |
311 |
|
312 |
def do_EOF(self, line): |
313 |
"""Exit the application. |
314 |
|
315 |
""" |
316 |
|
317 |
return True |
318 |
|
319 |
def do_quit(self, line): |
320 |
"""Exit the application. |
321 |
|
322 |
""" |
323 |
|
324 |
return True |
325 |
|
326 |
|
327 |
class Error(Exception): |
328 |
"""Generic exception""" |
329 |
pass |
330 |
|
331 |
|
332 |
def ParseOptions(): |
333 |
"""Parses the command line options. |
334 |
|
335 |
In case of command line errors, it will show the usage and exit the |
336 |
program. |
337 |
|
338 |
Returns: |
339 |
(options, args), as returned by OptionParser.parse_args |
340 |
""" |
341 |
|
342 |
parser = optparse.OptionParser() |
343 |
|
344 |
options, args = parser.parse_args() |
345 |
|
346 |
return options, args |
347 |
|
348 |
|
349 |
def main(): |
350 |
"""Application entry point. |
351 |
|
352 |
This is just a wrapper over BootStrap, to handle our own exceptions. |
353 |
""" |
354 |
_, args = ParseOptions() |
355 |
if args: |
356 |
cfg_file = args[0] |
357 |
else: |
358 |
cfg_file = None |
359 |
shell = ConfigShell(cfg_file=cfg_file) |
360 |
shell.cmdloop() |
361 |
|
362 |
|
363 |
if __name__ == "__main__": |
364 |
main() |