root / tools / cfgshell @ dcedd81a
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=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 |
# all do_/complete_* functions follow the same API |
57 |
# pylint: disable=W0613 |
58 |
prompt = "(/) " |
59 |
|
60 |
def __init__(self, cfg_file=None): |
61 |
"""Constructor for the ConfigShell object. |
62 |
|
63 |
The optional cfg_file argument will be used to load a config file |
64 |
at startup. |
65 |
|
66 |
""" |
67 |
cmd.Cmd.__init__(self) |
68 |
self.cfg = None |
69 |
self.parents = [] |
70 |
self.path = [] |
71 |
if cfg_file: |
72 |
self.do_load(cfg_file) |
73 |
self.postcmd(False, "") |
74 |
|
75 |
def emptyline(self): |
76 |
"""Empty line handling. |
77 |
|
78 |
Note that the default will re-run the last command. We don't want |
79 |
that, and just ignore the empty line. |
80 |
|
81 |
""" |
82 |
return False |
83 |
|
84 |
@staticmethod |
85 |
def _get_entries(obj): |
86 |
"""Computes the list of subdirs and files in the given object. |
87 |
|
88 |
This, depending on the passed object entry, look at each logical |
89 |
child of the object and decides if it's a container or a simple |
90 |
object. Based on this, it computes the list of subdir and files. |
91 |
|
92 |
""" |
93 |
dirs = [] |
94 |
entries = [] |
95 |
if isinstance(obj, objects.ConfigObject): |
96 |
for name in obj.GetAllSlots(): |
97 |
child = getattr(obj, name, None) |
98 |
if isinstance(child, (list, dict, tuple, objects.ConfigObject)): |
99 |
dirs.append(name) |
100 |
else: |
101 |
entries.append(name) |
102 |
elif isinstance(obj, (list, tuple)): |
103 |
for idx, child in enumerate(obj): |
104 |
if isinstance(child, (list, dict, tuple, objects.ConfigObject)): |
105 |
dirs.append(str(idx)) |
106 |
else: |
107 |
entries.append(str(idx)) |
108 |
elif isinstance(obj, dict): |
109 |
dirs = obj.keys() |
110 |
|
111 |
return dirs, entries |
112 |
|
113 |
def precmd(self, line): |
114 |
"""Precmd hook to prevent commands in invalid states. |
115 |
|
116 |
This will prevent everything except load and quit when no |
117 |
configuration is loaded. |
118 |
|
119 |
""" |
120 |
if line.startswith("load") or line == "EOF" or line == "quit": |
121 |
return line |
122 |
if not self.parents or self.cfg is None: |
123 |
print "No config data loaded" |
124 |
return "" |
125 |
return line |
126 |
|
127 |
def postcmd(self, stop, line): |
128 |
"""Postcmd hook to update the prompt. |
129 |
|
130 |
We show the current location in the prompt and this function is |
131 |
used to update it; this is only needed after cd and load, but we |
132 |
update it anyway. |
133 |
|
134 |
""" |
135 |
if self.cfg is None: |
136 |
self.prompt = "(#no config) " |
137 |
else: |
138 |
self.prompt = "(/%s) " % ("/".join(self.path),) |
139 |
return stop |
140 |
|
141 |
def do_load(self, line): |
142 |
"""Load function. |
143 |
|
144 |
Syntax: load [/path/to/config/file] |
145 |
|
146 |
This will load a new configuration, discarding any existing data |
147 |
(if any). If no argument has been passed, it will use the default |
148 |
config file location. |
149 |
|
150 |
""" |
151 |
if line: |
152 |
arg = line |
153 |
else: |
154 |
arg = None |
155 |
try: |
156 |
self.cfg = config.ConfigWriter(cfg_file=arg, offline=True) |
157 |
self.parents = [self.cfg._config_data] # pylint: disable=W0212 |
158 |
self.path = [] |
159 |
except errors.ConfigurationError, err: |
160 |
print "Error: %s" % str(err) |
161 |
return False |
162 |
|
163 |
def do_ls(self, line): |
164 |
"""List the current entry. |
165 |
|
166 |
This will show directories with a slash appended and files |
167 |
normally. |
168 |
|
169 |
""" |
170 |
dirs, entries = self._get_entries(self.parents[-1]) |
171 |
for i in dirs: |
172 |
print i + "/" |
173 |
for i in entries: |
174 |
print i |
175 |
return False |
176 |
|
177 |
def complete_cd(self, text, line, begidx, endidx): |
178 |
"""Completion function for the cd command. |
179 |
|
180 |
""" |
181 |
pointer = self.parents[-1] |
182 |
dirs, _ = self._get_entries(pointer) |
183 |
matches = [str(name) for name in dirs if name.startswith(text)] |
184 |
return matches |
185 |
|
186 |
def do_cd(self, line): |
187 |
"""Changes the current path. |
188 |
|
189 |
Valid arguments: either .., /, "" (no argument) or a child of the current |
190 |
object. |
191 |
|
192 |
""" |
193 |
if line == "..": |
194 |
if self.path: |
195 |
self.path.pop() |
196 |
self.parents.pop() |
197 |
return False |
198 |
else: |
199 |
print "Already at top level" |
200 |
return False |
201 |
elif len(line) == 0 or line == "/": |
202 |
self.parents = self.parents[0:1] |
203 |
self.path = [] |
204 |
return False |
205 |
|
206 |
pointer = self.parents[-1] |
207 |
dirs, _ = self._get_entries(pointer) |
208 |
|
209 |
if line not in dirs: |
210 |
print "No such child" |
211 |
return False |
212 |
if isinstance(pointer, (dict, list, tuple)): |
213 |
if isinstance(pointer, (list, tuple)): |
214 |
line = int(line) |
215 |
new_obj = pointer[line] |
216 |
else: |
217 |
new_obj = getattr(pointer, line) |
218 |
self.parents.append(new_obj) |
219 |
self.path.append(str(line)) |
220 |
return False |
221 |
|
222 |
def do_pwd(self, line): |
223 |
"""Shows the current path. |
224 |
|
225 |
This duplicates the prompt functionality, but it's reasonable to |
226 |
have. |
227 |
|
228 |
""" |
229 |
print "/" + "/".join(self.path) |
230 |
return False |
231 |
|
232 |
def complete_cat(self, text, line, begidx, endidx): |
233 |
"""Completion for the cat command. |
234 |
|
235 |
""" |
236 |
pointer = self.parents[-1] |
237 |
_, entries = self._get_entries(pointer) |
238 |
matches = [name for name in entries if name.startswith(text)] |
239 |
return matches |
240 |
|
241 |
def do_cat(self, line): |
242 |
"""Shows the contents of the given file. |
243 |
|
244 |
This will display the contents of the given file, which must be a |
245 |
child of the current path (as shows by `ls`). |
246 |
|
247 |
""" |
248 |
pointer = self.parents[-1] |
249 |
_, entries = self._get_entries(pointer) |
250 |
if line not in entries: |
251 |
print "No such entry" |
252 |
return False |
253 |
|
254 |
if isinstance(pointer, (dict, list, tuple)): |
255 |
if isinstance(pointer, (list, tuple)): |
256 |
line = int(line) |
257 |
val = pointer[line] |
258 |
else: |
259 |
val = getattr(pointer, line) |
260 |
print val |
261 |
return False |
262 |
|
263 |
def do_verify(self, line): |
264 |
"""Verify the configuration. |
265 |
|
266 |
This verifies the contents of the configuration file (and not the |
267 |
in-memory data, as every modify operation automatically saves the |
268 |
file). |
269 |
|
270 |
""" |
271 |
vdata = self.cfg.VerifyConfig() |
272 |
if vdata: |
273 |
print "Validation failed. Errors:" |
274 |
for text in vdata: |
275 |
print text |
276 |
return False |
277 |
|
278 |
def do_save(self, line): |
279 |
"""Saves the configuration data. |
280 |
|
281 |
Note that is redundant (all modify operations automatically save |
282 |
the data), but it is good to use it as in the future that could |
283 |
change. |
284 |
|
285 |
""" |
286 |
if self.cfg.VerifyConfig(): |
287 |
print "Config data does not validate, refusing to save." |
288 |
return False |
289 |
self.cfg._WriteConfig() # pylint: disable=W0212 |
290 |
|
291 |
def do_rm(self, line): |
292 |
"""Removes an instance or a node. |
293 |
|
294 |
This function works only on instances or nodes. You must be in |
295 |
either `/nodes` or `/instances` and give a valid argument. |
296 |
|
297 |
""" |
298 |
pointer = self.parents[-1] |
299 |
data = self.cfg._config_data # pylint: disable=W0212 |
300 |
if pointer not in (data.instances, data.nodes): |
301 |
print "Can only delete instances and nodes" |
302 |
return False |
303 |
if pointer == data.instances: |
304 |
if line in data.instances: |
305 |
self.cfg.RemoveInstance(line) |
306 |
else: |
307 |
print "Invalid instance name" |
308 |
else: |
309 |
if line in data.nodes: |
310 |
self.cfg.RemoveNode(line) |
311 |
else: |
312 |
print "Invalid node name" |
313 |
|
314 |
@staticmethod |
315 |
def do_EOF(line): |
316 |
"""Exit the application. |
317 |
|
318 |
""" |
319 |
|
320 |
return True |
321 |
|
322 |
@staticmethod |
323 |
def do_quit(line): |
324 |
"""Exit the application. |
325 |
|
326 |
""" |
327 |
|
328 |
return True |
329 |
|
330 |
|
331 |
class Error(Exception): |
332 |
"""Generic exception""" |
333 |
pass |
334 |
|
335 |
|
336 |
def ParseOptions(): |
337 |
"""Parses the command line options. |
338 |
|
339 |
In case of command line errors, it will show the usage and exit the |
340 |
program. |
341 |
|
342 |
@return: a tuple (options, args), as returned by OptionParser.parse_args |
343 |
|
344 |
""" |
345 |
parser = optparse.OptionParser() |
346 |
|
347 |
options, args = parser.parse_args() |
348 |
|
349 |
return options, args |
350 |
|
351 |
|
352 |
def main(): |
353 |
"""Application entry point. |
354 |
|
355 |
""" |
356 |
_, args = ParseOptions() |
357 |
if args: |
358 |
cfg_file = args[0] |
359 |
else: |
360 |
cfg_file = None |
361 |
shell = ConfigShell(cfg_file=cfg_file) |
362 |
shell.cmdloop() |
363 |
|
364 |
|
365 |
if __name__ == "__main__": |
366 |
main() |