Beispiel #1
0
 def execute(cls, name, args):
     command = cls.find(name)
     if command:
         command.run(name, args if args else [])
     else:
         error('unknown command.', culprit=name)
         codicil("Use 'avendesora help' for list of available commands."),
Beispiel #2
0
def test_wring():
    with messenger() as (msg, stdout, stderr, logfile):
        output('hey now!', flush=True)
        codicil('baby', 'bird', sep='\n')
        msg.flush_logfile()
        expected = dedent('''
            hey now!
            baby
            bird
        ''').strip()

        assert msg.errors_accrued() == 0
        assert errors_accrued() == 0
        assert strip(stdout) == expected
        assert strip(stderr) == ''
        assert log_strip(logfile) == dedent('''
            ack: invoked as: <exe>
            ack: invoked on: <date>
            {expected}
        ''').strip().format(expected=expected)

        try:
            terminate_if_errors()
            assert True
        except SystemExit:
            assert False
Beispiel #3
0
def test_fabricate():
    with messenger(hanging_indent=False) as (msg, stdout, stderr, logfile):
        error('hey now!')
        codicil('baby', 'bird', sep='\n')
        error('uh-huh\nuh-huh',
              culprit='yep yep yep yep yep yep yep yep yep yep yep'.split())
        expected = dedent('''
            error: hey now!
                baby
                bird
            error: yep, yep, yep, yep, yep, yep, yep, yep, yep, yep, yep:
                uh-huh
                uh-huh
        ''').strip()

        assert msg.errors_accrued() == 2
        assert errors_accrued(True) == 2
        assert msg.errors_accrued() == 0
        assert strip(stdout) == expected
        assert strip(stderr) == ''
        assert log_strip(logfile) == dedent('''
            ack: invoked as: <exe>
            ack: log opened on <date>
            {expected}
        ''').strip().format(expected=expected)
Beispiel #4
0
 def set_location(self, given=None):
     locations.set_location(given if given else self.network.location)
     unknown = locations.unknown_locations(self.locations)
     if unknown:
         warn(
             "the following locations are unknown (add them to LOCATIONS):")
         codicil(*sorted(unknown), sep="\n")
     self.location = self.locations.get(locations.my_location)
     if locations.my_location and not self.location:
         raise Error("unknown location, choose from:",
                     conjoin(self.locations))
Beispiel #5
0
def test_exact():
    with messenger() as (msg, stdout, stderr, logfile):
        error('aaa bbb ccc')
        codicil('000 111 222')
        codicil('!!! @@@ ###')
        assert msg.errors_accrued() == 1
        assert errors_accrued(True) == 1
        assert strip(stdout) == dedent('''
            error: aaa bbb ccc
                000 111 222
                !!! @@@ ###
        ''').strip()
        assert strip(stderr) == ''
Beispiel #6
0
def test_toboggan():
    with messenger() as (msg, stdout, stderr, logfile):
        warn('aaa bbb ccc')
        codicil('000 111 222')
        codicil('!!! @@@ ###')
        assert msg.errors_accrued() == 0
        assert errors_accrued(True) == 0
        assert strip(stdout) == dedent('''
            warning: aaa bbb ccc
                000 111 222
                !!! @@@ ###
        ''').strip()
        assert strip(stderr) == ''
Beispiel #7
0
    def update_params(self, **params):
        prev_params = self.metadata['parameters']
        curr_params = params

        if prev_params and prev_params != curr_params:
            error(f"{self.repr} parameters differ from those used previously!")
            for key in params:
                prev = prev_params.get(key, '')
                curr = curr_params.get(key, '')
                if prev != curr:
                    codicil(f"    {key!r} was {prev!r}, now {curr!r}")
            fatal("Use the -f flag to overwrite.  Aborting.")

        self.metadata['parameters'] = curr_params
Beispiel #8
0
    def display_field(self, account, field):
        # get string to display
        value, is_secret, name, desc = tuple(account.get_value(field))
        label = '%s (%s)' % (name, desc) if desc else name
        value = dedent(str(value)).strip()
        label_color = get_setting('_label_color')

        # indent multiline outputs
        sep = ' '
        if '\n' in value:
            if is_secret:
                warn('secret contains newlines, will not be fully concealed.')
            value = indent(value, get_setting('indent')).strip('\n')
            sep = '\n'

        if label:
            if label[0] == '_':
                # hidden field
                label = '!' + label[1:]
            text = label_color(label.replace('_', ' ') + ':') + sep + value
        else:
            text = value
            label = field
        log('Writing to TTY:', label)

        if is_secret:
            if Color.isTTY():
                # Write only if output is a TTY. This is a security feature.
                # The ideas is that when the TTY writer is called it is because
                # the user is expecting the output to go to the tty. This
                # eliminates the chance that the output can be intercepted and
                # recorded by replacing Avendesora with an alias or shell
                # script. If the user really want the output to go to something
                # other than the TTY, the user should use the --stdout option.
                try:
                    cursor.write(text)
                    cursor.conceal()
                    sleep(get_setting('display_time'))
                except KeyboardInterrupt:
                    pass
                cursor.reveal()
                cursor.clear()
            else:
                error('output is not a TTY.')
                codicil(
                    'Use --stdout option if you want to send secret',
                    'to a file or a pipe.'
                )
        else:
            output(text)
Beispiel #9
0
    def initialize_network(self):
        network = self.network

        # run the init script if given
        try:
            if network.init_script:
                script = Run(network.init_script, "sOEW")
                if script.stdout:
                    display(script.stdout.rstrip())
        except AttributeError:
            pass
        except Error as e:
            warn("{} network init_script failed: {}".format(
                network.name(), network.init_script))
            codicil(e.get_message())
Beispiel #10
0
    def run(cls, command, args, settings, options):
        # read command line
        cmdline = docopt(cls.USAGE, argv=[command] + args)
        paths = cmdline['<path>']
        archive = cmdline['--archive']
        date = cmdline['--date']

        # make sure source directories are given as absolute paths
        for src_dir in settings.src_dirs:
            if not src_dir.is_absolute():
                raise Error('restore command cannot be used',
                            'with relative source directories',
                            culprit=src_dir)

        # convert to absolute resolved paths
        paths = [to_path(p).resolve() for p in paths]

        # assure that paths correspond to src_dirs
        src_dirs = settings.src_dirs
        unknown_path = False
        for path in paths:
            if not any([str(path).startswith(str(sd)) for sd in src_dirs]):
                unknown_path = True
                warn('unknown path.', culprit=path)
        if unknown_path:
            codicil('Paths should start with:', conjoin(src_dirs,
                                                        conj=', or '))

        # remove leading / from paths
        paths = [str(p).lstrip('/') for p in paths]

        # get the desired archive
        if date and not archive:
            archive = get_name_of_nearest_archive(settings, date)
        if not archive:
            archive = get_name_of_latest_archive(settings)
        output('Archive:', archive)

        # run borg
        cd('/')
        borg = settings.run_borg(
            cmd='extract',
            args=[settings.destination(archive)] + paths,
            emborg_opts=options,
        )
        out = borg.stdout
        if out:
            output(out.rstrip())
Beispiel #11
0
    def preprocess(cls, master, fileinfo, seen):

        # return if this account has already been processed
        if hasattr(cls, '_file_info_'):
            return  # account has already been processed

        # add fileinfo
        cls._file_info_ = fileinfo

        # dedent any string attributes
        for k, v in cls.__dict__.items():
            if is_str(v) and '\n' in v:
                setattr(cls, k, dedent(v))

        # add master seed
        if master and not hasattr(cls, '_%s__NO_MASTER' % cls.__name__):
            if not hasattr(cls, 'master_seed'):
                cls.master_seed = master
                cls._master_source_ = 'file'
            else:
                cls._master_source_ = 'account'

        # convert aliases to a list
        if hasattr(cls, 'aliases'):
            aliases = list(Collection(cls.aliases))
            cls.aliases = aliases
        else:
            aliases = []

        # canonicalize names and look for duplicates
        new = {}
        account_name = cls.get_name()
        path = cls._file_info_.path
        for name in [account_name] + aliases:
            canonical = canonicalize(name)
            Account._accounts[canonical] = cls
            if canonical in seen:
                if name == account_name:
                    warn('duplicate account name.', culprit=name)
                else:
                    warn('alias duplicates existing name.', culprit=name)
                codicil('Seen in %s in %s.' % seen[canonical])
                codicil('And in %s in %s.' % (account_name, path))
                break
            else:
                new[canonical] = (account_name, path)
        seen.update(new)
Beispiel #12
0
    def _validate_components(self):
        from pkg_resources import resource_filename

        # check permissions on the settings directory
        path = get_setting('settings_dir')
        mask = get_setting('config_dir_mask')
        try:
            permissions = getmod(path)
        except FileNotFoundError:
            raise PasswordError('missing, must run initialize.', culprit=path)
        violation = permissions & mask
        if violation:
            recommended = permissions & ~mask & 0o777
            warn("directory permissions are too loose.", culprit=path)
            codicil("Recommend running: chmod {:o} {}".format(
                recommended, path))

        # Check that files that are critical to the integrity of the generated
        # secrets have not changed
        for path, kind in [
            (to_path(resource_filename(__name__,
                                       'secrets.py')), 'secrets_hash'),
            (to_path(resource_filename(__name__,
                                       'charsets.py')), 'charsets_hash'),
            ('default', 'dict_hash'),
            ('mnemonic', 'mnemonic_hash'),
        ]:
            try:
                contents = path.read_text()
            except AttributeError:
                contents = '\n'.join(Dictionary(path).get_words())
            except OSErrors as e:
                raise PasswordError(os_error(e))
            md5 = hashlib.md5(contents.encode('utf-8')).hexdigest()
            # Check that file has not changed.
            if md5 != get_setting(kind):
                warn("file contents have changed.", culprit=path)
                lines = wrap(
                    dedent("""\
                        This could result in passwords that are inconsistent with
                        those created in the past.  Use 'avendesora changed' to
                        assure that nothing has changed. Then, to suppress this
                        message, change {hashes} to contain:
                    """.format(hashes=get_setting('hashes_file'))))
                lines.append("     {kind} = '{md5}'".format(kind=kind,
                                                            md5=md5))
                codicil(*lines, sep='\n')
Beispiel #13
0
    def run(cls, command, args, settings, options):
        # read command line
        cmdline = docopt(cls.USAGE, argv=[command] + args)
        paths = cmdline['<path>']
        archive = cmdline['--archive']
        date = cmdline['--date']

        # remove initial / from paths
        src_dirs = [str(p).lstrip('/') for p in settings.src_dirs]
        new_paths = [p.lstrip('/') for p in paths]
        if paths != new_paths:
            for path in paths:
                if path.startswith('/'):
                    warn('removing initial /.', culprit=path)
            paths = new_paths

        # assure that paths correspond to src_dirs
        unknown_path = False
        for path in paths:
            if not any([path.startswith(src_dir) for src_dir in src_dirs]):
                unknown_path = True
                warn('unknown path.', culprit=path)
        if unknown_path:
            codicil('Paths should start with:', conjoin(src_dirs))

        # get the desired archive
        if date and not archive:
            archive = get_nearest_archive(settings, date)
            if not archive:
                raise Error('archive not available.', culprit=date)
        if not archive:
            archives = get_available_archives(settings)
            if not archives:
                raise Error('no archives are available.')
            archive = archives[-1]['name']
        output('Archive:', archive)

        # run borg
        borg = settings.run_borg(
            cmd='extract',
            args=[settings.destination(archive)] + paths,
            emborg_opts=options,
        )
        out = borg.stdout
        if out:
            output(out.rstrip())
Beispiel #14
0
    def __init__(self):
        self.collections = []

        def add(collection, i=None):
            is_available = collection.is_available()
            is_unique = collection.is_unique(self.collections)
            is_ignored = any(
                fnmatch(collection.name, x) for x in self.ignore_globs)

            if is_available and is_unique and not is_ignored:
                self.collections.insert(
                    len(self.collections) if i is None else i,
                    collection,
                )

        # Add directories found above the current working directory.
        cwd = Path.cwd().resolve()
        for parent in (cwd, *cwd.parents):
            for name in self.local_paths:
                add(PathCollection(parent / name))

        # Add specific directories specified by the user.
        for dir in self.global_paths:
            add(PathCollection(dir))

        # Add directories specified by plugins.
        for plugin in load_and_sort_plugins('stepwise.protocols'):
            try:
                add(PluginCollection(plugin))
            except AttributeError as err:
                warn(
                    f"no protocol directory specified for '{plugin.module_name}.{plugin.name}' plugin."
                )
                codicil(str(err))

        # Add the current working directory.

        # Do this after everything else, so it'll get bumped if we happen to be
        # in a directory represented by one of the other collections.  But put
        # it ahead of all the other collections, so that tags are evaluated for
        # local paths before anything else.

        add(CwdCollection(), 0)
Beispiel #15
0
def test_cartwheel():
    with messenger() as (msg, stdout, stderr, logfile):
        warn('hey now!', culprit='yo')
        codicil('baby', 'bird', sep='\n')
        warn('uh-huh\nuh-huh', culprit='yep yep yep yep yep yep yep yep yep yep yep')
        expected = dedent('''
            warning: yo: hey now!
                baby
                bird
            warning: yep yep yep yep yep yep yep yep yep yep yep:
                uh-huh
                uh-huh
        ''').strip()

        assert msg.errors_accrued() == 0
        assert errors_accrued(True) == 0
        assert strip(stdout) == expected
        assert strip(stderr) == ''
        assert log_strip(logfile) == dedent('''
            ack: invoked as: <exe>
            ack: invoked on: <date>
            {expected}
        ''').strip().format(expected=expected)
Beispiel #16
0
    def __init__(self, warnings=True):  # {{{2
        # initialize object {{{3
        self.loaded = set()
        self.name_index = {}
        self.name_manifests = {}
        self.url_manifests = {}
        self.title_manifests = {}
        self.shared_secrets = {}
        self.existing_names = {}

        # check permissions, dates on account files {{{3
        most_recently_updated = 0
        mask = get_setting('account_file_mask')
        for filename in get_setting('accounts_files', []):
            path = get_setting('settings_dir') / filename
            resolved_path = path.resolve()

            # check file permissions
            permissions = getmod(resolved_path)
            violation = permissions & mask
            if violation:
                recommended = permissions & ~mask & 0o777
                warn("file permissions are too loose.", culprit=path)
                codicil("Recommend running: chmod {:o} {}".format(recommended, resolved_path))

            # determine time of most recently updated account file
            updated = resolved_path.stat().st_mtime
            if updated > most_recently_updated:
                most_recently_updated = updated

        # check for missing or stale archive file {{{3
        archive_file = get_setting('archive_file')
        if archive_file and warnings:
            if archive_file.exists():
                resolved_path = archive_file.resolve()

                # check file permissions
                permissions = getmod(resolved_path)
                violation = permissions & mask
                if violation:
                    recommended = permissions & ~mask & 0o777
                    warn("file permissions are too loose.", culprit=path)
                    codicil("Recommend running: chmod {:o} {}".format(
                        recommended, resolved_path)
                    )

                # warn user if archive file is out of date
                stale = float(get_setting('archive_stale'))
                archive_updated = resolved_path.stat().st_mtime
                if most_recently_updated > archive_updated:
                    log('Avendesora archive is {:.0f} hours out of date.'.format(
                        (most_recently_updated - archive_updated)/3600
                    ))
                    # archive_age = time() - archive_updated
                    account_age = time() - most_recently_updated
                    if account_age > 86400 * stale:
                        warn('stale archive.')
                        codicil(dedent("""\
                            Recommend running 'avendesora changed' to determine
                            which account entries have changed, and if all the
                            changes are expected, running 'avendesora archive'
                            to update the archive.
                        """), wrap=True)
                else:
                    log('Avendesora archive is up to date.')
            else:
                warn('archive missing.')
                codicil(
                    "Recommend running 'avendesora archive'",
                    "to create the archive."
                )
Beispiel #17
0
    def read(self, name=None, path=None):
        """Recursively read configuration files.

        name (str):
            Name of desired configuration. Passed only when reading the top level
            settings file. Default is the default configuration as specified in the
            settings file, or if that is not specified then the first configuration
            given is used.
        path (str):
            Full path to settings file. Should not be given for the top level
            settings file (SETTINGS_FILE in CONFIG_DIR).
        """

        if path:
            settings = PythonFile(path).run()
            parent = path.parent
            includes = Collection(settings.get(INCLUDE_SETTING))
        else:
            # this is the generic settings file
            parent = self.config_dir
            if not parent.exists():
                # config dir does not exist, create and populate it
                narrate('creating config directory:', str(parent))
                parent.mkdir(mode=0o700, parents=True, exist_ok=True)
                for name, contents in [
                    (SETTINGS_FILE, INITIAL_SETTINGS_FILE_CONTENTS),
                    ('root', INITIAL_ROOT_CONFIG_FILE_CONTENTS),
                    ('home', INITIAL_HOME_CONFIG_FILE_CONTENTS),
                ]:
                    path = parent / name
                    path.write_text(contents)
                    path.chmod(0o600)
                output(
                    f'Configuration directory created: {parent!s}.',
                    'Includes example settings files. Edit them to suit your needs.',
                    'Search for and replace any fields delimited with << and >>.',
                    'Delete any configurations you do not need.',
                    'Generally you will use either home or root, but not both.',
                    sep='\n')
                done()

            path = PythonFile(parent, SETTINGS_FILE)
            settings_filename = path.path
            settings = path.run()

            config = get_config(name, settings, self.composite_config_allowed)
            settings['config_name'] = config
            self.config_name = config
            includes = Collection(settings.get(INCLUDE_SETTING))
            includes = [config] + list(includes.values())

        if settings.get('passphrase'):
            if getmod(path) & 0o077:
                warn("file permissions are too loose.", culprit=path)
                codicil(f"Recommend running: chmod 600 {path!s}")

        self.settings.update(settings)

        for include in includes:
            path = to_path(parent, include)
            self.read(path=path)

        # default src_dirs if not given
        if not self.settings.get('src_dirs'):
            self.settings['src_dirs'] = []

        # default archive if not given
        if 'archive' not in self.settings:
            if not 'prefix' in self.settings:
                self.settings[
                    'prefix'] = '{host_name}-{user_name}-{config_name}-'
            self.settings['archive'] = self.settings['prefix'] + '{{now}}'
Beispiel #18
0
    def read(self, name=None, path=None):
        """Recursively read configuration files.

        name (str):
            Name of desired configuration. Passed only when reading the top level
            settings file. Default is the default configuration as specified in the
            settings file, or if that is not specified then the first configuration
            given is used.
        path (str):
            Full path to settings file. Should not be given for the top level
            settings file (SETTINGS_FILE in CONFIG_DIR).
        """

        if path:
            settings = PythonFile(path).run()
            parent = path.parent
            includes = Collection(
                settings.get(INCLUDE_SETTING),
                split_lines,
                comment="#",
                strip=True,
                cull=True,
            )
        else:
            # this is the generic settings file
            parent = self.config_dir
            if not parent.exists():
                # config dir does not exist, create and populate it
                narrate("creating config directory:", str(parent))
                parent.mkdir(mode=0o700, parents=True, exist_ok=True)
                for name, contents in [
                    (SETTINGS_FILE, INITIAL_SETTINGS_FILE_CONTENTS),
                    ("root", INITIAL_ROOT_CONFIG_FILE_CONTENTS),
                    ("home", INITIAL_HOME_CONFIG_FILE_CONTENTS),
                ]:
                    path = parent / name
                    path.write_text(contents)
                    path.chmod(0o600)
                output(
                    f"Configuration directory created: {parent!s}.",
                    "Includes example settings files. Edit them to suit your needs.",
                    "Search for and replace any fields delimited with << and >>.",
                    "Delete any configurations you do not need.",
                    "Generally you will use either home or root, but not both.",
                    sep="\n",
                )
                done()

            path = PythonFile(parent, SETTINGS_FILE)
            self.settings_filename = path.path
            settings = path.run()

            config = get_config(name, settings, self.composite_config_response,
                                self.show_config_name)
            settings["config_name"] = config
            self.config_name = config
            includes = Collection(settings.get(INCLUDE_SETTING))
            includes = [config] + list(includes.values())

        if settings.get("passphrase"):
            if getmod(path) & 0o077:
                warn("file permissions are too loose.", culprit=path)
                codicil(f"Recommend running: chmod 600 {path!s}")

        self.settings.update(settings)

        # read include files, if any are specified
        for include in includes:
            path = to_path(parent, include)
            self.read(path=path)
Beispiel #19
0
    def read_config(self, name=None, path=None, queue=None):
        """Recursively read configuration files.

        name (str):
            Name of desired configuration. Passed only when reading the top level
            settings file. Default is the default configuration as specified in the
            settings file, or if that is not specified then the first configuration
            given is used.
        path (str):
            Full path to settings file. Should not be given for the top level
            settings file (SETTINGS_FILE in CONFIG_DIR).
        """

        if path:
            # we are reading an include file
            settings = PythonFile(path).run()
            parent = path.parent
            includes = Collection(
                settings.get(INCLUDE_SETTING),
                split_lines,
                comment="#",
                strip=True,
                cull=True,
            )
        else:
            # this is the base-level settings file
            parent = self.config_dir
            if not parent.exists():
                # config dir does not exist, create and populate it
                narrate("creating config directory:", str(parent))
                parent.mkdir(mode=0o700, parents=True, exist_ok=True)
                for fname, contents in [
                    (SETTINGS_FILE, INITIAL_SETTINGS_FILE_CONTENTS),
                    ("root", INITIAL_ROOT_CONFIG_FILE_CONTENTS),
                    ("home", INITIAL_HOME_CONFIG_FILE_CONTENTS),
                ]:
                    path = parent / fname
                    path.write_text(contents)
                    path.chmod(0o600)
                output(
                    f"Configuration directory created: {parent!s}.",
                    "Includes example settings files. Edit them to suit your needs.",
                    "Search for and replace any fields delimited with ⟪ and ⟫.",
                    "Delete any configurations you do not need.",
                    "Generally you will use either home or root, but not both.",
                    sep="\n",
                )
                done()

            # read the shared settings file
            path = PythonFile(parent, SETTINGS_FILE)
            self.settings_filename = path.path
            settings = path.run()

            # initialize the config queue
            if not queue:
                # this is a request through the API
                queue = ConfigQueue()
            if queue.uninitialized:
                queue.initialize(name, settings)
            config = queue.get_active_config()
            self.configs = queue.configs
            self.log_command = queue.log_command

            # save config name
            settings["config_name"] = config
            self.config_name = config
            if not config:
                # this happens on composite configs for commands that do not
                # need access to a specific config, such as help and configs
                self.settings.update(settings)
                return

            # get includes
            includes = Collection(settings.get(INCLUDE_SETTING))
            includes = [config] + list(includes.values())

        if settings.get("passphrase"):
            if getmod(path) & 0o077:
                warn("file permissions are too loose.", culprit=path)
                codicil(f"Recommend running: chmod 600 {path!s}")

        self.settings.update(settings)

        # read include files, if any are specified
        for include in includes:
            path = to_path(parent, include)
            self.read_config(path=path)