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."),
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
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)
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))
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) == ''
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) == ''
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
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)
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())
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())
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)
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')
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())
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)
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)
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." )
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}}'
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)
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)