summaryrefslogtreecommitdiffstats
path: root/crmsh/main.py
blob: e03c07e1873b7937ab6014a3b2fec982dec96746 (plain)
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
26
27
28
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
56
57
58
59
60
61
62
63
64
65
66
67
68
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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
237
238
239
240
241
242
243
244
245
246
247
248
249
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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de>
# See COPYING for license information.

import sys
import os
import atexit
import random

from . import config
from . import options
from . import constants
from . import clidisplay
from . import term
from . import upgradeutil
from . import utils
from . import userdir

from . import ui_root
from . import ui_context
from . import log


logger = log.setup_logger(__name__)
logger_utils = log.LoggerUtils(logger)


random.seed()


def load_rc(context, rcfile):
    # only load the RC file if there is no new-style user config
    if config.has_user_config():
        return

    try:
        f = open(rcfile)
    except:
        return
    save_stdin = sys.stdin
    sys.stdin = f
    while True:
        inp = utils.multi_input()
        if inp is None:
            break
        try:
            if not context.run(inp):
                raise ValueError("Error in RC file: " + rcfile)
        except ValueError as e:
            logger.error(e, exc_info=e)
    f.close()
    sys.stdin = save_stdin


def exit_handler():
    '''
    Write the history file. Remove tmp files.
    '''
    if options.interactive and not options.batch:
        try:
            from readline import write_history_file
            write_history_file(userdir.HISTORY_FILE)
        except:
            pass


# prefer the user set PATH
def envsetup():
    path = os.environ["PATH"].split(':')
    # always add these dirs to PATH if they exist
    libexec_dirs = ('/usr/lib64', '/usr/libexec', '/usr/lib',
                    '/usr/local/lib64', '/usr/local/libexec', '/usr/local/lib')
    pacemaker_dirs = set("{}/pacemaker".format(d) for d in libexec_dirs)
    pacemaker_dirs.add(config.path.crm_daemon_dir)
    pacemaker_dirs.add(os.path.dirname(sys.argv[0]))
    for p in pacemaker_dirs:
        if p not in path and os.path.isdir(p):
            os.environ['PATH'] = "%s:%s" % (os.environ['PATH'], p)


# three modes: interactive (no args supplied), batch (input from
# a file), half-interactive (args supplied, but not batch)
def cib_prompt():
    shadow = utils.get_cib_in_use()
    if not shadow:
        return constants.live_cib_prompt
    if constants.tmp_cib:
        return constants.tmp_cib_prompt
    return shadow


def make_option_parser():
    from argparse import ArgumentParser, REMAINDER
    parser = ArgumentParser(prog='crm', usage="""%(prog)s [-h|--help] [OPTIONS] [SUBCOMMAND ARGS...]
or %(prog)s help SUBCOMMAND

For a list of available subcommands, use %(prog)s help.

Use %(prog)s without arguments for an interactive session.
Call a subcommand directly for a "single-shot" use.
Call %(prog)s with a level name as argument to start an interactive
session from that level.

See the crm(8) man page or call %(prog)s help for more details.""")
    parser.add_argument('--version', action='version', version="%(prog)s " + config.CRM_VERSION)
    parser.add_argument("-f", "--file", dest="filename", metavar="FILE",
                        help="Load commands from the given file. If a dash (-) " +
                        "is used in place of a file name, crm will read commands " +
                        "from the shell standard input (stdin).")
    parser.add_argument("-c", "--cib", dest="cib", metavar="CIB",
                        help="Start the session using the given shadow CIB file. " +
                        "Equivalent to `cib use <CIB>`.")
    parser.add_argument("-D", "--display", dest="display", metavar="OUTPUT_TYPE",
                        help="Choose one of the output options: plain, color-always, color, or uppercase. " +
                        "The default is color if the terminal emulation supports colors, " +
                        "else plain.")
    parser.add_argument("-F", "--force", action="store_true", default=False, dest="force",
                        help="Make crm proceed with applying changes where it would normally " +
                        "ask the user to confirm before proceeding. This option is mainly useful " +
                        "in scripts, and should be used with care.")
    parser.add_argument("-n", "--no", action="store_true", default=False, dest="ask_no",
                        help="Automatically answer no when prompted")
    parser.add_argument("-w", "--wait", action="store_true", default=False, dest="wait",
                        help="Make crm wait for the cluster transition to finish " +
                        "(for the changes to take effect) after each processed line.")
    parser.add_argument("-H", "--history", dest="history", metavar="DIR|FILE|SESSION",
                        help="A directory or file containing a cluster report to load " +
                        "into history, or the name of a previously saved history session.")
    parser.add_argument("-d", "--debug", action="store_true", default=False, dest="debug",
                        help="Print verbose debugging information.")
    parser.add_argument("-R", "--regression-tests", action="store_true", default=False,
                        dest="regression_tests",
                        help="Enables extra verbose trace logging used by the regression " +
                        "tests. Logs all external calls made by crmsh.")
    parser.add_argument("--scriptdir", dest="scriptdir", metavar="DIR",
                        help="Extra directory where crm looks for cluster scripts, or a list " +
                        "of directories separated by semi-colons (e.g. /dir1;/dir2;etc.).")
    parser.add_argument("-X", dest="profile", metavar="PROFILE",
                        help="Collect profiling data and save in PROFILE.")
    parser.add_argument("-o", "--opt", action="append", type=str, metavar="OPTION=VALUE",
                        help="Set crmsh option temporarily. If the options are saved using" +
                        "+options save+ then the value passed here will also be saved." +
                        "Multiple options can be set by using +-o+ multiple times.")
    parser.add_argument("SUBCOMMAND", nargs=REMAINDER)
    return parser


option_parser = make_option_parser()


def usage(rc):
    option_parser.print_usage(file=(sys.stderr if rc != 0 else sys.stdout))
    sys.exit(rc)


def set_interactive():
    '''Set the interactive option only if we're on a tty.'''
    if utils.can_ask():
        options.interactive = True


def add_quotes(args):
    '''
    Add quotes if there's whitespace in one of the
    arguments; so that the user doesn't need to protect the
    quotes.

    If there are two kinds of quotes which actually _survive_
    the getopt, then we're _probably_ screwed.

    At any rate, stuff like ... '..."..."'
    as well as '...\'...\''  do work.
    '''
    l = []
    for s in args:
        if config.core.add_quotes and ' ' in s:
            q = '"' in s and "'" or '"'
            if q not in s:
                s = "%s%s%s" % (q, s, q)
        l.append(s)
    return l


def handle_noninteractive_use(context, user_args):
    """
    returns: either a status code of 0 or 1, or
    None to indicate that nothing was done here.
    """
    if options.shadow:
        if not context.run("cib use " + options.shadow):
            return 1

    # this special case is silly, but we have to keep it to
    # preserve the backward compatibility
    if len(user_args) == 1 and user_args[0].startswith("conf"):
        if not context.run("configure"):
            return 1
    elif len(user_args) > 0:
        # we're not sure yet whether it's an interactive session or not
        # (single-shot commands aren't)
        logger_utils.reset_lineno()
        options.interactive = False

        l = add_quotes(user_args)
        if context.run(' '.join(l)):
            # if the user entered a level, then just continue
            if not context.previous_level():
                return 0
            set_interactive()
            if options.interactive:
                logger_utils.reset_lineno(-1)
        else:
            return 1
    return None


def render_prompt(context):
    rendered_prompt = constants.prompt
    if options.interactive and not options.batch:
        # TODO: fix how color interacts with readline,
        # seems the color prompt messes it up
        promptstr = "crm(%s/%s)%s# " % (cib_prompt(), utils.this_node(), context.prompt())
        constants.prompt = promptstr
        if clidisplay.colors_enabled():
            rendered_prompt = term.render(clidisplay.prompt(promptstr))
        else:
            rendered_prompt = promptstr
    return rendered_prompt


def setup_context(context):
    if options.input_file and options.input_file != "-":
        try:
            sys.stdin = open(options.input_file)
        except IOError as msg:
            logger.error(msg)
            usage(2)

    if options.interactive and not options.batch:
        context.setup_readline()


def main_input_loop(context, user_args):
    """
    Main input loop for crmsh. Parses input
    line by line.
    """
    rc = handle_noninteractive_use(context, user_args)
    if rc is not None:
        return rc

    setup_context(context)

    rc = 0
    while True:
        try:
            inp = utils.multi_input(render_prompt(context))
            if inp is None:
                if options.interactive:
                    rc = 0
                context.quit(rc)
            try:
                if not context.run(inp):
                    rc = 1
            except ValueError as e:
                rc = 1
                logger.error(e, exc_info=e)
        except KeyboardInterrupt:
            if options.interactive and not options.batch:
                print("Ctrl-C, leaving")
            context.quit(1)
        except Exception as e:
            logger.error(e, exc_info=e)
            context.quit(1)


def compgen():
    args = sys.argv[2:]
    if len(args) < 2:
        return

    options.shell_completion = True

    # point = int(args[0])
    line = args[1]

    # remove [*]crm from commandline
    idx = line.find('crm')
    if idx >= 0:
        line = line[idx+3:].lstrip()

    options.interactive = False
    ui = ui_root.Root()
    context = ui_context.Context(ui)
    last_word = line.rsplit(' ', 1)
    if len(last_word) > 1 and ':' in last_word[1]:
        idx = last_word[1].rfind(':')
        for w in context.complete(line):
            print(w[idx+1:])
    else:
        for w in context.complete(line):
            print(w)


def parse_options():
    opts, args = option_parser.parse_known_args()
    utils.check_empty_option_value(opts)
    config.core.debug = "yes" if opts.debug else config.core.debug
    options.profile = opts.profile or options.profile
    options.regression_tests = opts.regression_tests or options.regression_tests
    config.color.style = opts.display or config.color.style
    config.core.force = opts.force or config.core.force
    if opts.filename:
        logger_utils.reset_lineno()
        options.input_file, options.batch, options.interactive = opts.filename, True, False
    options.history = opts.history or options.history
    config.core.wait = opts.wait or config.core.wait
    options.shadow = opts.cib or options.shadow
    options.scriptdir = opts.scriptdir or options.scriptdir
    options.ask_no = opts.ask_no
    for opt in opts.opt or []:
        try:
            k, v = opt.split('=')
            s, n = k.split('.')
            config.set_option(s, n, v)
        except ValueError as e:
            raise ValueError("Expected -o <section>.<name>=<value>: %s" % (e))
    return opts.SUBCOMMAND


def profile_run(context, user_args):
    import cProfile
    cProfile.runctx('main_input_loop(context, user_args)',
                    globals(),
                    {'context': context, 'user_args': user_args},
                    filename=options.profile)
    # print how to use the profile file, but don't disturb
    # the regression tests
    if not options.regression_tests:
        stats_cmd = "; ".join(['import pstats',
                               's = pstats.Stats("%s")' % options.profile,
                               's.sort_stats("cumulative").print_stats()'])
        print("python -c '%s' | less" % (stats_cmd))
    return 0


def run():
    try:
        if len(sys.argv) >= 2 and sys.argv[1] == '--compgen':
            compgen()
            return 0
        envsetup()
        userdir.mv_user_files()

        ui = ui_root.Root()
        context = ui_context.Context(ui)

        load_rc(context, userdir.RC_FILE)
        atexit.register(exit_handler)
        options.interactive = utils.can_ask()
        if not options.interactive:
            logger_utils.reset_lineno()
            options.batch = True
        user_args = parse_options()
        if config.core.debug:
            logger.debug(utils.debug_timestamp())
        term.init()
        if options.profile:
            return profile_run(context, user_args)
        else:
            upgradeutil.upgrade_if_needed()
            return main_input_loop(context, user_args)
    except KeyboardInterrupt:
        if config.core.debug:
            raise
        else:
            print("Ctrl-C, leaving")
            sys.exit(1)
    except ValueError as e:
        logger.error(e, exc_info=e)
        sys.exit(1)
    except Exception as e:
        logger.error(e, exc_info=e)
        raise

# vim:ts=4:sw=4:et: