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

pronovic / cedar-backup3 / 17933822248

23 Sep 2025 02:13AM UTC coverage: 73.371% (-0.02%) from 73.39%
17933822248

Pull #59

github

web-flow
Merge f14375987 into c082bd7a2
Pull Request #59: Migrate from Poetry to UV

7905 of 10774 relevant lines covered (73.37%)

2.2 hits per line

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

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

33
########################################################################
34
# Module documentation
35
########################################################################
36

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

40
Summary
41
=======
42

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

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

54
Backwards Compatibility
55
=======================
56

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

62
Module Attributes
63
=================
64

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

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

77
########################################################################
78
# Imported modules
79
########################################################################
80

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

83
import getopt
3✔
84
import logging
3✔
85
import os
3✔
86
import sys
3✔
87
from functools import total_ordering
3✔
88

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

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

116
logger = logging.getLogger("CedarBackup3.log.cli")
3✔
117

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

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

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

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

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

160

161
#######################################################################
162
# Public functions
163
#######################################################################
164

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

169

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

297

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

302
####################
303
# _ActionItem class
304
####################
305

306

307
@total_ordering
3✔
308
class _ActionItem:
3✔
309
    """
310
    Class representing a single action to be executed.
311

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

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

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

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

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

333
    SORT_ORDER = 0
3✔
334

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

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

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

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

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

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

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

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

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

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

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

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

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

440

441
###########################
442
# _ManagedActionItem class
443
###########################
444

445

446
@total_ordering
3✔
447
class _ManagedActionItem:
3✔
448
    """
449
    Class representing a single action to be executed on a managed peer.
450

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

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

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

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

467
    SORT_ORDER = 1
3✔
468

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

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

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

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

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

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

518
    def executeAction(self, configPath, options, config):  # noqa: ARG002
3✔
519
        """
520
        Executes the managed action associated with an item.
521

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

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

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

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

545

546
###################
547
# _ActionSet class
548
###################
549

550

551
class _ActionSet:
3✔
552
    """
553
    Class representing a set of local actions to be executed.
554

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

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

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

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

574
    """
575

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

996

997
#######################################################################
998
# Utility functions
999
#######################################################################
1000

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

1005

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

1060

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

1065

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

1084

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

1089

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

1103

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

1108

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

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

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

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

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

1145

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

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

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

1165
    Args:
1166
       options: Command-line options
1167

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

1195

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

1208

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

1220

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

1238

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

1260

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

1278

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

1283

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

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

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

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

1305

1306
#########################################################################
1307
# Options class definition
1308
########################################################################
1309

1310

1311
@total_ordering
3✔
1312
class Options:
3✔
1313
    ######################
1314
    # Class documentation
1315
    ######################
1316

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

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

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

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

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

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

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

1354
    """
1355

1356
    ##############
1357
    # Constructor
1358
    ##############
1359

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

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

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

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

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

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

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

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

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

1430
    #########################
1431
    # String representations
1432
    #########################
1433

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

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

1446
    #############################
1447
    # Standard comparison method
1448
    #############################
1449

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

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

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

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

1555
    #############
1556
    # Properties
1557
    #############
1558

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1859
    ##################
1860
    # Utility methods
1861
    ##################
1862

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2084

2085
#########################################################################
2086
# Main routine
2087
########################################################################
2088

2089
if __name__ == "__main__":
3✔
2090
    result = cli()
×
2091
    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

© 2025 Coveralls, Inc