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