Implement “cd /” and “cd” to get to the root directory.
[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
27 import os
28 import sys
29 import optparse
30 import time
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 = self.cluster_name = 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:/%s) " % (self.cluster_name, "/".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       self.cluster_name = self.cfg.GetClusterName()
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, entries = 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, entries = 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     dirs, 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     dirs, 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()
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
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   def do_EOF(self, line):
315     print
316     return True
317
318   def do_quit(self, line):
319     """Exit the application.
320
321     """
322     print
323     return True
324
325
326 class Error(Exception):
327   """Generic exception"""
328   pass
329
330
331 def ParseOptions():
332   """Parses the command line options.
333
334   In case of command line errors, it will show the usage and exit the
335   program.
336
337   Returns:
338     (options, args), as returned by OptionParser.parse_args
339   """
340
341   parser = optparse.OptionParser()
342
343   options, args = parser.parse_args()
344
345   return options, args
346
347
348 def main():
349   """Application entry point.
350
351   This is just a wrapper over BootStrap, to handle our own exceptions.
352   """
353   options, args = ParseOptions()
354   if args:
355     cfg_file = args[0]
356   else:
357     cfg_file = None
358   shell = ConfigShell(cfg_file=cfg_file)
359   shell.cmdloop()
360
361
362 if __name__ == "__main__":
363   main()