cfgshell 9.07 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/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.

"""

26
27
28
# functions in this module need to have a given name structure, so:
# pylint: disable-msg=C0103

Iustin Pop's avatar
Iustin Pop committed
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

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.

  """
56
57
  # all do_/complete_* functions follow the same API
  # pylint: disable-msg=W0613
Iustin Pop's avatar
Iustin Pop committed
58
59
60
61
62
63
64
65
66
67
  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)
68
    self.cfg = None
Iustin Pop's avatar
Iustin Pop committed
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
    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.__slots__:
        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:
138
      self.prompt = "(/%s) " % ("/".join(self.path),)
Iustin Pop's avatar
Iustin Pop committed
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
    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)
Iustin Pop's avatar
Iustin Pop committed
157
      self.parents = [self.cfg._config_data] # pylint: disable-msg=W0212
Iustin Pop's avatar
Iustin Pop committed
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
      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]
Iustin Pop's avatar
Iustin Pop committed
182
    dirs, _ = self._get_entries(pointer)
Iustin Pop's avatar
Iustin Pop committed
183
184
185
186
187
188
    matches = [str(name) for name in dirs if name.startswith(text)]
    return matches

  def do_cd(self, line):
    """Changes the current path.

189
190
    Valid arguments: either .., /, "" (no argument) or a child of the current
    object.
Iustin Pop's avatar
Iustin Pop committed
191
192
193
194
195
196
197
198
199
200

    """
    if line == "..":
      if self.path:
        self.path.pop()
        self.parents.pop()
        return False
      else:
        print "Already at top level"
        return False
201
202
203
204
    elif len(line) == 0 or line == "/":
      self.parents = self.parents[0:1]
      self.path = []
      return False
Iustin Pop's avatar
Iustin Pop committed
205
206

    pointer = self.parents[-1]
Iustin Pop's avatar
Iustin Pop committed
207
    dirs, _ = self._get_entries(pointer)
Iustin Pop's avatar
Iustin Pop committed
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

    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]
Iustin Pop's avatar
Iustin Pop committed
237
    _, entries = self._get_entries(pointer)
Iustin Pop's avatar
Iustin Pop committed
238
239
240
241
242
243
244
245
246
247
248
    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]
Iustin Pop's avatar
Iustin Pop committed
249
    _, entries = self._get_entries(pointer)
Iustin Pop's avatar
Iustin Pop committed
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
    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
Iustin Pop's avatar
Iustin Pop committed
289
    self.cfg._WriteConfig() # pylint: disable-msg=W0212
Iustin Pop's avatar
Iustin Pop committed
290
291
292
293
294
295
296
297
298

  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]
Iustin Pop's avatar
Iustin Pop committed
299
    data = self.cfg._config_data  # pylint: disable-msg=W0212
Iustin Pop's avatar
Iustin Pop committed
300
301
302
303
304
305
306
307
308
309
310
311
312
313
    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"

314
315
  @staticmethod
  def do_EOF(line):
316
317
318
    """Exit the application.

    """
Iustin Pop's avatar
Iustin Pop committed
319
320
321
    print
    return True

322
323
  @staticmethod
  def do_quit(line):
Iustin Pop's avatar
Iustin Pop committed
324
325
326
327
328
329
    """Exit the application.

    """
    print
    return True

330

Iustin Pop's avatar
Iustin Pop committed
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
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.

  Returns:
    (options, args), as returned by OptionParser.parse_args
  """

  parser = optparse.OptionParser()

  options, args = parser.parse_args()

  return options, args


def main():
  """Application entry point.

  This is just a wrapper over BootStrap, to handle our own exceptions.
  """
Iustin Pop's avatar
Iustin Pop committed
358
  _, args = ParseOptions()
Iustin Pop's avatar
Iustin Pop committed
359
360
361
362
363
364
365
366
367
368
  if args:
    cfg_file = args[0]
  else:
    cfg_file = None
  shell = ConfigShell(cfg_file=cfg_file)
  shell.cmdloop()


if __name__ == "__main__":
  main()