#!/usr/bin/python # # Copyright (C) 2006, 2007 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. """Tool to do manual changes to the config file. """ # functions in this module need to have a given name structure, so: # pylint: disable=C0103 import optparse import cmd try: import readline _wd = readline.get_completer_delims() _wd = _wd.replace("-", "") readline.set_completer_delims(_wd) del _wd except ImportError: pass from ganeti import errors from ganeti import config from ganeti import objects class ConfigShell(cmd.Cmd): """Command tool for editing the config file. Note that although we don't do saves after remove, the current ConfigWriter code does that; so we can't prevent someone from actually breaking the config with this tool. It's the users' responsibility to know what they're doing. """ # all do_/complete_* functions follow the same API # pylint: disable=W0613 prompt = "(/) " def __init__(self, cfg_file=None): """Constructor for the ConfigShell object. The optional cfg_file argument will be used to load a config file at startup. """ cmd.Cmd.__init__(self) self.cfg = None self.parents = [] self.path = [] if cfg_file: self.do_load(cfg_file) self.postcmd(False, "") def emptyline(self): """Empty line handling. Note that the default will re-run the last command. We don't want that, and just ignore the empty line. """ return False @staticmethod def _get_entries(obj): """Computes the list of subdirs and files in the given object. This, depending on the passed object entry, look at each logical child of the object and decides if it's a container or a simple object. Based on this, it computes the list of subdir and files. """ dirs = [] entries = [] if isinstance(obj, objects.ConfigObject): for name in obj.GetAllSlots(): child = getattr(obj, name, None) if isinstance(child, (list, dict, tuple, objects.ConfigObject)): dirs.append(name) else: entries.append(name) elif isinstance(obj, (list, tuple)): for idx, child in enumerate(obj): if isinstance(child, (list, dict, tuple, objects.ConfigObject)): dirs.append(str(idx)) else: entries.append(str(idx)) elif isinstance(obj, dict): dirs = obj.keys() return dirs, entries def precmd(self, line): """Precmd hook to prevent commands in invalid states. This will prevent everything except load and quit when no configuration is loaded. """ if line.startswith("load") or line == "EOF" or line == "quit": return line if not self.parents or self.cfg is None: print "No config data loaded" return "" return line def postcmd(self, stop, line): """Postcmd hook to update the prompt. We show the current location in the prompt and this function is used to update it; this is only needed after cd and load, but we update it anyway. """ if self.cfg is None: self.prompt = "(#no config) " else: self.prompt = "(/%s) " % ("/".join(self.path),) return stop def do_load(self, line): """Load function. Syntax: load [/path/to/config/file] This will load a new configuration, discarding any existing data (if any). If no argument has been passed, it will use the default config file location. """ if line: arg = line else: arg = None try: self.cfg = config.ConfigWriter(cfg_file=arg, offline=True) self.parents = [self.cfg._config_data] # pylint: disable=W0212 self.path = [] except errors.ConfigurationError, err: print "Error: %s" % str(err) return False def do_ls(self, line): """List the current entry. This will show directories with a slash appended and files normally. """ dirs, entries = self._get_entries(self.parents[-1]) for i in dirs: print i + "/" for i in entries: print i return False def complete_cd(self, text, line, begidx, endidx): """Completion function for the cd command. """ pointer = self.parents[-1] dirs, _ = self._get_entries(pointer) matches = [str(name) for name in dirs if name.startswith(text)] return matches def do_cd(self, line): """Changes the current path. Valid arguments: either .., /, "" (no argument) or a child of the current object. """ if line == "..": if self.path: self.path.pop() self.parents.pop() return False else: print "Already at top level" return False elif len(line) == 0 or line == "/": self.parents = self.parents[0:1] self.path = [] return False pointer = self.parents[-1] dirs, _ = self._get_entries(pointer) if line not in dirs: print "No such child" return False if isinstance(pointer, (dict, list, tuple)): if isinstance(pointer, (list, tuple)): line = int(line) new_obj = pointer[line] else: new_obj = getattr(pointer, line) self.parents.append(new_obj) self.path.append(str(line)) return False def do_pwd(self, line): """Shows the current path. This duplicates the prompt functionality, but it's reasonable to have. """ print "/" + "/".join(self.path) return False def complete_cat(self, text, line, begidx, endidx): """Completion for the cat command. """ pointer = self.parents[-1] _, entries = self._get_entries(pointer) matches = [name for name in entries if name.startswith(text)] return matches def do_cat(self, line): """Shows the contents of the given file. This will display the contents of the given file, which must be a child of the current path (as shows by `ls`). """ pointer = self.parents[-1] _, entries = self._get_entries(pointer) if line not in entries: print "No such entry" return False if isinstance(pointer, (dict, list, tuple)): if isinstance(pointer, (list, tuple)): line = int(line) val = pointer[line] else: val = getattr(pointer, line) print val return False def do_verify(self, line): """Verify the configuration. This verifies the contents of the configuration file (and not the in-memory data, as every modify operation automatically saves the file). """ vdata = self.cfg.VerifyConfig() if vdata: print "Validation failed. Errors:" for text in vdata: print text return False def do_save(self, line): """Saves the configuration data. Note that is redundant (all modify operations automatically save the data), but it is good to use it as in the future that could change. """ if self.cfg.VerifyConfig(): print "Config data does not validate, refusing to save." return False self.cfg._WriteConfig() # pylint: disable=W0212 def do_rm(self, line): """Removes an instance or a node. This function works only on instances or nodes. You must be in either `/nodes` or `/instances` and give a valid argument. """ pointer = self.parents[-1] data = self.cfg._config_data # pylint: disable=W0212 if pointer not in (data.instances, data.nodes): print "Can only delete instances and nodes" return False if pointer == data.instances: if line in data.instances: self.cfg.RemoveInstance(line) else: print "Invalid instance name" else: if line in data.nodes: self.cfg.RemoveNode(line) else: print "Invalid node name" @staticmethod def do_EOF(line): """Exit the application. """ print return True @staticmethod def do_quit(line): """Exit the application. """ print return True class Error(Exception): """Generic exception""" pass def ParseOptions(): """Parses the command line options. In case of command line errors, it will show the usage and exit the program. @return: a tuple (options, args), as returned by OptionParser.parse_args """ parser = optparse.OptionParser() options, args = parser.parse_args() return options, args def main(): """Application entry point. """ _, args = ParseOptions() if args: cfg_file = args[0] else: cfg_file = None shell = ConfigShell(cfg_file=cfg_file) shell.cmdloop() if __name__ == "__main__": main()