blob: 90b3dcf9b232753a5bfaa7ebaa7685285f1574bd [file] [log] [blame]
#!/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()