root / tools / cfgshell @ 2ec08468
History | View | Annotate | Download (8.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.cfg._OpenConfig() |
156 |
self.parents = [self.cfg._config_data] |
157 |
self.path = [] |
158 |
except errors.ConfigurationError, err: |
159 |
print "Error: %s" % str(err) |
160 |
return False |
161 |
|
162 |
def do_ls(self, line): |
163 |
"""List the current entry. |
164 |
|
165 |
This will show directories with a slash appended and files |
166 |
normally. |
167 |
|
168 |
""" |
169 |
dirs, entries = self._get_entries(self.parents[-1]) |
170 |
for i in dirs: |
171 |
print i + "/" |
172 |
for i in entries: |
173 |
print i |
174 |
return False |
175 |
|
176 |
def complete_cd(self, text, line, begidx, endidx): |
177 |
"""Completion function for the cd command. |
178 |
|
179 |
""" |
180 |
pointer = self.parents[-1] |
181 |
dirs, entries = self._get_entries(pointer) |
182 |
matches = [str(name) for name in dirs if name.startswith(text)] |
183 |
return matches |
184 |
|
185 |
def do_cd(self, line): |
186 |
"""Changes the current path. |
187 |
|
188 |
Valid arguments: either .., /, "" (no argument) or a child of the current |
189 |
object. |
190 |
|
191 |
""" |
192 |
if line == "..": |
193 |
if self.path: |
194 |
self.path.pop() |
195 |
self.parents.pop() |
196 |
return False |
197 |
else: |
198 |
print "Already at top level" |
199 |
return False |
200 |
elif len(line) == 0 or line == "/": |
201 |
self.parents = self.parents[0:1] |
202 |
self.path = [] |
203 |
return False |
204 |
|
205 |
pointer = self.parents[-1] |
206 |
dirs, entries = self._get_entries(pointer) |
207 |
|
208 |
if line not in dirs: |
209 |
print "No such child" |
210 |
return False |
211 |
if isinstance(pointer, (dict, list, tuple)): |
212 |
if isinstance(pointer, (list, tuple)): |
213 |
line = int(line) |
214 |
new_obj = pointer[line] |
215 |
else: |
216 |
new_obj = getattr(pointer, line) |
217 |
self.parents.append(new_obj) |
218 |
self.path.append(str(line)) |
219 |
return False |
220 |
|
221 |
def do_pwd(self, line): |
222 |
"""Shows the current path. |
223 |
|
224 |
This duplicates the prompt functionality, but it's reasonable to |
225 |
have. |
226 |
|
227 |
""" |
228 |
print "/" + "/".join(self.path) |
229 |
return False |
230 |
|
231 |
def complete_cat(self, text, line, begidx, endidx): |
232 |
"""Completion for the cat command. |
233 |
|
234 |
""" |
235 |
pointer = self.parents[-1] |
236 |
dirs, entries = self._get_entries(pointer) |
237 |
matches = [name for name in entries if name.startswith(text)] |
238 |
return matches |
239 |
|
240 |
def do_cat(self, line): |
241 |
"""Shows the contents of the given file. |
242 |
|
243 |
This will display the contents of the given file, which must be a |
244 |
child of the current path (as shows by `ls`). |
245 |
|
246 |
""" |
247 |
pointer = self.parents[-1] |
248 |
dirs, entries = self._get_entries(pointer) |
249 |
if line not in entries: |
250 |
print "No such entry" |
251 |
return False |
252 |
|
253 |
if isinstance(pointer, (dict, list, tuple)): |
254 |
if isinstance(pointer, (list, tuple)): |
255 |
line = int(line) |
256 |
val = pointer[line] |
257 |
else: |
258 |
val = getattr(pointer, line) |
259 |
print val |
260 |
return False |
261 |
|
262 |
def do_verify(self, line): |
263 |
"""Verify the configuration. |
264 |
|
265 |
This verifies the contents of the configuration file (and not the |
266 |
in-memory data, as every modify operation automatically saves the |
267 |
file). |
268 |
|
269 |
""" |
270 |
vdata = self.cfg.VerifyConfig() |
271 |
if vdata: |
272 |
print "Validation failed. Errors:" |
273 |
for text in vdata: |
274 |
print text |
275 |
return False |
276 |
|
277 |
def do_save(self, line): |
278 |
"""Saves the configuration data. |
279 |
|
280 |
Note that is redundant (all modify operations automatically save |
281 |
the data), but it is good to use it as in the future that could |
282 |
change. |
283 |
|
284 |
""" |
285 |
if self.cfg.VerifyConfig(): |
286 |
print "Config data does not validate, refusing to save." |
287 |
return False |
288 |
self.cfg._WriteConfig() |
289 |
|
290 |
def do_rm(self, line): |
291 |
"""Removes an instance or a node. |
292 |
|
293 |
This function works only on instances or nodes. You must be in |
294 |
either `/nodes` or `/instances` and give a valid argument. |
295 |
|
296 |
""" |
297 |
pointer = self.parents[-1] |
298 |
data = self.cfg._config_data |
299 |
if pointer not in (data.instances, data.nodes): |
300 |
print "Can only delete instances and nodes" |
301 |
return False |
302 |
if pointer == data.instances: |
303 |
if line in data.instances: |
304 |
self.cfg.RemoveInstance(line) |
305 |
else: |
306 |
print "Invalid instance name" |
307 |
else: |
308 |
if line in data.nodes: |
309 |
self.cfg.RemoveNode(line) |
310 |
else: |
311 |
print "Invalid node name" |
312 |
|
313 |
def do_EOF(self, line): |
314 |
"""Exit the application. |
315 |
|
316 |
""" |
317 |
|
318 |
return True |
319 |
|
320 |
def do_quit(self, line): |
321 |
"""Exit the application. |
322 |
|
323 |
""" |
324 |
|
325 |
return True |
326 |
|
327 |
|
328 |
class Error(Exception): |
329 |
"""Generic exception""" |
330 |
pass |
331 |
|
332 |
|
333 |
def ParseOptions(): |
334 |
"""Parses the command line options. |
335 |
|
336 |
In case of command line errors, it will show the usage and exit the |
337 |
program. |
338 |
|
339 |
Returns: |
340 |
(options, args), as returned by OptionParser.parse_args |
341 |
""" |
342 |
|
343 |
parser = optparse.OptionParser() |
344 |
|
345 |
options, args = parser.parse_args() |
346 |
|
347 |
return options, args |
348 |
|
349 |
|
350 |
def main(): |
351 |
"""Application entry point. |
352 |
|
353 |
This is just a wrapper over BootStrap, to handle our own exceptions. |
354 |
""" |
355 |
options, args = ParseOptions() |
356 |
if args: |
357 |
cfg_file = args[0] |
358 |
else: |
359 |
cfg_file = None |
360 |
shell = ConfigShell(cfg_file=cfg_file) |
361 |
shell.cmdloop() |
362 |
|
363 |
|
364 |
if __name__ == "__main__": |
365 |
main() |