Statistics
| Branch: | Tag: | Revision:

root / tools / cfgshell @ 18397489

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
    print
320
    return True
321

    
322
  @staticmethod
323
  def do_quit(line):
324
    """Exit the application.
325

    
326
    """
327
    print
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()