• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pronovic / cedar-backup3 / 17226347256

26 Aug 2025 02:41AM UTC coverage: 73.311%. Remained the same
17226347256

push

github

web-flow
Replace Pylint with Ruff linter (#52)

27 of 46 new or added lines in 20 files covered. (58.7%)

2 existing lines in 1 file now uncovered.

7922 of 10806 relevant lines covered (73.31%)

2.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.38
/src/CedarBackup3/cli.py
1
# -*- coding: utf-8 -*-
2
# vim: set ft=python ts=4 sw=4 expandtab:
3
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
4
#
5
#              C E D A R
6
#          S O L U T I O N S       "Software done right."
7
#           S O F T W A R E
8
#
9
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
10
#
11
# Copyright (c) 2004-2007,2010,2015 Kenneth J. Pronovici.
12
# All rights reserved.
13
#
14
# This program is free software; you can redistribute it and/or
15
# modify it under the terms of the GNU General Public License,
16
# Version 2, as published by the Free Software Foundation.
17
#
18
# This program is distributed in the hope that it will be useful,
19
# but WITHOUT ANY WARRANTY; without even the implied warranty of
20
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
21
#
22
# Copies of the GNU General Public License are available from
23
# the Free Software Foundation website, http://www.gnu.org/.
24
#
25
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
26
#
27
# Author   : Kenneth J. Pronovici <pronovic@ieee.org>
28
# Language : Python 3
29
# Project  : Cedar Backup, release 3
30
# Purpose  : Provides command-line interface implementation.
31
#
32
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
33

34
########################################################################
35
# Module documentation
36
########################################################################
37

38
"""
39
Provides command-line interface implementation for the cback3 script.
40

41
Summary
42
=======
43

44
   The functionality in this module encapsulates the command-line interface for
45
   the cback3 script.  The cback3 script itself is very short, basically just an
46
   invokation of one function implemented here.  That, in turn, makes it
47
   simpler to validate the command line interface (for instance, it's easier to
48
   run pychecker against a module, and unit tests are easier, too).
49

50
   The objects and functions implemented in this module are probably not useful
51
   to any code external to Cedar Backup.   Anyone else implementing their own
52
   command-line interface would have to reimplement (or at least enhance) all
53
   of this anyway.
54

55
Backwards Compatibility
56
=======================
57

58
   The command line interface has changed between Cedar Backup 1.x and Cedar
59
   Backup 2.x.  Some new switches have been added, and the actions have become
60
   simple arguments rather than switches (which is a much more standard command
61
   line format).  Old 1.x command lines are generally no longer valid.
62

63
Module Attributes
64
=================
65

66
Attributes:
67
   DEFAULT_CONFIG: The default configuration file
68
   DEFAULT_LOGFILE: The default log file path
69
   DEFAULT_OWNERSHIP: Default ownership for the logfile
70
   DEFAULT_MODE: Default file permissions mode on the logfile
71
   VALID_ACTIONS: List of valid actions
72
   COMBINE_ACTIONS: List of actions which can be combined with other actions
73
   NONCOMBINE_ACTIONS: List of actions which cannot be combined with other actions
74

75
:author: Kenneth J. Pronovici <pronovic@ieee.org>
76
"""
77

78
########################################################################
79
# Imported modules
80
########################################################################
81

82
# Note: getopt is "soft deprecated" only and is safe to use; see: https://github.com/python/cpython/pull/105735
83

84
import getopt
4✔
85
import logging
4✔
86
import os
4✔
87
import sys
4✔
88
from functools import total_ordering
4✔
89

90
from CedarBackup3.actions.collect import executeCollect
4✔
91
from CedarBackup3.actions.initialize import executeInitialize
4✔
92
from CedarBackup3.actions.purge import executePurge
4✔
93
from CedarBackup3.actions.rebuild import executeRebuild
4✔
94
from CedarBackup3.actions.stage import executeStage
4✔
95
from CedarBackup3.actions.store import executeStore
4✔
96
from CedarBackup3.actions.validate import executeValidate
4✔
97
from CedarBackup3.config import Config
4✔
98
from CedarBackup3.customize import customizeOverrides
4✔
99
from CedarBackup3.peer import RemotePeer
4✔
100
from CedarBackup3.release import AUTHOR, EMAIL, VERSION
4✔
101
from CedarBackup3.util import (
4✔
102
    Diagnostics,
103
    DirectedGraph,
104
    PathResolverSingleton,
105
    encodePath,
106
    executeCommand,
107
    getFunctionReference,
108
    getUidGid,
109
    sortDict,
110
    splitCommandLine,
111
)
112

113
########################################################################
114
# Module-wide constants and variables
115
########################################################################
116

117
logger = logging.getLogger("CedarBackup3.log.cli")
4✔
118

119
DISK_LOG_FORMAT = "%(asctime)s --> [%(levelname)-7s] %(message)s"
4✔
120
DISK_OUTPUT_FORMAT = "%(message)s"
4✔
121
SCREEN_LOG_FORMAT = "%(message)s"
4✔
122
SCREEN_LOG_STREAM = sys.stdout
4✔
123
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S %Z"
4✔
124

125
DEFAULT_CONFIG = "/etc/cback3.conf"
4✔
126
DEFAULT_LOGFILE = "/var/log/cback3.log"
4✔
127
DEFAULT_OWNERSHIP = ["root", "adm"]
4✔
128
DEFAULT_MODE = 0o640
4✔
129

130
REBUILD_INDEX = 0  # can't run with anything else, anyway
4✔
131
VALIDATE_INDEX = 0  # can't run with anything else, anyway
4✔
132
INITIALIZE_INDEX = 0  # can't run with anything else, anyway
4✔
133
COLLECT_INDEX = 100
4✔
134
STAGE_INDEX = 200
4✔
135
STORE_INDEX = 300
4✔
136
PURGE_INDEX = 400
4✔
137

138
VALID_ACTIONS = ["collect", "stage", "store", "purge", "rebuild", "validate", "initialize", "all"]
4✔
139
COMBINE_ACTIONS = ["collect", "stage", "store", "purge"]
4✔
140
NONCOMBINE_ACTIONS = ["rebuild", "validate", "initialize", "all"]
4✔
141

142
SHORT_SWITCHES = "hVbqc:fMNl:o:m:OdsD"
4✔
143
LONG_SWITCHES = [
4✔
144
    "help",
145
    "version",
146
    "verbose",
147
    "quiet",
148
    "config=",
149
    "full",
150
    "managed",
151
    "managed-only",
152
    "logfile=",
153
    "owner=",
154
    "mode=",
155
    "output",
156
    "debug",
157
    "stack",
158
    "diagnostics",
159
]
160

161

162
#######################################################################
163
# Public functions
164
#######################################################################
165

166
#################
167
# cli() function
168
#################
169

170

171
def cli():
4✔
172
    """
173
    Implements the command-line interface for the ``cback3`` script.
174

175
    Essentially, this is the "main routine" for the cback3 script.  It does all
176
    of the argument processing for the script, and then sets about executing the
177
    indicated actions.
178

179
    As a general rule, only the actions indicated on the command line will be
180
    executed.   We will accept any of the built-in actions and any of the
181
    configured extended actions (which makes action list verification a two-
182
    step process).
183

184
    The ``'all'`` action has a special meaning: it means that the built-in set of
185
    actions (collect, stage, store, purge) will all be executed, in that order.
186
    Extended actions will be ignored as part of the ``'all'`` action.
187

188
    Raised exceptions always result in an immediate return.  Otherwise, we
189
    generally return when all specified actions have been completed.  Actions
190
    are ignored if the help, version or validate flags are set.
191

192
    A different error code is returned for each type of failure:
193

194
       - ``1``: The Python interpreter version is not supported
195
       - ``2``: Error processing command-line arguments
196
       - ``3``: Error configuring logging
197
       - ``4``: Error parsing indicated configuration file
198
       - ``5``: Backup was interrupted with a CTRL-C or similar
199
       - ``6``: Error executing specified backup actions
200

201
    *Note:* This function contains a good amount of logging at the INFO level,
202
    because this is the right place to document high-level flow of control (i.e.
203
    what the command-line options were, what config file was being used, etc.)
204

205
    *Note:* We assume that anything that *must* be seen on the screen is logged
206
    at the ERROR level.  Errors that occur before logging can be configured are
207
    written to ``sys.stderr``.
208

209
    Returns:
210
        Error code as described above
211
    """
212
    try:
×
213
        if list(map(int, [sys.version_info[0], sys.version_info[1]])) < [3, 8]:
×
214
            sys.stderr.write("Python 3 version 3.8 or greater required.\n")
×
215
            return 1
×
216
    except:
×
217
        # sys.version_info isn't available before 2.0
218
        sys.stderr.write("Python 3 version 3.8 or greater required.\n")
×
219
        return 1
×
220

221
    try:
×
222
        options = Options(argumentList=sys.argv[1:])
×
223
        logger.info("Specified command-line actions: %s", options.actions)
×
224
    except Exception as e:
×
225
        _usage()
×
226
        sys.stderr.write(" *** Error: %s\n" % e)
×
227
        return 2
×
228

229
    if options.help:
×
230
        _usage()
×
231
        return 0
×
232
    if options.version:
×
233
        _version()
×
234
        return 0
×
235
    if options.diagnostics:
×
236
        _diagnostics()
×
237
        return 0
×
238

239
    if options.stacktrace:
×
240
        logfile = setupLogging(options)
×
241
    else:
242
        try:
×
243
            logfile = setupLogging(options)
×
244
        except Exception as e:
×
245
            sys.stderr.write("Error setting up logging: %s\n" % e)
×
246
            return 3
×
247

248
    logger.info("Cedar Backup run started.")
×
249
    logger.info("Options were [%s]", options)
×
250
    logger.info("Logfile is [%s]", logfile)
×
251
    Diagnostics().logDiagnostics(method=logger.info)
×
252

253
    if options.config is None:
×
254
        logger.debug("Using default configuration file.")
×
255
        configPath = DEFAULT_CONFIG
×
256
    else:
257
        logger.debug("Using user-supplied configuration file.")
×
258
        configPath = options.config
×
259

260
    executeLocal = True
×
261
    executeManaged = False
×
262
    if options.managedOnly:
×
263
        executeLocal = False
×
264
        executeManaged = True
×
265
    if options.managed:
×
266
        executeManaged = True
×
267
    logger.debug("Execute local actions: %s", executeLocal)
×
268
    logger.debug("Execute managed actions: %s", executeManaged)
×
269

270
    try:
×
271
        logger.info("Configuration path is [%s]", configPath)
×
272
        config = Config(xmlPath=configPath)
×
273
        customizeOverrides(config)
×
274
        setupPathResolver(config)
×
275
        actionSet = _ActionSet(options.actions, config.extensions, config.options, config.peers, executeManaged, executeLocal)
×
276
    except Exception as e:
×
277
        logger.error("Error reading or handling configuration: %s", e)
×
278
        logger.info("Cedar Backup run completed with status 4.")
×
279
        return 4
×
280

281
    if options.stacktrace:
×
282
        actionSet.executeActions(configPath, options, config)
×
283
    else:
284
        try:
×
285
            actionSet.executeActions(configPath, options, config)
×
286
        except KeyboardInterrupt:
×
287
            logger.error("Backup interrupted.")
×
288
            logger.info("Cedar Backup run completed with status 5.")
×
289
            return 5
×
290
        except Exception as e:
×
291
            logger.error("Error executing backup: %s", e)
×
292
            logger.info("Cedar Backup run completed with status 6.")
×
293
            return 6
×
294

295
    logger.info("Cedar Backup run completed with status 0.")
×
296
    return 0
×
297

298

299
########################################################################
300
# Action-related class definition
301
########################################################################
302

303
####################
304
# _ActionItem class
305
####################
306

307

308
@total_ordering
4✔
309
class _ActionItem(object):
4✔
310
    """
311
    Class representing a single action to be executed.
312

313
    This class represents a single named action to be executed, and understands
314
    how to execute that action.
315

316
    The built-in actions will use only the options and config values.  We also
317
    pass in the config path so that extension modules can re-parse configuration
318
    if they want to, to add in extra information.
319

320
    This class is also where pre-action and post-action hooks are executed.  An
321
    action item is instantiated in terms of optional pre- and post-action hook
322
    objects (config.ActionHook), which are then executed at the appropriate time
323
    (if set).
324

325
    *Note:* The comparison operators for this class have been implemented to only
326
    compare based on the index and SORT_ORDER value, and ignore all other
327
    values.  This is so that the action set list can be easily sorted first by
328
    type (_ActionItem before _ManagedActionItem) and then by index within type.
329

330
    Attributes:
331
       SORT_ORDER: Defines a sort order to order properly between types
332
    """
333

334
    SORT_ORDER = 0
4✔
335

336
    def __init__(self, index, name, preHooks, postHooks, function):
4✔
337
        """
338
        Default constructor.
339

340
        It's OK to pass ``None`` for ``index``, ``preHooks`` or ``postHooks``, but not
341
        for ``name``.
342

343
        Args:
344
           index: Index of the item (or ``None``)
345
           name: Name of the action that is being executed
346
           preHooks: List of pre-action hooks in terms of an ``ActionHook`` object, or ``None``
347
           postHooks: List of post-action hooks in terms of an ``ActionHook`` object, or ``None``
348
           function: Reference to function associated with item
349
        """
350
        self.index = index
4✔
351
        self.name = name
4✔
352
        self.preHooks = preHooks
4✔
353
        self.postHooks = postHooks
4✔
354
        self.function = function
4✔
355

356
    def __eq__(self, other):
4✔
357
        """Equals operator, implemented in terms of original Python 2 compare operator."""
358
        return self.__cmp__(other) == 0
×
359

360
    def __lt__(self, other):
4✔
361
        """Less-than operator, implemented in terms of original Python 2 compare operator."""
362
        return self.__cmp__(other) < 0
4✔
363

364
    def __gt__(self, other):
4✔
365
        """Greater-than operator, implemented in terms of original Python 2 compare operator."""
366
        return self.__cmp__(other) > 0
×
367

368
    def __cmp__(self, other):
4✔
369
        """
370
        Original Python 2 comparison operator.
371
        The only thing we compare is the item's index.
372
        Args:
373
           other: Other object to compare to
374
        Returns:
375
            -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
376
        """
377
        if other is None:
4✔
378
            return 1
×
379
        if self.index != other.index:
4✔
380
            if int(self.index or 0) < int(other.index or 0):
4✔
381
                return -1
4✔
382
            else:
383
                return 1
4✔
384
        else:
385
            if self.SORT_ORDER != other.SORT_ORDER:
4✔
386
                if int(self.SORT_ORDER or 0) < int(other.SORT_ORDER or 0):
1✔
387
                    return -1
1✔
388
                else:
389
                    return 1
×
390
        return 0
4✔
391

392
    def executeAction(self, configPath, options, config):
4✔
393
        """
394
        Executes the action associated with an item, including hooks.
395

396
        See class notes for more details on how the action is executed.
397

398
        Args:
399
           configPath: Path to configuration file on disk
400
           options: Command-line options to be passed to action
401
           config: Parsed configuration to be passed to action
402

403
        Raises:
404
           Exception: If there is a problem executing the action
405
        """
406
        logger.debug("Executing [%s] action.", self.name)
×
407
        if self.preHooks is not None:
×
408
            for hook in self.preHooks:
×
409
                self._executeHook("pre-action", hook)
×
410
        self._executeAction(configPath, options, config)
×
411
        if self.postHooks is not None:
×
412
            for hook in self.postHooks:
×
413
                self._executeHook("post-action", hook)
×
414

415
    def _executeAction(self, configPath, options, config):
4✔
416
        """
417
        Executes the action, specifically the function associated with the action.
418
        Args:
419
           configPath: Path to configuration file on disk
420
           options: Command-line options to be passed to action
421
           config: Parsed configuration to be passed to action
422
        """
423
        name = "%s.%s" % (self.function.__module__, self.function.__name__)
×
424
        logger.debug("Calling action function [%s], execution index [%d]", name, self.index)
×
425
        self.function(configPath, options, config)
×
426

427
    def _executeHook(self, type, hook):
4✔
428
        """
429
        Executes a hook command via :any:`util.executeCommand`.
430
        Args:
431
           type: String describing the type of hook, for logging
432
           hook: Hook, in terms of a ``ActionHook`` object
433
        """
434
        fields = splitCommandLine(hook.command)
×
435
        logger.debug("Executing %s hook for action %s: %s", type, hook.action, fields)
×
436
        result, output = executeCommand(command=fields[0:1], args=fields[1:], returnOutput=True)
×
437
        if result != 0:
×
438
            logger.error("Hook failed, tail is: %s", "\n   %s" % "   ".join(output[-10:]) if output else "<empty>")
×
439
            raise IOError("Error (%d) executing %s hook for action %s: %s" % (result, type, hook.action, fields))
×
440

441

442
###########################
443
# _ManagedActionItem class
444
###########################
445

446

447
@total_ordering
4✔
448
class _ManagedActionItem(object):
4✔
449
    """
450
    Class representing a single action to be executed on a managed peer.
451

452
    This class represents a single named action to be executed, and understands
453
    how to execute that action.
454

455
    Actions to be executed on a managed peer rely on peer configuration and
456
    on the full-backup flag.  All other configuration takes place on the remote
457
    peer itself.
458

459
    *Note:* The comparison operators for this class have been implemented to only
460
    compare based on the index and SORT_ORDER value, and ignore all other
461
    values.  This is so that the action set list can be easily sorted first by
462
    type (_ActionItem before _ManagedActionItem) and then by index within type.
463

464
    Attributes:
465
       SORT_ORDER: Defines a sort order to order properly between types
466
    """
467

468
    SORT_ORDER = 1
4✔
469

470
    def __init__(self, index, name, remotePeers):
4✔
471
        """
472
        Default constructor.
473

474
        Args:
475
           index: Index of the item (or ``None``)
476
           name: Name of the action that is being executed
477
           remotePeers: List of remote peers on which to execute the action
478
        """
479
        self.index = index
4✔
480
        self.name = name
4✔
481
        self.remotePeers = remotePeers
4✔
482

483
    def __eq__(self, other):
4✔
484
        """Equals operator, implemented in terms of original Python 2 compare operator."""
485
        return self.__cmp__(other) == 0
×
486

487
    def __lt__(self, other):
4✔
488
        """Less-than operator, implemented in terms of original Python 2 compare operator."""
489
        return self.__cmp__(other) < 0
4✔
490

491
    def __gt__(self, other):
4✔
492
        """Greater-than operator, implemented in terms of original Python 2 compare operator."""
493
        return self.__cmp__(other) > 0
×
494

495
    def __cmp__(self, other):
4✔
496
        """
497
        Original Python 2 comparison operator.
498
        The only thing we compare is the item's index.
499
        Args:
500
           other: Other object to compare to
501
        Returns:
502
            -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
503
        """
504
        if other is None:
4✔
505
            return 1
×
506
        if self.index != other.index:
4✔
507
            if int(self.index or 0) < int(other.index or 0):
4✔
508
                return -1
4✔
509
            else:
510
                return 1
4✔
511
        else:
512
            if self.SORT_ORDER != other.SORT_ORDER:
4✔
513
                if int(self.SORT_ORDER or 0) < int(other.SORT_ORDER or 0):
4✔
514
                    return -1
×
515
                else:
516
                    return 1
4✔
517
        return 0
×
518

519
    def executeAction(self, configPath, options, config):
4✔
520
        """
521
        Executes the managed action associated with an item.
522

523
        *Note:* Only options.full is actually used.  The rest of the arguments
524
        exist to satisfy the ActionItem iterface.
525

526
        *Note:* Errors here result in a message logged to ERROR, but no thrown
527
        exception.  The analogy is the stage action where a problem with one host
528
        should not kill the entire backup.  Since we're logging an error, the
529
        administrator will get an email.
530

531
        Args:
532
           configPath: Path to configuration file on disk
533
           options: Command-line options to be passed to action
534
           config: Parsed configuration to be passed to action
535

536
        Raises:
537
           Exception: If there is a problem executing the action
538
        """
539
        for peer in self.remotePeers:
×
540
            logger.debug("Executing managed action [%s] on peer [%s].", self.name, peer.name)
×
541
            try:
×
542
                peer.executeManagedAction(self.name, options.full)
×
543
            except IOError as e:
×
544
                logger.error(e)  # log the message and go on, so we don't kill the backup
×
545

546

547
###################
548
# _ActionSet class
549
###################
550

551

552
class _ActionSet(object):
4✔
553
    """
554
    Class representing a set of local actions to be executed.
555

556
    This class does four different things.  First, it ensures that the actions
557
    specified on the command-line are sensible.  The command-line can only list
558
    either built-in actions or extended actions specified in configuration.
559
    Also, certain actions (in :any:`NONCOMBINE_ACTIONS`) cannot be combined with
560
    other actions.
561

562
    Second, the class enforces an execution order on the specified actions.  Any
563
    time actions are combined on the command line (either built-in actions or
564
    extended actions), we must make sure they get executed in a sensible order.
565

566
    Third, the class ensures that any pre-action or post-action hooks are
567
    scheduled and executed appropriately.  Hooks are configured by building a
568
    dictionary mapping between hook action name and command.  Pre-action hooks
569
    are executed immediately before their associated action, and post-action
570
    hooks are executed immediately after their associated action.
571

572
    Finally, the class properly interleaves local and managed actions so that
573
    the same action gets executed first locally and then on managed peers.
574

575
    """
576

577
    def __init__(self, actions, extensions, options, peers, managed, local):
4✔
578
        """
579
        Constructor for the ``_ActionSet`` class.
580

581
        This is kind of ugly, because the constructor has to set up a lot of data
582
        before being able to do anything useful.  The following data structures
583
        are initialized based on the input:
584

585
           - ``extensionNames``: List of extensions available in configuration
586
           - ``preHookMap``: Mapping from action name to list of ``PreActionHook``
587
           - ``postHookMap``: Mapping from action name to list of ``PostActionHook``
588
           - ``functionMap``: Mapping from action name to Python function
589
           - ``indexMap``: Mapping from action name to execution index
590
           - ``peerMap``: Mapping from action name to set of ``RemotePeer``
591
           - ``actionMap``: Mapping from action name to ``_ActionItem``
592

593
        Once these data structures are set up, the command line is validated to
594
        make sure only valid actions have been requested, and in a sensible
595
        combination.  Then, all of the data is used to build ``self.actionSet``,
596
        the set action items to be executed by ``executeActions()``.  This list
597
        might contain either ``_ActionItem`` or ``_ManagedActionItem``.
598

599
        Args:
600
           actions: Names of actions specified on the command-line
601
           extensions: Extended action configuration (i.e. config.extensions)
602
           options: Options configuration (i.e. config.options)
603
           peers: Peers configuration (i.e. config.peers)
604
           managed: Whether to include managed actions in the set
605
           local: Whether to include local actions in the set
606

607
        Raises:
608
           ValueError: If one of the specified actions is invalid
609
        """
610
        extensionNames = _ActionSet._deriveExtensionNames(extensions)
4✔
611
        (preHookMap, postHookMap) = _ActionSet._buildHookMaps(options.hooks)
4✔
612
        functionMap = _ActionSet._buildFunctionMap(extensions)
4✔
613
        indexMap = _ActionSet._buildIndexMap(extensions)
4✔
614
        peerMap = _ActionSet._buildPeerMap(options, peers)
4✔
615
        actionMap = _ActionSet._buildActionMap(
4✔
616
            managed, local, extensionNames, functionMap, indexMap, preHookMap, postHookMap, peerMap
617
        )
618
        _ActionSet._validateActions(actions, extensionNames)
4✔
619
        self.actionSet = _ActionSet._buildActionSet(actions, actionMap)
4✔
620

621
    @staticmethod
4✔
622
    def _deriveExtensionNames(extensions):
4✔
623
        """
624
        Builds a list of extended actions that are available in configuration.
625
        Args:
626
           extensions: Extended action configuration (i.e. config.extensions)
627
        Returns:
628
            List of extended action names
629
        """
630
        extensionNames = []
4✔
631
        if extensions is not None and extensions.actions is not None:
4✔
632
            for action in extensions.actions:
4✔
633
                extensionNames.append(action.name)
4✔
634
        return extensionNames
4✔
635

636
    @staticmethod
4✔
637
    def _buildHookMaps(hooks):
4✔
638
        """
639
        Build two mappings from action name to configured ``ActionHook``.
640
        Args:
641
           hooks: List of pre- and post-action hooks (i.e. config.options.hooks)
642
        Returns:
643
            Tuple of (pre hook dictionary, post hook dictionary)
644
        """
645
        preHookMap = {}
4✔
646
        postHookMap = {}
4✔
647
        if hooks is not None:
4✔
648
            for hook in hooks:
4✔
649
                if hook.before:
4✔
650
                    if hook.action not in preHookMap:
4✔
651
                        preHookMap[hook.action] = []
4✔
652
                    preHookMap[hook.action].append(hook)
4✔
653
                elif hook.after:
4✔
654
                    if hook.action not in postHookMap:
4✔
655
                        postHookMap[hook.action] = []
4✔
656
                    postHookMap[hook.action].append(hook)
4✔
657
        return (preHookMap, postHookMap)
4✔
658

659
    @staticmethod
4✔
660
    def _buildFunctionMap(extensions):
4✔
661
        """
662
        Builds a mapping from named action to action function.
663
        Args:
664
           extensions: Extended action configuration (i.e. config.extensions)
665
        Returns:
666
            Dictionary mapping action to function
667
        """
668
        functionMap = {}
4✔
669
        functionMap["rebuild"] = executeRebuild
4✔
670
        functionMap["validate"] = executeValidate
4✔
671
        functionMap["initialize"] = executeInitialize
4✔
672
        functionMap["collect"] = executeCollect
4✔
673
        functionMap["stage"] = executeStage
4✔
674
        functionMap["store"] = executeStore
4✔
675
        functionMap["purge"] = executePurge
4✔
676
        if extensions is not None and extensions.actions is not None:
4✔
677
            for action in extensions.actions:
4✔
678
                functionMap[action.name] = getFunctionReference(action.module, action.function)
4✔
679
        return functionMap
4✔
680

681
    @staticmethod
4✔
682
    def _buildIndexMap(extensions):
4✔
683
        """
684
        Builds a mapping from action name to proper execution index.
685

686
        If extensions configuration is ``None``, or there are no configured
687
        extended actions, the ordering dictionary will only include the built-in
688
        actions and their standard indices.
689

690
        Otherwise, if the extensions order mode is ``None`` or ``"index"``, actions
691
        will scheduled by explicit index; and if the extensions order mode is
692
        ``"dependency"``, actions will be scheduled using a dependency graph.
693

694
        Args:
695
           extensions: Extended action configuration (i.e. config.extensions)
696

697
        Returns:
698
            Dictionary mapping action name to integer execution index
699
        """
700
        indexMap = {}
4✔
701
        if extensions is None or extensions.actions is None or extensions.actions == []:
4✔
702
            logger.info("Action ordering will use 'index' order mode.")
4✔
703
            indexMap["rebuild"] = REBUILD_INDEX
4✔
704
            indexMap["validate"] = VALIDATE_INDEX
4✔
705
            indexMap["initialize"] = INITIALIZE_INDEX
4✔
706
            indexMap["collect"] = COLLECT_INDEX
4✔
707
            indexMap["stage"] = STAGE_INDEX
4✔
708
            indexMap["store"] = STORE_INDEX
4✔
709
            indexMap["purge"] = PURGE_INDEX
4✔
710
            logger.debug("Completed filling in action indices for built-in actions.")
4✔
711
            logger.info("Action order will be: %s", sortDict(indexMap))
4✔
712
        else:
713
            if extensions.orderMode is None or extensions.orderMode == "index":
4✔
714
                logger.info("Action ordering will use 'index' order mode.")
4✔
715
                indexMap["rebuild"] = REBUILD_INDEX
4✔
716
                indexMap["validate"] = VALIDATE_INDEX
4✔
717
                indexMap["initialize"] = INITIALIZE_INDEX
4✔
718
                indexMap["collect"] = COLLECT_INDEX
4✔
719
                indexMap["stage"] = STAGE_INDEX
4✔
720
                indexMap["store"] = STORE_INDEX
4✔
721
                indexMap["purge"] = PURGE_INDEX
4✔
722
                logger.debug("Completed filling in action indices for built-in actions.")
4✔
723
                for action in extensions.actions:
4✔
724
                    indexMap[action.name] = action.index
4✔
725
                logger.debug("Completed filling in action indices for extended actions.")
4✔
726
                logger.info("Action order will be: %s", sortDict(indexMap))
4✔
727
            else:
728
                logger.info("Action ordering will use 'dependency' order mode.")
4✔
729
                graph = DirectedGraph("dependencies")
4✔
730
                graph.createVertex("rebuild")
4✔
731
                graph.createVertex("validate")
4✔
732
                graph.createVertex("initialize")
4✔
733
                graph.createVertex("collect")
4✔
734
                graph.createVertex("stage")
4✔
735
                graph.createVertex("store")
4✔
736
                graph.createVertex("purge")
4✔
737
                for action in extensions.actions:
4✔
738
                    graph.createVertex(action.name)
4✔
739
                graph.createEdge("collect", "stage")  # Collect must run before stage, store or purge
4✔
740
                graph.createEdge("collect", "store")
4✔
741
                graph.createEdge("collect", "purge")
4✔
742
                graph.createEdge("stage", "store")  # Stage must run before store or purge
4✔
743
                graph.createEdge("stage", "purge")
4✔
744
                graph.createEdge("store", "purge")  # Store must run before purge
4✔
745
                for action in extensions.actions:
4✔
746
                    if action.dependencies.beforeList is not None:
4✔
747
                        for vertex in action.dependencies.beforeList:
4✔
748
                            try:
4✔
749
                                graph.createEdge(action.name, vertex)  # actions that this action must be run before
4✔
750
                            except ValueError:
4✔
751
                                logger.error("Dependency [%s] on extension [%s] is unknown.", vertex, action.name)
4✔
752
                                raise ValueError("Unable to determine proper action order due to invalid dependency.")
4✔
753
                    if action.dependencies.afterList is not None:
4✔
754
                        for vertex in action.dependencies.afterList:
4✔
755
                            try:
4✔
756
                                graph.createEdge(vertex, action.name)  # actions that this action must be run after
4✔
757
                            except ValueError:
×
758
                                logger.error("Dependency [%s] on extension [%s] is unknown.", vertex, action.name)
×
759
                                raise ValueError("Unable to determine proper action order due to invalid dependency.")
×
760
                try:
4✔
761
                    ordering = graph.topologicalSort()
4✔
762
                    indexMap = dict([(ordering[i], i + 1) for i in range(0, len(ordering))])
4✔
763
                    logger.info("Action order will be: %s", ordering)
4✔
764
                except ValueError:
4✔
765
                    logger.error("Unable to determine proper action order due to dependency recursion.")
4✔
766
                    logger.error("Extensions configuration is invalid (check for loops).")
4✔
767
                    raise ValueError("Unable to determine proper action order due to dependency recursion.")
4✔
768
        return indexMap
4✔
769

770
    @staticmethod
4✔
771
    def _buildActionMap(managed, local, extensionNames, functionMap, indexMap, preHookMap, postHookMap, peerMap):
4✔
772
        """
773
        Builds a mapping from action name to list of action items.
774

775
        We build either ``_ActionItem`` or ``_ManagedActionItem`` objects here.
776

777
        In most cases, the mapping from action name to ``_ActionItem`` is 1:1.
778
        The exception is the "all" action, which is a special case.  However, a
779
        list is returned in all cases, just for consistency later.  Each
780
        ``_ActionItem`` will be created with a proper function reference and index
781
        value for execution ordering.
782

783
        The mapping from action name to ``_ManagedActionItem`` is always 1:1.
784
        Each managed action item contains a list of peers which the action should
785
        be executed.
786

787
        Args:
788
           managed: Whether to include managed actions in the set
789
           local: Whether to include local actions in the set
790
           extensionNames: List of valid extended action names
791
           functionMap: Dictionary mapping action name to Python function
792
           indexMap: Dictionary mapping action name to integer execution index
793
           preHookMap: Dictionary mapping action name to pre hooks (if any) for the action
794
           postHookMap: Dictionary mapping action name to post hooks (if any) for the action
795
           peerMap: Dictionary mapping action name to list of remote peers on which to execute the action
796

797
        Returns:
798
            Dictionary mapping action name to list of ``_ActionItem`` objects
799
        """
800
        actionMap = {}
4✔
801
        for name in extensionNames + VALID_ACTIONS:
4✔
802
            if name != "all":  # do this one later
4✔
803
                function = functionMap[name]
4✔
804
                index = indexMap[name]
4✔
805
                actionMap[name] = []
4✔
806
                if local:
4✔
807
                    (preHooks, postHooks) = _ActionSet._deriveHooks(name, preHookMap, postHookMap)
4✔
808
                    actionMap[name].append(_ActionItem(index, name, preHooks, postHooks, function))
4✔
809
                if managed:
4✔
810
                    if name in peerMap:
4✔
811
                        actionMap[name].append(_ManagedActionItem(index, name, peerMap[name]))
4✔
812
        actionMap["all"] = actionMap["collect"] + actionMap["stage"] + actionMap["store"] + actionMap["purge"]
4✔
813
        return actionMap
4✔
814

815
    @staticmethod
4✔
816
    def _buildPeerMap(options, peers):
4✔
817
        """
818
        Build a mapping from action name to list of remote peers.
819

820
        There will be one entry in the mapping for each managed action.  If there
821
        are no managed peers, the mapping will be empty.  Only managed actions
822
        will be listed in the mapping.
823

824
        Args:
825
           options: Option configuration (i.e. config.options)
826
           peers: Peers configuration (i.e. config.peers)
827
        """
828
        peerMap = {}
4✔
829
        if peers is not None:
4✔
830
            if peers.remotePeers is not None:
4✔
831
                for peer in peers.remotePeers:
4✔
832
                    if peer.managed:
4✔
833
                        remoteUser = _ActionSet._getRemoteUser(options, peer)
4✔
834
                        rshCommand = _ActionSet._getRshCommand(options, peer)
4✔
835
                        cbackCommand = _ActionSet._getCbackCommand(options, peer)
4✔
836
                        managedActions = _ActionSet._getManagedActions(options, peer)
4✔
837
                        remotePeer = RemotePeer(
4✔
838
                            peer.name, None, options.workingDir, remoteUser, None, options.backupUser, rshCommand, cbackCommand
839
                        )
840
                        if managedActions is not None:
4✔
841
                            for managedAction in managedActions:
4✔
842
                                if managedAction in peerMap:
4✔
843
                                    if remotePeer not in peerMap[managedAction]:
4✔
844
                                        peerMap[managedAction].append(remotePeer)
4✔
845
                                else:
846
                                    peerMap[managedAction] = [remotePeer]
4✔
847
        return peerMap
4✔
848

849
    @staticmethod
4✔
850
    def _deriveHooks(action, preHookDict, postHookDict):
4✔
851
        """
852
        Derive pre- and post-action hooks, if any, associated with named action.
853
        Args:
854
           action: Name of action to look up
855
           preHookDict: Dictionary mapping pre-action hooks to action name
856
           postHookDict: Dictionary mapping post-action hooks to action name
857
        @return Tuple (preHooks, postHooks) per mapping, with None values if there is no hook
858
        """
859
        preHooks = None
4✔
860
        postHooks = None
4✔
861
        if action in preHookDict:
4✔
862
            preHooks = preHookDict[action]
4✔
863
        if action in postHookDict:
4✔
864
            postHooks = postHookDict[action]
4✔
865
        return (preHooks, postHooks)
4✔
866

867
    @staticmethod
4✔
868
    def _validateActions(actions, extensionNames):
4✔
869
        """
870
        Validate that the set of specified actions is sensible.
871

872
        Any specified action must either be a built-in action or must be among
873
        the extended actions defined in configuration.  The actions from within
874
        :any:`NONCOMBINE_ACTIONS` may not be combined with other actions.
875

876
        Args:
877
           actions: Names of actions specified on the command-line
878
           extensionNames: Names of extensions specified in configuration
879

880
        Raises:
881
           ValueError: If one or more configured actions are not valid
882
        """
883
        if actions is None or actions == []:
4✔
884
            raise ValueError("No actions specified.")
4✔
885
        for action in actions:
4✔
886
            if action not in VALID_ACTIONS and action not in extensionNames:
4✔
887
                raise ValueError("Action [%s] is not a valid action or extended action." % action)
4✔
888
        for action in NONCOMBINE_ACTIONS:
4✔
889
            if action in actions and actions != [action]:
4✔
890
                raise ValueError("Action [%s] may not be combined with other actions." % action)
4✔
891

892
    @staticmethod
4✔
893
    def _buildActionSet(actions, actionMap):
4✔
894
        """
895
        Build set of actions to be executed.
896

897
        The set of actions is built in the proper order, so ``executeActions`` can
898
        spin through the set without thinking about it.  Since we've already validated
899
        that the set of actions is sensible, we don't take any precautions here to
900
        make sure things are combined properly.  If the action is listed, it will
901
        be "scheduled" for execution.
902

903
        Args:
904
           actions: Names of actions specified on the command-line
905
           actionMap: Dictionary mapping action name to ``_ActionItem`` object
906

907
        Returns:
908
            Set of action items in proper order
909
        """
910
        actionSet = []
4✔
911
        for action in actions:
4✔
912
            actionSet.extend(actionMap[action])
4✔
913
        actionSet.sort()  # sort the actions in order by index
4✔
914
        return actionSet
4✔
915

916
    def executeActions(self, configPath, options, config):
4✔
917
        """
918
        Executes all actions and extended actions, in the proper order.
919

920
        Each action (whether built-in or extension) is executed in an identical
921
        manner.  The built-in actions will use only the options and config
922
        values.  We also pass in the config path so that extension modules can
923
        re-parse configuration if they want to, to add in extra information.
924

925
        Args:
926
           configPath: Path to configuration file on disk
927
           options: Command-line options to be passed to action functions
928
           config: Parsed configuration to be passed to action functions
929

930
        Raises:
931
           Exception: If there is a problem executing the actions
932
        """
933
        logger.debug("Executing local actions.")
×
934
        for actionItem in self.actionSet:
×
935
            actionItem.executeAction(configPath, options, config)
×
936

937
    @staticmethod
4✔
938
    def _getRemoteUser(options, remotePeer):
4✔
939
        """
940
        Gets the remote user associated with a remote peer.
941
        Use peer's if possible, otherwise take from options section.
942
        Args:
943
           options: OptionsConfig object, as from config.options
944
           remotePeer: Configuration-style remote peer object
945
        Returns:
946
            Name of remote user associated with remote peer
947
        """
948
        if remotePeer.remoteUser is None:
4✔
949
            return options.backupUser
4✔
950
        return remotePeer.remoteUser
4✔
951

952
    @staticmethod
4✔
953
    def _getRshCommand(options, remotePeer):
4✔
954
        """
955
        Gets the RSH command associated with a remote peer.
956
        Use peer's if possible, otherwise take from options section.
957
        Args:
958
           options: OptionsConfig object, as from config.options
959
           remotePeer: Configuration-style remote peer object
960
        Returns:
961
            RSH command associated with remote peer
962
        """
963
        if remotePeer.rshCommand is None:
4✔
964
            return options.rshCommand
4✔
965
        return remotePeer.rshCommand
4✔
966

967
    @staticmethod
4✔
968
    def _getCbackCommand(options, remotePeer):
4✔
969
        """
970
        Gets the cback command associated with a remote peer.
971
        Use peer's if possible, otherwise take from options section.
972
        Args:
973
           options: OptionsConfig object, as from config.options
974
           remotePeer: Configuration-style remote peer object
975
        Returns:
976
            cback command associated with remote peer
977
        """
978
        if remotePeer.cbackCommand is None:
4✔
979
            return options.cbackCommand
4✔
980
        return remotePeer.cbackCommand
4✔
981

982
    @staticmethod
4✔
983
    def _getManagedActions(options, remotePeer):
4✔
984
        """
985
        Gets the managed actions list associated with a remote peer.
986
        Use peer's if possible, otherwise take from options section.
987
        Args:
988
           options: OptionsConfig object, as from config.options
989
           remotePeer: Configuration-style remote peer object
990
        Returns:
991
            Set of managed actions associated with remote peer
992
        """
993
        if remotePeer.managedActions is None:
4✔
994
            return options.managedActions
4✔
995
        return remotePeer.managedActions
4✔
996

997

998
#######################################################################
999
# Utility functions
1000
#######################################################################
1001

1002
####################
1003
# _usage() function
1004
####################
1005

1006

1007
def _usage(fd=sys.stderr):
4✔
1008
    """
1009
    Prints usage information for the cback3 script.
1010
    Args:
1011
       fd: File descriptor used to print information
1012
    *Note:* The ``fd`` is used rather than ``print`` to facilitate unit testing.
1013
    """
1014
    fd.write("\n")
4✔
1015
    fd.write(" Usage: cback3 [switches] action(s)\n")
4✔
1016
    fd.write("\n")
4✔
1017
    fd.write(" The following switches are accepted:\n")
4✔
1018
    fd.write("\n")
4✔
1019
    fd.write("   -h, --help         Display this usage/help listing\n")
4✔
1020
    fd.write("   -V, --version      Display version information\n")
4✔
1021
    fd.write("   -b, --verbose      Print verbose output as well as logging to disk\n")
4✔
1022
    fd.write("   -q, --quiet        Run quietly (display no output to the screen)\n")
4✔
1023
    fd.write("   -c, --config       Path to config file (default: %s)\n" % DEFAULT_CONFIG)
4✔
1024
    fd.write("   -f, --full         Perform a full backup, regardless of configuration\n")
4✔
1025
    fd.write("   -M, --managed      Include managed clients when executing actions\n")
4✔
1026
    fd.write("   -N, --managed-only Include ONLY managed clients when executing actions\n")
4✔
1027
    fd.write("   -l, --logfile      Path to logfile (default: %s)\n" % DEFAULT_LOGFILE)
4✔
1028
    fd.write(
4✔
1029
        "   -o, --owner        Logfile ownership, user:group (default: %s:%s)\n" % (DEFAULT_OWNERSHIP[0], DEFAULT_OWNERSHIP[1])
1030
    )
1031
    fd.write("   -m, --mode         Octal logfile permissions mode (default: %o)\n" % DEFAULT_MODE)
4✔
1032
    fd.write("   -O, --output       Record some sub-command (i.e. cdrecord) output to the log\n")
4✔
1033
    fd.write("   -d, --debug        Write debugging information to the log (implies --output)\n")
4✔
1034
    fd.write(
4✔
1035
        "   -s, --stack        Dump a Python stack trace instead of swallowing exceptions\n"
1036
    )  # exactly 80 characters in width!
1037
    fd.write("   -D, --diagnostics  Print runtime diagnostics to the screen and exit\n")
4✔
1038
    fd.write("\n")
4✔
1039
    fd.write(" The following actions may be specified:\n")
4✔
1040
    fd.write("\n")
4✔
1041
    fd.write("   all                Take all normal actions (collect, stage, store, purge)\n")
4✔
1042
    fd.write("   collect            Take the collect action\n")
4✔
1043
    fd.write("   stage              Take the stage action\n")
4✔
1044
    fd.write("   store              Take the store action\n")
4✔
1045
    fd.write("   purge              Take the purge action\n")
4✔
1046
    fd.write('   rebuild            Rebuild "this week\'s" disc if possible\n')
4✔
1047
    fd.write("   validate           Validate configuration only\n")
4✔
1048
    fd.write("   initialize         Initialize media for use with Cedar Backup\n")
4✔
1049
    fd.write("\n")
4✔
1050
    fd.write(" You may also specify extended actions that have been defined in\n")
4✔
1051
    fd.write(" configuration.\n")
4✔
1052
    fd.write("\n")
4✔
1053
    fd.write(" You must specify at least one action to take.  More than one of\n")
4✔
1054
    fd.write(' the "collect", "stage", "store" or "purge" actions and/or\n')
4✔
1055
    fd.write(" extended actions may be specified in any arbitrary order; they\n")
4✔
1056
    fd.write(' will be executed in a sensible order.  The "all", "rebuild",\n')
4✔
1057
    fd.write(' "validate", and "initialize" actions may not be combined with\n')
4✔
1058
    fd.write(" other actions.\n")
4✔
1059
    fd.write("\n")
4✔
1060

1061

1062
######################
1063
# _version() function
1064
######################
1065

1066

1067
def _version(fd=sys.stdout):
4✔
1068
    """
1069
    Prints version information for the cback3 script.
1070
    Args:
1071
       fd: File descriptor used to print information
1072
    *Note:* The ``fd`` is used rather than ``print`` to facilitate unit testing.
1073
    """
1074
    fd.write("\n")
4✔
1075
    fd.write(" Cedar Backup version %s.\n" % VERSION)
4✔
1076
    fd.write("\n")
4✔
1077
    fd.write(" Copyright (c) %s <%s>.\n" % (AUTHOR, EMAIL))
4✔
1078
    fd.write(" See NOTICE for a list of included code and other contributors.\n")
4✔
1079
    fd.write(" This is free software; there is NO warranty.  See the\n")
4✔
1080
    fd.write(" GNU General Public License version 2 for copying conditions.\n")
4✔
1081
    fd.write("\n")
4✔
1082
    fd.write(" Use the --help option for usage information.\n")
4✔
1083
    fd.write("\n")
4✔
1084

1085

1086
##########################
1087
# _diagnostics() function
1088
##########################
1089

1090

1091
def _diagnostics(fd=sys.stdout):
4✔
1092
    """
1093
    Prints runtime diagnostics information.
1094
    Args:
1095
       fd: File descriptor used to print information
1096
    *Note:* The ``fd`` is used rather than ``print`` to facilitate unit testing.
1097
    """
1098
    fd.write("\n")
4✔
1099
    fd.write("Diagnostics:\n")
4✔
1100
    fd.write("\n")
4✔
1101
    Diagnostics().printDiagnostics(fd=fd, prefix="   ")
4✔
1102
    fd.write("\n")
4✔
1103

1104

1105
##########################
1106
# setupLogging() function
1107
##########################
1108

1109

1110
def setupLogging(options):
4✔
1111
    """
1112
    Set up logging based on command-line options.
1113

1114
    There are two kinds of logging: flow logging and output logging.  Output
1115
    logging contains information about system commands executed by Cedar Backup,
1116
    for instance the calls to ``mkisofs`` or ``mount``, etc.  Flow logging
1117
    contains error and informational messages used to understand program flow.
1118
    Flow log messages and output log messages are written to two different
1119
    loggers target (``CedarBackup3.log`` and ``CedarBackup3.output``).  Flow log
1120
    messages are written at the ERROR, INFO and DEBUG log levels, while output
1121
    log messages are generally only written at the INFO log level.
1122

1123
    By default, output logging is disabled.  When the ``options.output`` or
1124
    ``options.debug`` flags are set, output logging will be written to the
1125
    configured logfile.  Output logging is never written to the screen.
1126

1127
    By default, flow logging is enabled at the ERROR level to the screen and at
1128
    the INFO level to the configured logfile.  If the ``options.quiet`` flag is
1129
    set, flow logging is enabled at the INFO level to the configured logfile
1130
    only (i.e. no output will be sent to the screen).  If the ``options.verbose``
1131
    flag is set, flow logging is enabled at the INFO level to both the screen
1132
    and the configured logfile.  If the ``options.debug`` flag is set, flow
1133
    logging is enabled at the DEBUG level to both the screen and the configured
1134
    logfile.
1135

1136
    Args:
1137
       options (:any:`Options` object): Command-line options
1138
    Returns:
1139
        Path to logfile on disk
1140
    """
1141
    logfile = _setupLogfile(options)
×
1142
    _setupFlowLogging(logfile, options)
×
1143
    _setupOutputLogging(logfile, options)
×
1144
    return logfile
×
1145

1146

1147
def _setupLogfile(options):
4✔
1148
    """
1149
    Sets up and creates logfile as needed.
1150

1151
    If the logfile already exists on disk, it will be left as-is, under the
1152
    assumption that it was created with appropriate ownership and permissions.
1153
    If the logfile does not exist on disk, it will be created as an empty file.
1154
    Ownership and permissions will remain at their defaults unless user/group
1155
    and/or mode are set in the options.  We ignore errors setting the indicated
1156
    user and group.
1157

1158
    *Note:* This function is vulnerable to a race condition.  If the log file
1159
    does not exist when the function is run, it will attempt to create the file
1160
    as safely as possible (using ``O_CREAT``).  If two processes attempt to
1161
    create the file at the same time, then one of them will fail.  In practice,
1162
    this shouldn't really be a problem, but it might happen occassionally if two
1163
    instances of cback3 run concurrently or if cback3 collides with logrotate or
1164
    something.
1165

1166
    Args:
1167
       options: Command-line options
1168

1169
    Returns:
1170
        Path to logfile on disk
1171
    """
1172
    if options.logfile is None:
×
1173
        logfile = DEFAULT_LOGFILE
×
1174
    else:
1175
        logfile = options.logfile
×
1176
    if not os.path.exists(logfile):
×
1177
        mode = DEFAULT_MODE if options.mode is None else options.mode
×
1178
        orig = os.umask(0)  # Per os.open(), "When computing mode, the current umask value is first masked out"
×
1179
        try:
×
1180
            fd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND, mode)
×
1181
            with os.fdopen(fd, "a+") as f:
×
1182
                f.write("")
×
1183
        finally:
1184
            os.umask(orig)
×
1185
        try:
×
1186
            if options.owner is None or len(options.owner) < 2:
×
1187
                (uid, gid) = getUidGid(DEFAULT_OWNERSHIP[0], DEFAULT_OWNERSHIP[1])
×
1188
            else:
1189
                (uid, gid) = getUidGid(options.owner[0], options.owner[1])
×
1190
            if sys.platform != "win32":
×
NEW
1191
                os.chown(logfile, uid, gid)
×
1192
        except:
×
1193
            pass
×
1194
    return logfile
×
1195

1196

1197
def _setupFlowLogging(logfile, options):
4✔
1198
    """
1199
    Sets up flow logging.
1200
    Args:
1201
       logfile: Path to logfile on disk
1202
       options: Command-line options
1203
    """
1204
    flowLogger = logging.getLogger("CedarBackup3.log")
×
1205
    flowLogger.setLevel(logging.DEBUG)  # let the logger see all messages
×
1206
    _setupDiskFlowLogging(flowLogger, logfile, options)
×
1207
    _setupScreenFlowLogging(flowLogger, options)
×
1208

1209

1210
def _setupOutputLogging(logfile, options):
4✔
1211
    """
1212
    Sets up command output logging.
1213
    Args:
1214
       logfile: Path to logfile on disk
1215
       options: Command-line options
1216
    """
1217
    outputLogger = logging.getLogger("CedarBackup3.output")
×
1218
    outputLogger.setLevel(logging.DEBUG)  # let the logger see all messages
×
1219
    _setupDiskOutputLogging(outputLogger, logfile, options)
×
1220

1221

1222
def _setupDiskFlowLogging(flowLogger, logfile, options):
4✔
1223
    """
1224
    Sets up on-disk flow logging.
1225
    Args:
1226
       flowLogger: Python flow logger object
1227
       logfile: Path to logfile on disk
1228
       options: Command-line options
1229
    """
1230
    formatter = logging.Formatter(fmt=DISK_LOG_FORMAT, datefmt=DATE_FORMAT)
×
1231
    handler = logging.FileHandler(logfile, mode="a", encoding="utf-8")
×
1232
    handler.setFormatter(formatter)
×
1233
    if options.debug:
×
1234
        handler.setLevel(logging.DEBUG)
×
1235
    else:
1236
        handler.setLevel(logging.INFO)
×
1237
    flowLogger.addHandler(handler)
×
1238

1239

1240
def _setupScreenFlowLogging(flowLogger, options):
4✔
1241
    """
1242
    Sets up on-screen flow logging.
1243
    Args:
1244
       flowLogger: Python flow logger object
1245
       options: Command-line options
1246
    """
1247
    formatter = logging.Formatter(fmt=SCREEN_LOG_FORMAT)
×
1248
    handler = logging.StreamHandler(SCREEN_LOG_STREAM)
×
1249
    handler.setFormatter(formatter)
×
1250
    if options.quiet:
×
1251
        handler.setLevel(logging.CRITICAL)  # effectively turn it off
×
1252
    elif options.verbose:
×
1253
        if options.debug:
×
1254
            handler.setLevel(logging.DEBUG)
×
1255
        else:
1256
            handler.setLevel(logging.INFO)
×
1257
    else:
1258
        handler.setLevel(logging.ERROR)
×
1259
    flowLogger.addHandler(handler)
×
1260

1261

1262
def _setupDiskOutputLogging(outputLogger, logfile, options):
4✔
1263
    """
1264
    Sets up on-disk command output logging.
1265
    Args:
1266
       outputLogger: Python command output logger object
1267
       logfile: Path to logfile on disk
1268
       options: Command-line options
1269
    """
1270
    formatter = logging.Formatter(fmt=DISK_OUTPUT_FORMAT, datefmt=DATE_FORMAT)
×
1271
    handler = logging.FileHandler(logfile, mode="a", encoding="utf-8")
×
1272
    handler.setFormatter(formatter)
×
1273
    if options.debug or options.output:
×
1274
        handler.setLevel(logging.DEBUG)
×
1275
    else:
1276
        handler.setLevel(logging.CRITICAL)  # effectively turn it off
×
1277
    outputLogger.addHandler(handler)
×
1278

1279

1280
###############################
1281
# setupPathResolver() function
1282
###############################
1283

1284

1285
def setupPathResolver(config):
4✔
1286
    """
1287
    Set up the path resolver singleton based on configuration.
1288

1289
    Cedar Backup's path resolver is implemented in terms of a singleton, the
1290
    :any:`PathResolverSingleton` class.  This function takes options configuration,
1291
    converts it into the dictionary form needed by the singleton, and then
1292
    initializes the singleton.  After that, any function that needs to resolve
1293
    the path of a command can use the singleton.
1294

1295
    Args:
1296
       config (:any:`Config` object): Configuration
1297

1298
    """
1299
    mapping = {}
4✔
1300
    if config.options.overrides is not None:
4✔
1301
        for override in config.options.overrides:
×
1302
            mapping[override.command] = override.absolutePath
×
1303
    singleton = PathResolverSingleton()
4✔
1304
    singleton.fill(mapping)
4✔
1305

1306

1307
#########################################################################
1308
# Options class definition
1309
########################################################################
1310

1311

1312
@total_ordering
4✔
1313
class Options(object):
4✔
1314
    ######################
1315
    # Class documentation
1316
    ######################
1317

1318
    """
1319
    Class representing command-line options for the cback3 script.
1320

1321
    The ``Options`` class is a Python object representation of the command-line
1322
    options of the cback3 script.
1323

1324
    The object representation is two-way: a command line string or a list of
1325
    command line arguments can be used to create an ``Options`` object, and then
1326
    changes to the object can be propogated back to a list of command-line
1327
    arguments or to a command-line string.  An ``Options`` object can even be
1328
    created from scratch programmatically (if you have a need for that).
1329

1330
    There are two main levels of validation in the ``Options`` class.  The first
1331
    is field-level validation.  Field-level validation comes into play when a
1332
    given field in an object is assigned to or updated.  We use Python's
1333
    ``property`` functionality to enforce specific validations on field values,
1334
    and in some places we even use customized list classes to enforce
1335
    validations on list members.  You should expect to catch a ``ValueError``
1336
    exception when making assignments to fields if you are programmatically
1337
    filling an object.
1338

1339
    The second level of validation is post-completion validation.  Certain
1340
    validations don't make sense until an object representation of options is
1341
    fully "complete".  We don't want these validations to apply all of the time,
1342
    because it would make building up a valid object from scratch a real pain.
1343
    For instance, we might have to do things in the right order to keep from
1344
    throwing exceptions, etc.
1345

1346
    All of these post-completion validations are encapsulated in the
1347
    :any:`Options.validate` method.  This method can be called at any time by a
1348
    client, and will always be called immediately after creating a ``Options``
1349
    object from a command line and before exporting a ``Options`` object back to
1350
    a command line.  This way, we get acceptable ease-of-use but we also don't
1351
    accept or emit invalid command lines.
1352

1353
    *Note:* Lists within this class are "unordered" for equality comparisons.
1354

1355
    """
1356

1357
    ##############
1358
    # Constructor
1359
    ##############
1360

1361
    def __init__(self, argumentList=None, argumentString=None, validate=True):
4✔
1362
        """
1363
        Initializes an options object.
1364

1365
        If you initialize the object without passing either ``argumentList`` or
1366
        ``argumentString``, the object will be empty and will be invalid until it
1367
        is filled in properly.
1368

1369
        No reference to the original arguments is saved off by this class.  Once
1370
        the data has been parsed (successfully or not) this original information
1371
        is discarded.
1372

1373
        The argument list is assumed to be a list of arguments, not including the
1374
        name of the command, something like ``sys.argv[1:]``.  If you pass
1375
        ``sys.argv`` instead, things are not going to work.
1376

1377
        The argument string will be parsed into an argument list by the
1378
        :any:`util.splitCommandLine` function (see the documentation for that
1379
        function for some important notes about its limitations).  There is an
1380
        assumption that the resulting list will be equivalent to ``sys.argv[1:]``,
1381
        just like ``argumentList``.
1382

1383
        Unless the ``validate`` argument is ``False``, the :any:`Options.validate`
1384
        method will be called (with its default arguments) after successfully
1385
        parsing any passed-in command line.  This validation ensures that
1386
        appropriate actions, etc. have been specified.  Keep in mind that even if
1387
        ``validate`` is ``False``, it might not be possible to parse the passed-in
1388
        command line, so an exception might still be raised.
1389

1390
        *Note:* The command line format is specified by the ``_usage`` function.
1391
        Call ``_usage`` to see a usage statement for the cback3 script.
1392

1393
        *Note:* It is strongly suggested that the ``validate`` option always be set
1394
        to ``True`` (the default) unless there is a specific need to read in
1395
        invalid command line arguments.
1396

1397
        Args:
1398
           argumentList (List of arguments, i.e. ``sys.argv``): Command line for a program
1399
           argumentString (String, i.e. "cback3 --verbose stage store"): Command line for a program
1400
           validate (Boolean true/false): Validate the command line after parsing it
1401
        Raises:
1402
           getopt.GetoptError: If the command-line arguments could not be parsed
1403
           ValueError: If the command-line arguments are invalid
1404
        """
1405
        self._help = False
4✔
1406
        self._version = False
4✔
1407
        self._verbose = False
4✔
1408
        self._quiet = False
4✔
1409
        self._config = None
4✔
1410
        self._full = False
4✔
1411
        self._managed = False
4✔
1412
        self._managedOnly = False
4✔
1413
        self._logfile = None
4✔
1414
        self._owner = None
4✔
1415
        self._mode = None
4✔
1416
        self._output = False
4✔
1417
        self._debug = False
4✔
1418
        self._stacktrace = False
4✔
1419
        self._diagnostics = False
4✔
1420
        self._actions = None
4✔
1421
        self.actions = []  # initialize to an empty list; remainder are OK
4✔
1422
        if argumentList is not None and argumentString is not None:
4✔
1423
            raise ValueError("Use either argumentList or argumentString, but not both.")
×
1424
        if argumentString is not None:
4✔
1425
            argumentList = splitCommandLine(argumentString)
4✔
1426
        if argumentList is not None:
4✔
1427
            self._parseArgumentList(argumentList)
4✔
1428
            if validate:
4✔
1429
                self.validate()
4✔
1430

1431
    #########################
1432
    # String representations
1433
    #########################
1434

1435
    def __repr__(self):
4✔
1436
        """
1437
        Official string representation for class instance.
1438
        """
1439
        return self.buildArgumentString(validate=False)
4✔
1440

1441
    def __str__(self):
4✔
1442
        """
1443
        Informal string representation for class instance.
1444
        """
1445
        return self.__repr__()
4✔
1446

1447
    #############################
1448
    # Standard comparison method
1449
    #############################
1450

1451
    def __eq__(self, other):
4✔
1452
        """Equals operator, implemented in terms of original Python 2 compare operator."""
1453
        return self.__cmp__(other) == 0
4✔
1454

1455
    def __lt__(self, other):
4✔
1456
        """Less-than operator, implemented in terms of original Python 2 compare operator."""
1457
        return self.__cmp__(other) < 0
4✔
1458

1459
    def __gt__(self, other):
4✔
1460
        """Greater-than operator, implemented in terms of original Python 2 compare operator."""
1461
        return self.__cmp__(other) > 0
4✔
1462

1463
    def __cmp__(self, other):
4✔
1464
        """
1465
        Original Python 2 comparison operator.
1466
        Lists within this class are "unordered" for equality comparisons.
1467
        Args:
1468
           other: Other object to compare to
1469
        Returns:
1470
            -1/0/1 depending on whether self is ``<``, ``=`` or ``>`` other
1471
        """
1472
        if other is None:
4✔
1473
            return 1
×
1474
        if self.help != other.help:
4✔
1475
            if self.help < other.help:
4✔
1476
                return -1
×
1477
            else:
1478
                return 1
4✔
1479
        if self.version != other.version:
4✔
1480
            if self.version < other.version:
4✔
1481
                return -1
4✔
1482
            else:
1483
                return 1
×
1484
        if self.verbose != other.verbose:
4✔
1485
            if self.verbose < other.verbose:
4✔
1486
                return -1
4✔
1487
            else:
1488
                return 1
×
1489
        if self.quiet != other.quiet:
4✔
1490
            if self.quiet < other.quiet:
4✔
1491
                return -1
×
1492
            else:
1493
                return 1
4✔
1494
        if self.config != other.config:
4✔
1495
            if self.config < other.config:
4✔
1496
                return -1
×
1497
            else:
1498
                return 1
4✔
1499
        if self.full != other.full:
4✔
1500
            if self.full < other.full:
4✔
1501
                return -1
4✔
1502
            else:
1503
                return 1
×
1504
        if self.managed != other.managed:
4✔
1505
            if self.managed < other.managed:
4✔
1506
                return -1
4✔
1507
            else:
1508
                return 1
×
1509
        if self.managedOnly != other.managedOnly:
4✔
1510
            if self.managedOnly < other.managedOnly:
4✔
1511
                return -1
4✔
1512
            else:
1513
                return 1
×
1514
        if self.logfile != other.logfile:
4✔
1515
            if str(self.logfile or "") < str(other.logfile or ""):
4✔
1516
                return -1
4✔
1517
            else:
1518
                return 1
×
1519
        if self.owner != other.owner:
4✔
1520
            if str(self.owner or "") < str(other.owner or ""):
4✔
1521
                return -1
4✔
1522
            else:
1523
                return 1
×
1524
        if self.mode != other.mode:
4✔
1525
            if int(self.mode or 0) < int(other.mode or 0):
4✔
1526
                return -1
4✔
1527
            else:
1528
                return 1
×
1529
        if self.output != other.output:
4✔
1530
            if self.output < other.output:
4✔
1531
                return -1
4✔
1532
            else:
1533
                return 1
×
1534
        if self.debug != other.debug:
4✔
1535
            if self.debug < other.debug:
4✔
1536
                return -1
×
1537
            else:
1538
                return 1
4✔
1539
        if self.stacktrace != other.stacktrace:
4✔
1540
            if self.stacktrace < other.stacktrace:
4✔
1541
                return -1
4✔
1542
            else:
1543
                return 1
×
1544
        if self.diagnostics != other.diagnostics:
4✔
1545
            if self.diagnostics < other.diagnostics:
4✔
1546
                return -1
4✔
1547
            else:
1548
                return 1
×
1549
        if self.actions != other.actions:
4✔
1550
            if self.actions < other.actions:
×
1551
                return -1
×
1552
            else:
1553
                return 1
×
1554
        return 0
4✔
1555

1556
    #############
1557
    # Properties
1558
    #############
1559

1560
    def _setHelp(self, value):
4✔
1561
        """
1562
        Property target used to set the help flag.
1563
        No validations, but we normalize the value to ``True`` or ``False``.
1564
        """
1565
        if value:
4✔
1566
            self._help = True
4✔
1567
        else:
1568
            self._help = False
4✔
1569

1570
    def _getHelp(self):
4✔
1571
        """
1572
        Property target used to get the help flag.
1573
        """
1574
        return self._help
4✔
1575

1576
    def _setVersion(self, value):
4✔
1577
        """
1578
        Property target used to set the version flag.
1579
        No validations, but we normalize the value to ``True`` or ``False``.
1580
        """
1581
        if value:
4✔
1582
            self._version = True
4✔
1583
        else:
1584
            self._version = False
4✔
1585

1586
    def _getVersion(self):
4✔
1587
        """
1588
        Property target used to get the version flag.
1589
        """
1590
        return self._version
4✔
1591

1592
    def _setVerbose(self, value):
4✔
1593
        """
1594
        Property target used to set the verbose flag.
1595
        No validations, but we normalize the value to ``True`` or ``False``.
1596
        """
1597
        if value:
4✔
1598
            self._verbose = True
4✔
1599
        else:
1600
            self._verbose = False
4✔
1601

1602
    def _getVerbose(self):
4✔
1603
        """
1604
        Property target used to get the verbose flag.
1605
        """
1606
        return self._verbose
4✔
1607

1608
    def _setQuiet(self, value):
4✔
1609
        """
1610
        Property target used to set the quiet flag.
1611
        No validations, but we normalize the value to ``True`` or ``False``.
1612
        """
1613
        if value:
4✔
1614
            self._quiet = True
4✔
1615
        else:
1616
            self._quiet = False
4✔
1617

1618
    def _getQuiet(self):
4✔
1619
        """
1620
        Property target used to get the quiet flag.
1621
        """
1622
        return self._quiet
4✔
1623

1624
    def _setConfig(self, value):
4✔
1625
        """
1626
        Property target used to set the config parameter.
1627
        """
1628
        if value is not None:
4✔
1629
            if len(value) < 1:
4✔
1630
                raise ValueError("The config parameter must be a non-empty string.")
×
1631
        self._config = value
4✔
1632

1633
    def _getConfig(self):
4✔
1634
        """
1635
        Property target used to get the config parameter.
1636
        """
1637
        return self._config
4✔
1638

1639
    def _setFull(self, value):
4✔
1640
        """
1641
        Property target used to set the full flag.
1642
        No validations, but we normalize the value to ``True`` or ``False``.
1643
        """
1644
        if value:
4✔
1645
            self._full = True
4✔
1646
        else:
1647
            self._full = False
4✔
1648

1649
    def _getFull(self):
4✔
1650
        """
1651
        Property target used to get the full flag.
1652
        """
1653
        return self._full
4✔
1654

1655
    def _setManaged(self, value):
4✔
1656
        """
1657
        Property target used to set the managed flag.
1658
        No validations, but we normalize the value to ``True`` or ``False``.
1659
        """
1660
        if value:
4✔
1661
            self._managed = True
4✔
1662
        else:
1663
            self._managed = False
4✔
1664

1665
    def _getManaged(self):
4✔
1666
        """
1667
        Property target used to get the managed flag.
1668
        """
1669
        return self._managed
4✔
1670

1671
    def _setManagedOnly(self, value):
4✔
1672
        """
1673
        Property target used to set the managedOnly flag.
1674
        No validations, but we normalize the value to ``True`` or ``False``.
1675
        """
1676
        if value:
4✔
1677
            self._managedOnly = True
4✔
1678
        else:
1679
            self._managedOnly = False
4✔
1680

1681
    def _getManagedOnly(self):
4✔
1682
        """
1683
        Property target used to get the managedOnly flag.
1684
        """
1685
        return self._managedOnly
4✔
1686

1687
    def _setLogfile(self, value):
4✔
1688
        """
1689
        Property target used to set the logfile parameter.
1690
        Raises:
1691
           ValueError: If the value cannot be encoded properly
1692
        """
1693
        if value is not None:
4✔
1694
            if len(value) < 1:
4✔
1695
                raise ValueError("The logfile parameter must be a non-empty string.")
×
1696
        self._logfile = encodePath(value)
4✔
1697

1698
    def _getLogfile(self):
4✔
1699
        """
1700
        Property target used to get the logfile parameter.
1701
        """
1702
        return self._logfile
4✔
1703

1704
    def _setOwner(self, value):
4✔
1705
        """
1706
        Property target used to set the owner parameter.
1707
        If not ``None``, the owner must be a ``(user,group)`` tuple or list.
1708
        Strings (and inherited children of strings) are explicitly disallowed.
1709
        The value will be normalized to a tuple.
1710
        Raises:
1711
           ValueError: If the value is not valid
1712
        """
1713
        if value is None:
4✔
1714
            self._owner = None
×
1715
        else:
1716
            if isinstance(value, str):
4✔
1717
                raise ValueError("Must specify user and group tuple for owner parameter.")
×
1718
            if len(value) != 2:
4✔
1719
                raise ValueError("Must specify user and group tuple for owner parameter.")
4✔
1720
            if len(value[0]) < 1 or len(value[1]) < 1:
4✔
1721
                raise ValueError("User and group tuple values must be non-empty strings.")
×
1722
            self._owner = (value[0], value[1])
4✔
1723

1724
    def _getOwner(self):
4✔
1725
        """
1726
        Property target used to get the owner parameter.
1727
        The parameter is a tuple of ``(user, group)``.
1728
        """
1729
        return self._owner
4✔
1730

1731
    def _setMode(self, value):
4✔
1732
        """
1733
        Property target used to set the mode parameter.
1734
        """
1735
        if value is None:
4✔
1736
            self._mode = None
×
1737
        else:
1738
            try:
4✔
1739
                if isinstance(value, str):
4✔
1740
                    value = int(value, 8)
4✔
1741
                else:
1742
                    value = int(value)
4✔
1743
            except TypeError:
4✔
1744
                raise ValueError("Mode must be an octal integer >= 0, i.e. 644.")
×
1745
            if value < 0:
4✔
1746
                raise ValueError("Mode must be an octal integer >= 0. i.e. 644.")
×
1747
            self._mode = value
4✔
1748

1749
    def _getMode(self):
4✔
1750
        """
1751
        Property target used to get the mode parameter.
1752
        """
1753
        return self._mode
4✔
1754

1755
    def _setOutput(self, value):
4✔
1756
        """
1757
        Property target used to set the output flag.
1758
        No validations, but we normalize the value to ``True`` or ``False``.
1759
        """
1760
        if value:
4✔
1761
            self._output = True
4✔
1762
        else:
1763
            self._output = False
4✔
1764

1765
    def _getOutput(self):
4✔
1766
        """
1767
        Property target used to get the output flag.
1768
        """
1769
        return self._output
4✔
1770

1771
    def _setDebug(self, value):
4✔
1772
        """
1773
        Property target used to set the debug flag.
1774
        No validations, but we normalize the value to ``True`` or ``False``.
1775
        """
1776
        if value:
4✔
1777
            self._debug = True
4✔
1778
        else:
1779
            self._debug = False
4✔
1780

1781
    def _getDebug(self):
4✔
1782
        """
1783
        Property target used to get the debug flag.
1784
        """
1785
        return self._debug
4✔
1786

1787
    def _setStacktrace(self, value):
4✔
1788
        """
1789
        Property target used to set the stacktrace flag.
1790
        No validations, but we normalize the value to ``True`` or ``False``.
1791
        """
1792
        if value:
4✔
1793
            self._stacktrace = True
4✔
1794
        else:
1795
            self._stacktrace = False
4✔
1796

1797
    def _getStacktrace(self):
4✔
1798
        """
1799
        Property target used to get the stacktrace flag.
1800
        """
1801
        return self._stacktrace
4✔
1802

1803
    def _setDiagnostics(self, value):
4✔
1804
        """
1805
        Property target used to set the diagnostics flag.
1806
        No validations, but we normalize the value to ``True`` or ``False``.
1807
        """
1808
        if value:
4✔
1809
            self._diagnostics = True
4✔
1810
        else:
1811
            self._diagnostics = False
4✔
1812

1813
    def _getDiagnostics(self):
4✔
1814
        """
1815
        Property target used to get the diagnostics flag.
1816
        """
1817
        return self._diagnostics
4✔
1818

1819
    def _setActions(self, value):
4✔
1820
        """
1821
        Property target used to set the actions list.
1822
        We don't restrict the contents of actions.  They're validated somewhere else.
1823
        Raises:
1824
           ValueError: If the value is not valid
1825
        """
1826
        if value is None:
4✔
1827
            self._actions = None
×
1828
        else:
1829
            try:
4✔
1830
                saved = self._actions
4✔
1831
                self._actions = []
4✔
1832
                self._actions.extend(value)
4✔
1833
            except Exception as e:
×
1834
                self._actions = saved
×
1835
                raise e
×
1836

1837
    def _getActions(self):
4✔
1838
        """
1839
        Property target used to get the actions list.
1840
        """
1841
        return self._actions
4✔
1842

1843
    help = property(_getHelp, _setHelp, None, "Command-line help (``-h,--help``) flag.")
4✔
1844
    version = property(_getVersion, _setVersion, None, "Command-line version (``-V,--version``) flag.")
4✔
1845
    verbose = property(_getVerbose, _setVerbose, None, "Command-line verbose (``-b,--verbose``) flag.")
4✔
1846
    quiet = property(_getQuiet, _setQuiet, None, "Command-line quiet (``-q,--quiet``) flag.")
4✔
1847
    config = property(_getConfig, _setConfig, None, "Command-line configuration file (``-c,--config``) parameter.")
4✔
1848
    full = property(_getFull, _setFull, None, "Command-line full-backup (``-f,--full``) flag.")
4✔
1849
    managed = property(_getManaged, _setManaged, None, "Command-line managed (``-M,--managed``) flag.")
4✔
1850
    managedOnly = property(_getManagedOnly, _setManagedOnly, None, "Command-line managed-only (``-N,--managed-only``) flag.")
4✔
1851
    logfile = property(_getLogfile, _setLogfile, None, "Command-line logfile (``-l,--logfile``) parameter.")
4✔
1852
    owner = property(_getOwner, _setOwner, None, "Command-line owner (``-o,--owner``) parameter, as tuple ``(user,group)``.")
4✔
1853
    mode = property(_getMode, _setMode, None, "Command-line mode (``-m,--mode``) parameter.")
4✔
1854
    output = property(_getOutput, _setOutput, None, "Command-line output (``-O,--output``) flag.")
4✔
1855
    debug = property(_getDebug, _setDebug, None, "Command-line debug (``-d,--debug``) flag.")
4✔
1856
    stacktrace = property(_getStacktrace, _setStacktrace, None, "Command-line stacktrace (``-s,--stack``) flag.")
4✔
1857
    diagnostics = property(_getDiagnostics, _setDiagnostics, None, "Command-line diagnostics (``-D,--diagnostics``) flag.")
4✔
1858
    actions = property(_getActions, _setActions, None, "Command-line actions list.")
4✔
1859

1860
    ##################
1861
    # Utility methods
1862
    ##################
1863

1864
    def validate(self):
4✔
1865
        """
1866
        Validates command-line options represented by the object.
1867

1868
        Unless ``--help`` or ``--version`` are supplied, at least one action must
1869
        be specified.  Other validations (as for allowed values for particular
1870
        options) will be taken care of at assignment time by the properties
1871
        functionality.
1872

1873
        *Note:* The command line format is specified by the ``_usage`` function.
1874
        Call ``_usage`` to see a usage statement for the cback3 script.
1875

1876
        Raises:
1877
           ValueError: If one of the validations fails
1878
        """
1879
        if not self.help and not self.version and not self.diagnostics:
4✔
1880
            if self.actions is None or len(self.actions) == 0:
4✔
1881
                raise ValueError("At least one action must be specified.")
4✔
1882
        if self.managed and self.managedOnly:
4✔
1883
            raise ValueError("The --managed and --managed-only options may not be combined.")
4✔
1884

1885
    def buildArgumentList(self, validate=True):
4✔
1886
        """
1887
        Extracts options into a list of command line arguments.
1888

1889
        The original order of the various arguments (if, indeed, the object was
1890
        initialized with a command-line) is not preserved in this generated
1891
        argument list.   Besides that, the argument list is normalized to use the
1892
        long option names (i.e. --version rather than -V).  The resulting list
1893
        will be suitable for passing back to the constructor in the
1894
        ``argumentList`` parameter.  Unlike :any:`buildArgumentString`, string
1895
        arguments are not quoted here, because there is no need for it.
1896

1897
        Unless the ``validate`` parameter is ``False``, the :any:`Options.validate`
1898
        method will be called (with its default arguments) against the
1899
        options before extracting the command line.  If the options are not valid,
1900
        then an argument list will not be extracted.
1901

1902
        *Note:* It is strongly suggested that the ``validate`` option always be set
1903
        to ``True`` (the default) unless there is a specific need to extract an
1904
        invalid command line.
1905

1906
        Args:
1907
           validate (Boolean true/false): Validate the options before extracting the command line
1908
        Returns:
1909
            List representation of command-line arguments
1910
        Raises:
1911
           ValueError: If options within the object are invalid
1912
        """
1913
        if validate:
4✔
1914
            self.validate()
4✔
1915
        argumentList = []
4✔
1916
        if self._help:
4✔
1917
            argumentList.append("--help")
4✔
1918
        if self.version:
4✔
1919
            argumentList.append("--version")
4✔
1920
        if self.verbose:
4✔
1921
            argumentList.append("--verbose")
4✔
1922
        if self.quiet:
4✔
1923
            argumentList.append("--quiet")
4✔
1924
        if self.config is not None:
4✔
1925
            argumentList.append("--config")
4✔
1926
            argumentList.append(self.config)
4✔
1927
        if self.full:
4✔
1928
            argumentList.append("--full")
4✔
1929
        if self.managed:
4✔
1930
            argumentList.append("--managed")
4✔
1931
        if self.managedOnly:
4✔
1932
            argumentList.append("--managed-only")
4✔
1933
        if self.logfile is not None:
4✔
1934
            argumentList.append("--logfile")
4✔
1935
            argumentList.append(self.logfile)
4✔
1936
        if self.owner is not None:
4✔
1937
            argumentList.append("--owner")
4✔
1938
            argumentList.append("%s:%s" % (self.owner[0], self.owner[1]))
4✔
1939
        if self.mode is not None:
4✔
1940
            argumentList.append("--mode")
4✔
1941
            argumentList.append("%o" % self.mode)
4✔
1942
        if self.output:
4✔
1943
            argumentList.append("--output")
4✔
1944
        if self.debug:
4✔
1945
            argumentList.append("--debug")
4✔
1946
        if self.stacktrace:
4✔
1947
            argumentList.append("--stack")
4✔
1948
        if self.diagnostics:
4✔
1949
            argumentList.append("--diagnostics")
4✔
1950
        if self.actions is not None:
4✔
1951
            for action in self.actions:
4✔
1952
                argumentList.append(action)
4✔
1953
        return argumentList
4✔
1954

1955
    def buildArgumentString(self, validate=True):
4✔
1956
        """
1957
        Extracts options into a string of command-line arguments.
1958

1959
        The original order of the various arguments (if, indeed, the object was
1960
        initialized with a command-line) is not preserved in this generated
1961
        argument string.   Besides that, the argument string is normalized to use
1962
        the long option names (i.e. --version rather than -V) and to quote all
1963
        string arguments with double quotes (``"``).  The resulting string will be
1964
        suitable for passing back to the constructor in the ``argumentString``
1965
        parameter.
1966

1967
        Unless the ``validate`` parameter is ``False``, the :any:`Options.validate`
1968
        method will be called (with its default arguments) against the options
1969
        before extracting the command line.  If the options are not valid, then
1970
        an argument string will not be extracted.
1971

1972
        *Note:* It is strongly suggested that the ``validate`` option always be set
1973
        to ``True`` (the default) unless there is a specific need to extract an
1974
        invalid command line.
1975

1976
        Args:
1977
           validate (Boolean true/false): Validate the options before extracting the command line
1978
        Returns:
1979
            String representation of command-line arguments
1980
        Raises:
1981
           ValueError: If options within the object are invalid
1982
        """
1983
        if validate:
4✔
1984
            self.validate()
4✔
1985
        argumentString = ""
4✔
1986
        if self._help:
4✔
1987
            argumentString += "--help "
4✔
1988
        if self.version:
4✔
1989
            argumentString += "--version "
4✔
1990
        if self.verbose:
4✔
1991
            argumentString += "--verbose "
4✔
1992
        if self.quiet:
4✔
1993
            argumentString += "--quiet "
4✔
1994
        if self.config is not None:
4✔
1995
            argumentString += '--config "%s" ' % self.config
4✔
1996
        if self.full:
4✔
1997
            argumentString += "--full "
4✔
1998
        if self.managed:
4✔
1999
            argumentString += "--managed "
4✔
2000
        if self.managedOnly:
4✔
2001
            argumentString += "--managed-only "
4✔
2002
        if self.logfile is not None:
4✔
2003
            argumentString += '--logfile "%s" ' % self.logfile
4✔
2004
        if self.owner is not None:
4✔
2005
            argumentString += '--owner "%s:%s" ' % (self.owner[0], self.owner[1])
4✔
2006
        if self.mode is not None:
4✔
2007
            argumentString += "--mode %o " % self.mode
4✔
2008
        if self.output:
4✔
2009
            argumentString += "--output "
4✔
2010
        if self.debug:
4✔
2011
            argumentString += "--debug "
4✔
2012
        if self.stacktrace:
4✔
2013
            argumentString += "--stack "
4✔
2014
        if self.diagnostics:
4✔
2015
            argumentString += "--diagnostics "
4✔
2016
        if self.actions is not None:
4✔
2017
            for action in self.actions:
4✔
2018
                argumentString += '"%s" ' % action
4✔
2019
        return argumentString
4✔
2020

2021
    def _parseArgumentList(self, argumentList):
4✔
2022
        """
2023
        Internal method to parse a list of command-line arguments.
2024

2025
        Most of the validation we do here has to do with whether the arguments
2026
        can be parsed and whether any values which exist are valid.  We don't do
2027
        any validation as to whether required elements exist or whether elements
2028
        exist in the proper combination (instead, that's the job of the
2029
        :any:`validate` method).
2030

2031
        For any of the options which supply parameters, if the option is
2032
        duplicated with long and short switches (i.e. ``-l`` and a ``--logfile``)
2033
        then the long switch is used.  If the same option is duplicated with the
2034
        same switch (long or short), then the last entry on the command line is
2035
        used.
2036

2037
        Args:
2038
           argumentList (List of arguments to a command, i.e. ``sys.argv[1:]``): List of arguments to a command
2039
        Raises:
2040
           ValueError: If the argument list cannot be successfully parsed
2041
        """
2042
        switches = {}
4✔
2043
        opts, self.actions = getopt.getopt(argumentList, SHORT_SWITCHES, LONG_SWITCHES)
4✔
2044
        for o, a in opts:  # push the switches into a hash
4✔
2045
            switches[o] = a
4✔
2046
        if "-h" in switches or "--help" in switches:
4✔
2047
            self.help = True
4✔
2048
        if "-V" in switches or "--version" in switches:
4✔
2049
            self.version = True
4✔
2050
        if "-b" in switches or "--verbose" in switches:
4✔
2051
            self.verbose = True
4✔
2052
        if "-q" in switches or "--quiet" in switches:
4✔
2053
            self.quiet = True
4✔
2054
        if "-c" in switches:
4✔
2055
            self.config = switches["-c"]
4✔
2056
        if "--config" in switches:
4✔
2057
            self.config = switches["--config"]
4✔
2058
        if "-f" in switches or "--full" in switches:
4✔
2059
            self.full = True
4✔
2060
        if "-M" in switches or "--managed" in switches:
4✔
2061
            self.managed = True
4✔
2062
        if "-N" in switches or "--managed-only" in switches:
4✔
2063
            self.managedOnly = True
4✔
2064
        if "-l" in switches:
4✔
2065
            self.logfile = switches["-l"]
4✔
2066
        if "--logfile" in switches:
4✔
2067
            self.logfile = switches["--logfile"]
4✔
2068
        if "-o" in switches:
4✔
2069
            self.owner = switches["-o"].split(":", 1)
4✔
2070
        if "--owner" in switches:
4✔
2071
            self.owner = switches["--owner"].split(":", 1)
4✔
2072
        if "-m" in switches:
4✔
2073
            self.mode = switches["-m"]
4✔
2074
        if "--mode" in switches:
4✔
2075
            self.mode = switches["--mode"]
4✔
2076
        if "-O" in switches or "--output" in switches:
4✔
2077
            self.output = True
4✔
2078
        if "-d" in switches or "--debug" in switches:
4✔
2079
            self.debug = True
4✔
2080
        if "-s" in switches or "--stack" in switches:
4✔
2081
            self.stacktrace = True
4✔
2082
        if "-D" in switches or "--diagnostics" in switches:
4✔
2083
            self.diagnostics = True
4✔
2084

2085

2086
#########################################################################
2087
# Main routine
2088
########################################################################
2089

2090
if __name__ == "__main__":
4✔
2091
    result = cli()
×
2092
    sys.exit(result)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc