def publish_passcode(self): passcommand = self.passcommand passcode = self.passphrase # process passcomand if passcommand: if passcode: warn('passphrase unneeded.', culprit='passcommand') return dict(BORG_PASSCOMMAND=passcommand) # get passphrase from avendesora if not passcode and self.avendesora_account: narrate('running avendesora to access passphrase.') try: from avendesora import PasswordGenerator pw = PasswordGenerator() account = pw.get_account(self.value('avendesora_account')) field = self.value('avendesora_field', None) passcode = str(account.get_value(field)) except ImportError: raise Error('Avendesora is not available', 'you must specify passphrase in settings.', sep=', ') if passcode: return dict(BORG_PASSPHRASE=passcode) if self.encryption is None: self.encryption = 'none' if self.encryption == 'none': narrate('passphrase is not available, encryption disabled.') return {} raise Error('Cannot determine the encryption passphrase.')
def from_text(cls, text): """ Read a protocol from the given text. See `Protocol.parse()` for more information. This function adds some additional error handling and sanity checking. """ io = cls() try: io.protocol = Protocol.parse(text) except ParseError as err: if not cls.all_errors: warn( "the protocol could not be properly rendered due to error(s):" ) err.report(informant=warn) io.protocol = err.content io.errors = 1 else: io.protocol.set_current_date() if not io.protocol.steps: warn("protocol is empty.", culprit=inform.get_culprit()) return io
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline["<mount_point>"] if mount_point: mount_point = settings.to_path(mount_point, resolve=False) else: mount_point = settings.as_path("default_mount_point") if not mount_point: raise Error("must specify directory to use as mount point") # run borg try: settings.run_borg( cmd="umount", args=[mount_point], emborg_opts=options, ) try: mount_point.rmdir() except OSError as e: warn(os_error(e)) except Error as e: if "busy" in str(e): e.reraise( codicil= f"Try running 'lsof +D {mount_point!s}' to find culprit.")
def run_borg(self, cmd, args='', borg_opts=None, emborg_opts=()): # prepare the command os.environ.update(self.publish_passcode()) os.environ['BORG_DISPLAY_PASSPHRASE'] = 'no' if self.ssh_command: os.environ['BORG_RSH'] = self.ssh_command executable = self.value('borg_executable', BORG) if borg_opts is None: borg_opts = self.borg_options(cmd, emborg_opts) command = ([executable] + cmd.split() + borg_opts + (args.split() if is_str(args) else args)) environ = { k: v for k, v in os.environ.items() if k.startswith('BORG_') } if 'BORG_PASSPHRASE' in environ: environ['BORG_PASSPHRASE'] = '<redacted>' narrate('setting environment variables:', render(environ)) # check if ssh agent is present if self.needs_ssh_agent: for ssh_var in 'SSH_AGENT_PID SSH_AUTH_SOCK'.split(): if ssh_var not in os.environ: warn( 'environment variable not found, is ssh-agent running?', culprit=ssh_var) # run the command narrate('running:\n{}'.format( indent(render_command(command, borg_options_arg_count)))) narrating = 'verbose' in emborg_opts or 'narrate' in emborg_opts modes = 'soeW' if narrating else 'sOEW' return Run(command, modes=modes, stdin='', env=os.environ, log=False)
def add_songs(self, paths, cwd='.'): for path in paths: path = Path(cwd, path).expanduser() if path.is_file(): ext = path.suffix.lower() if ext in media_file_extensions: self.songs += [path] elif ext == '.m3u': try: playlist = path.read_text() lines = [l.strip() for l in playlist.splitlines()] self.add_songs( [l for l in lines if l and l[0] != '#'], path.parent ) except OSError as e: raise Error(os_error(e)) elif path.stem != restart_path.stem: if not self.informer.quiet: warn('skipping descriptor of unknown type.', culprit=path) elif path.is_dir(): self.add_songs(path.iterdir()) else: if not self.informer.quiet: warn('not found.', culprit=path) if not self.songs: raise Error('playlist is empty.')
def read_manifests(self): # {{{2 if self.name_index: return cache_dir = get_setting('cache_dir') manifests_path = cache_dir / MANIFESTS_FILENAME try: encrypted = manifests_path.read_bytes() user_key = get_setting('user_key') if not user_key: raise Error('no user key.') key = base64.urlsafe_b64encode(sha256(user_key.encode('ascii')).digest()) fernet = Fernet(key) contents = fernet.decrypt(encrypted) try: cache = pickle.loads(contents, **PICKLE_ARGS) self.name_manifests = cache['names'] self.url_manifests = cache['urls'] self.title_manifests = cache['titles'] # build the name_index by inverting the name_manifests self.name_index = { h:n for n,l in self.name_manifests.items() for h in l } except (ValueError, pickle.UnpicklingError) as e: warn('garbled manifest.', culprit=manifests_path, codicil=str(e)) manifests_path.unlink() assert isinstance(self.name_index, dict) except OSErrors as e: comment(os_error(e)) except Exception as e: comment(e)
def fail(self, *msg, comment=''): msg = full_stop(' '.join(str(m) for m in msg)) try: if self.notify: Run(['mail', f'-s "{PROGRAM_NAME}: {msg}"', self.notify], stdin=dedent(f''' {msg} {comment} config = {self.config_name} source = {hostname}:{self.src_dir} destination = {self.dest_server}:{self.dest_dir} ''').lstrip(), modes='soeW') except OSError as e: pass try: if self.notifier: Run(self.notifier.format( msg=msg, host_name=hostname, user_name=username, prog_name=PROGRAM_NAME, ), modes='soeW') except OSError as e: pass except KeyError as e: warn('unknown key.', culprit=(self.settings_file, 'notifier', e)) raise Error(msg)
def read_confs(self): # read the .conf files in our config directory (except for hosts.conf) for name in "ssh networks locations proxies".split(): conf_file = to_path(CONFIG_DIR, name + ".conf") if conf_file.exists(): settings = PythonFile(conf_file).run() overlap = settings.keys() & self.settings.keys() overlap -= sshconfig_names overlap = [k for k in overlap if not k.startswith("_")] if overlap: warn("conflicting settings:", conjoin(overlap), culprit=conf_file) self.settings.update(settings) self.ssh_config_file = to_path( self.settings.get("CONFIG_FILE", SSH_CONFIG_FILE)) if not self.ssh_config_file.is_absolute(): raise Error( "path to SSH config file should be absolute.", culprit=self.ssh_config_file, ) self.ssh_defaults = self.settings.get("DEFAULTS", "") self.ssh_overrides = self.settings.get("OVERRIDES", "") self.preferred_networks = self.settings.get("PREFERRED_NETWORKS", []) self.locations = self.settings.get("LOCATIONS", {}) self.proxies = self.settings.get("PROXIES", {}) self.available_ciphers = self.settings.get("AVAILABLE_CIPHERS") self.available_macs = self.settings.get("AVAILABLE_MACS") self.available_host_key_algorithms = self.settings.get( "AVAILABLE_HOST_KEY_ALGORITHMS") self.available_kex_algorithms = self.settings.get( "AVAILABLE_KEX_ALGORITHMS")
def initialize(cls, gpg_path=None, gpg_home=None, armor=None ): from .config import get_setting, override_setting cls.gpg_path = to_path( gpg_path if gpg_path else get_setting('gpg_executable') ) override_setting('gpg_executable', cls.gpg_path) cls.gpg_home = to_path( gpg_home if gpg_home else get_setting('gpg_home') ) override_setting('gpg_home', cls.gpg_home) armor = armor if armor is not None else get_setting('gpg_armor') if armor not in ARMOR_CHOICES: warn( "'%s' is not valid, choose from %s." % ( armor, conjoin(ARMOR_CHOICES) ), culprit=(get_setting('config_file'), 'gpg_armor') ) armor = None cls.armor = armor override_setting('gpg_armor', armor) gpg_args = {} if cls.gpg_path: gpg_args.update({'gpgbinary': str(cls.gpg_path)}) if cls.gpg_home: gpg_args.update({'gnupghome': str(cls.gpg_home)}) cls.gpg = gnupg.GPG(**gpg_args)
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline['<mount_point>'] if not mount_point: mount_point = settings.value('default_mount_point') if not mount_point: raise Error('must specify directory to use as mount point') mount_point = to_path(mount_point) # run borg try: settings.run_borg( cmd='umount', args=[mount_point], emborg_opts=options, ) try: to_path(mount_point).rmdir() except OSError as e: warn(os_error(e)) except Error as e: if 'busy' in str(e): e.reraise( codicil= f"Try running 'lsof +D {mount_point}' to find culprit.")
def __new__(cls, server, include_file, bypass, trial_run): if server in AuthKeys.known: self = AuthKeys.known[server] if include_file != self.include_file: warn( 'inconsistent remote include file:', fmt('{include_file} != {self.include_file} in {server}.') ) return self self = super(AuthKeys, cls).__new__(cls) AuthKeys.known[server] = self self.server = server self.bypass = bypass self.trial_run = trial_run self.keys = {} self.comment = {} self.restrictions = {} self.include_file = include_file self.include = None # get remote include file if it exists if include_file and not bypass: narrate(fmt(' retrieving remote include file from {server}.')) try: try: run_sftp(self.server, [ fmt('get .ssh/{inc} {inc}.{server}', inc=include_file) ]) self.include = to_path(include_file + '.' + server).read_text() except OSError as err: comment(fmt(' sftp {server}: {include_file} not found.')) except OSError as err: error(os_error(err)) return self
def initialize(cls, gpg_path=None, gpg_home=None, armor=None): cls.gpg_path = to_path( gpg_path if gpg_path else get_setting('gpg_executable')) override_setting('gpg_executable', cls.gpg_path) cls.gpg_home = to_path( gpg_home if gpg_home else get_setting('gpg_home')) override_setting('gpg_home', cls.gpg_home) armor = armor if armor is not None else get_setting('gpg_armor') if armor not in ARMOR_CHOICES: warn("'%s' is not valid, choose from %s." % (armor, conjoin(ARMOR_CHOICES)), culprit=setting_path('gpg_armor')) armor = 'extension' cls.armor = armor override_setting('gpg_armor', armor) gpg_args = {} if cls.gpg_path: gpg_args.update({'gpgbinary': str(cls.gpg_path)}) if cls.gpg_home: gpg_args.update({'gnupghome': str(cls.gpg_home)}) try: cls.gpg = gnupg.GPG(**gpg_args) except ValueError as e: fatal(e)
def __init__(self, config=None, emborg_opts=(), _queue=None): self.settings = dict() self.do_not_expand = () self.emborg_opts = emborg_opts self.version = tuple(int(p) for p in __version__.split('.')) # reset the logfile so anything logged after this is placed in the # logfile for this config get_informer().set_logfile(LoggingCache()) self.config_dir = to_path(CONFIG_DIR) self.read_config(name=config, queue=_queue) self.check() set_shlib_prefs( encoding=self.encoding if self.encoding else DEFAULT_ENCODING) self.hooks = Hooks(self) self.borg_ran = False # set colorscheme if self.colorscheme: colorscheme = self.colorscheme.lower() if colorscheme == 'none': get_informer().colorscheme = None elif colorscheme in ('light', 'dark'): get_informer().colorscheme = colorscheme else: warn(f'unknown colorscheme: {self.colorscheme}.')
def read_defaults(self): settings = {} try: from appdirs import user_config_dir config_file = to_path(user_config_dir('vdiff'), 'config') try: code = config_file.read_text() try: compiled = compile(code, str(config_file), 'exec') exec(compiled, settings) except Exception as e: error(e, culprit=config_file) except FileNotFoundError: pass except OSError as e: warn(os_error(e)) if self.useGUI is not None: settings['gui'] = self.useGUI except ImportError: pass if settings.get('gui', DEFAULT_GUI): if 'DISPLAY' not in os.environ: warn('$DISPLAY not set, ignoring request for gvim.') else: self.cmd = settings.get('gvimdiff', DEFAULT_GVIM) return self.cmd = settings.get('vimdiff', DEFAULT_VIM)
def fail(self, *msg, comment=''): msg = full_stop(' '.join(str(m) for m in msg)) try: if self.notify: Run(['mail', '-s', f'{PROGRAM_NAME} on {hostname}: {msg}'] + self.notify.split(), stdin=dedent(f''' {msg} {comment} config = {self.config_name} source = {username}@{hostname}:{', '.join(str(d) for d in self.src_dirs)} destination = {self.repository} ''').lstrip(), modes='soeW') except Error: pass try: if self.notifier: Run(self.notifier.format( msg=msg, hostname=hostname, user_name=username, prog_name=PROGRAM_NAME, ), modes='soeW') except Error: pass except KeyError as e: warn('unknown key.', culprit=(self.settings_file, 'notifier', e)) raise Error(msg)
def generate(self, field_name, field_key, account): try: if self.secret: return except AttributeError: pass account_name = account.get_name() account_seed = account.get_seed() if self.master is None: master = account.get_field("master", default=None) master_source = account.get_field("_master_source", default=None) else: master = self.master master_source = "secret" if not master: master = get_setting("user_key") master_source = "user_key" if not master: try: try: master = getpass.getpass("master password for %s: " % account_name) master_source = "user" except EOFError: output() if not master: warn("master password is empty.") except (EOFError, KeyboardInterrupt): terminate() log("Generating secret, source of master seed:", master_source) field_key = self.get_key(field_key) if self.version: version = self.version else: version = account.get_field("version", default="") if account.request_seed(): try: try: interactive_seed = getpass.getpass("seed for %s: " % account_name) except EOFError: output() if not interactive_seed: warn("seed is empty.") except (EOFError, KeyboardInterrupt): terminate() else: interactive_seed = "" seeds = [master, account_seed, field_name, field_key, version, interactive_seed] key = " ".join([str(seed) for seed in seeds]) # Convert the key into 512 bit number digest = hashlib.sha512((key).encode("utf-8")).digest() bits_per_byte = 8 radix = 1 << bits_per_byte bits = 0 for byte in digest: bits = radix * bits + byte self.pool = bits
def read_config(): if Config.get('READ'): return # already read # First open the config file from .gpg import PythonFile path = get_setting('config_file') assert path.suffix.lower() not in ['.gpg', '.asc'] config_file = PythonFile(path) try: contents = config_file.run() for k, v in contents.items(): if k.startswith('_'): continue if k not in CONFIG_DEFAULTS: warn('%s: unknown.' % k, culprit=config_file) continue if k.endswith('_executable'): argv = v.split() if is_str(v) else list(v) path = to_path(argv[0]) if not path.is_absolute(): warn( 'should use absolute path for executables.', culprit=(config_file, k) ) Config[k] = v Config['READ'] = True except Error as err: comment('not found.', culprit=config_file) # Now open the hashes file hashes_file = PythonFile(get_setting('hashes_file')) try: contents = hashes_file.run() Config.update({k.lower(): v for k,v in contents.items()}) except Error as err: pass # Now open the account list file account_list_file = PythonFile(get_setting('account_list_file')) try: contents = account_list_file.run() Config.update({k.lower(): v for k,v in contents.items()}) except Error as err: pass # initilize GPG from .gpg import GnuPG GnuPG.initialize() # Now read the user key file user_key_file = get_setting('user_key_file') if user_key_file: user_key_file = PythonFile(get_setting('user_key_file')) try: contents = user_key_file.run() Config.update({k.lower(): v for k,v in contents.items()}) except Error as err: pass
def run_duplicity(cmd, settings, narrating): os.environ.update(publish_passcode(settings)) for ssh_var in 'SSH_AGENT_PID SSH_AUTH_SOCK'.split(): if ssh_var not in os.environ: warn('environment variable not found, is ssh-agent running?', culprit=ssh_var) narrate('running:\n{}'.format(indent(render_command(cmd)))) modes = 'soeW' if narrating else 'sOEW' Run(cmd, modes=modes, env=os.environ)
def now_playing(self): if self.now_playing_path: out = [each for each in [self.artist, self.title] if each] try: self.now_playing_path.write_text(' - '.join(out)) except OSError as e: if not self.warned: warn(os_error(e)) self.warned = True
def gather_public_keys(self): comment(' gathering public keys') keyname = self.keyname data = self.data clients = conjoin(self.data.get('clients', [])) default_purpose = fmt('This key allows access from {clients}.') purpose = self.data.get('purpose', default_purpose) servers = self.data.get('servers', []) prov = '.provisional' if self.trial_run else '' # read contents of public key try: pubkey = to_path(keyname + '.pub') key = pubkey.read_text().strip() except OSError as err: narrate('%s, skipping.' % os_error(err)) return # get fingerprint of public key try: keygen = Run(['ssh-keygen', '-l', '-f', pubkey], modes='wOeW') fields = keygen.stdout.strip().split() fingerprint = ' '.join([fields[0], fields[1], fields[-1]]) except OSError as err: error(os_error(err)) return # contribute commented and restricted public key to the authorized_key # file for each server for server in servers: if self.update and server not in self.update: continue if server in self.skip: continue server_data = servers[server] description = server_data.get('description', None) restrictions = server_data.get('restrictions', []) remarks = [ '# %s' % t for t in cull([purpose, description, self.warning, fingerprint]) if t ] include_file = server_data.get( 'remote-include-filename', data['remote-include-filename'] ) bypass = server_data.get('bypass') authkeys = AuthKeys(server, include_file, bypass, self.trial_run) authkeys.add_public_key(keyname, key, remarks, restrictions) if not servers: warn( 'no servers specified, you must update them manually.', culprit=keyname )
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_slice(): with messenger() as (msg, stdout, stderr, logfile): warn('aaa bbb ccc', codicil=('000 111 222', '!!! @@@ ###')) 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 initialize(cls, interactive_seed=False): cls._interactive_seed = interactive_seed log("initializing", cls.get_name()) try: if cls.master.is_secure(): if not cls._file_info.encrypted: warn( "high value master password not contained in encrypted", "account file.", culprit=cls.get_name() ) except AttributeError as err: pass
def publish_passcode(self): for v in ['BORG_PASSPHRASE', 'BORG_PASSCOMMAND', 'BORG_PASSPHRASE_FD']: if v in os.environ: narrate(f"Using existing {v}.") return passcommand = self.value('passcommand') passcode = self.passphrase # process passcomand if passcommand: if passcode: warn("passphrase unneeded.", culprit="passcommand") narrate(f"Setting BORG_PASSCOMMAND.") os.environ['BORG_PASSCOMMAND'] = passcommand self.borg_passcode_env_var_set_by_emborg = 'BORG_PASSCOMMAND' return # get passphrase from avendesora if not passcode and self.avendesora_account: narrate("running avendesora to access passphrase.") try: from avendesora import PasswordGenerator pw = PasswordGenerator() account_spec = self.value("avendesora_account") if ':' in account_spec: passcode = str(pw.get_value(account_spec)) else: account = pw.get_account(self.value("avendesora_account")) field = self.value("avendesora_field", None) passcode = str(account.get_value(field)) except ImportError: raise Error( "Avendesora is not available", "you must specify passphrase in settings.", sep=", ", ) if passcode: os.environ['BORG_PASSPHRASE'] = passcode narrate(f"Setting BORG_PASSPHRASE.") self.borg_passcode_env_var_set_by_emborg = 'BORG_PASSPHRASE' return if self.encryption is None: self.encryption = "none" if self.encryption == "none" or self.encryption.startswith( 'authenticated'): comment("Encryption is disabled.") return raise Error("Cannot determine the encryption passphrase.")
def get_psf_filename(psf_file): if not psf_file: try: with open(saved_psf_file_filename) as f: psf_file = f.read().strip() display('Using PSF file:', psf_file) except OSError: fatal('missing PSF file name.') try: with open(saved_psf_file_filename, 'w') as f: f.write(psf_file) except OSError as e: warn(os_error(e)) return psf_file
def publish(self): narrate('publishing authorized_keys to', self.server) prov = '.provisional' if self.trial_run else '' entries = [ fmt("# This file was generated by sshdeploy on {date}.") ] if self.include: entries += [ '\n'.join([ fmt('# Contents of {self.include_file}:'), self.include ]) ] for name in sorted(self.keys.keys()): key = self.keys[name] comment = self.comment[name] comment = [comment] if is_str(comment) else comment restrictions = self.restrictions[name] if not is_str(restrictions): restrictions = ','.join(restrictions) restricted_key = ' '.join(cull([restrictions, key])) entries.append('\n'.join(comment + [restricted_key])) # delete any pre-existing provisional files # the goal here is to leave a clean directory when not trial-run try: run_sftp(self.server, [ fmt('rm .ssh/authorized_keys.provisional') ]) except OSError as err: pass # now upload the new authorized_keys file try: authkey = to_path('authorized_keys.%s' % self.server) with authkey.open('w') as f: f.write('\n\n'.join(entries) + '\n') authkey.chmod(0o600) if self.bypass: warn( 'You must manually upload', fmt('<keydir>/authorized_keys.{self.server}.'), culprit=self.server ) else: run_sftp(self.server, [ fmt('put -p {authkey} .ssh/authorized_keys{prov}') ]) except OSError as err: error(os_error(err))
def borg_options(self, cmd, options): # handle special cases first {{{3 args = [] if 'verbose' in options: args.append('--verbose') if 'trial-run' in options and cmd in commands_with_dryrun: args.append('--dry-run') if cmd == 'create': if 'verbose' in options: args.append('--list') if 'trial-run' not in options: args.append('--stats') for path in render_paths(self.values('excludes')): args.extend(['--exclude', path]) if cmd == 'extract': if 'verbose' in options: args.append('--list') if cmd == 'init': if self.passphrase or self.avendesora_account: encryption = self.encryption if self.encryption else 'repokey' args.append(f'--encryption={encryption}') if encryption == 'none': warn( 'passphrase given but not needed as encryption set to none.' ) if encryption in 'keyfile keyfile-blake2'.split(): warn("you should use 'borg export key' to export the", "encryption key, and then keep that key in a safe", "place. If you lose the key you will lose access to", "your backups.", wrap=True) else: encryption = self.encryption if self.encryption else 'none' if encryption != 'none': raise Error('passphrase not specified.') args.append(f'--encryption={encryption}') # add the borg command line options appropriate to this command {{{3 for name, attrs in BORG_SETTINGS.items(): if cmd in attrs['cmds'] or 'all' in attrs['cmds']: opt = convert_name_to_option(name) val = self.settings.get(name) if val: if 'arg' in attrs and attrs['arg']: args.extend([opt, str(val)]) else: args.extend([opt]) return args
def test_culprits(culprits, culprits_as_str): with messenger(culprit_sep='.') as (msg, stdout, stderr, logfile): stimulus = 'hey now!' expected = 'warning: {}{}'.format(culprits_as_str, stimulus) warn(stimulus, culprit=culprits) 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: log opened on <date> {expected} ''').strip().format(expected=expected)
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 list_signals(): # Read command line {{{2 cmdline = docopt(__doc__) args = cmdline['<signal>'] if not args: args = ['*'] psf_file = get_psf_filename(cmdline['--psf-file']) show_meta = cmdline['--long'] use_cache = not cmdline['--no-cache'] # List signals {{{2 try: psf = PSF(psf_file, sep=':', use_cache=use_cache) if show_meta: nw = uw = kw = 0 # name width, units width, kind width data = [] for name in expand_args(psf.signals.keys(), args, allow_diff=False): if name not in psf.signals: warn('not found.', culprit=name) signal = psf.get_signal(name) if len(signal.name) > nw: nw = len(signal.name) units = psf.units_to_unicode(signal.units) if len(units) > uw: uw = len(units) kind = signal.type.kind kind = kinds.get(kind, kind) if len(kind) > kw: kw = len(kind) try: points = len(signal.ordinate) except TypeError: points = None data.append((signal.name, units, kind, points)) if not data: raise Error(f'{plural(args):no match/es}.', culprit=args) for name, units, kind, points in data: if points is None: display(f' {name:<{nw}} {units:<{uw}} {kind}') else: display(f' {name:<{nw}} {units:<{uw}} {kind:<{kw}} ({points} points)') else: signals = expand_args(psf.signals.keys(), args, allow_diff=False) if not signals: raise Error(f'{plural(args):no match/es}.', culprit=args) display(columns(signals)) except Error as e: e.terminate()
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 get_seed(cls): # need to handle case where stdin/stdout is not available. # perhaps write generic password getter that supports both gui and tui. # Then have global option that indicates which should be used. # Separate name from seed. Only request seed when generating a password. import getpass try: name = getpass.getpass("account name: ") except EOFError: output() name = "" if not name: warn("null account name.") return name
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 _format_field(self, field): comment_leader = "\n # " key, value, desc = field if key.lower() not in SSH_SETTINGS: warn('unknown SSH setting.', culprit=key) key = SSH_SETTINGS.get(key.lower(), key) if value is True: value = 'yes' elif value is False: value = 'no' text = " {} {}".format(key, value) if desc: if not isinstance(desc, list): desc = [desc] text += comment_leader + comment_leader.join(desc) return text
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 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 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 fail(self, *msg, cmd='<unknown>'): msg = join(*msg) try: msg = msg.decode('ascii', errors='replace') except AttributeError: pass try: if self.notify and not Color.isTTY(): Run( [ "mail", "-s", f"{PROGRAM_NAME} failed on {username}@{hostname}" ] + self.notify.split(), stdin=dedent(f"""\ {PROGRAM_NAME} fails. command: {cmd} config: {self.config_name} source: {username}@{fullhostname}:{', '.join(str(d) for d in self.src_dirs)} destination: {self.repository!s} error message: """) + indent(msg) + "\n", modes="soeW", encoding="ascii", ) except Error: pass try: notifier = self.settings.get("notifier") # don't use self.value as we don't want arguments expanded yet if notifier and not Color.isTTY(): Run(self.notifier.format( cmd=cmd, msg=msg, hostname=hostname, user_name=username, prog_name=PROGRAM_NAME, ), modes="SoeW" # need to use the shell as user will generally quote msg ) except Error: pass except KeyError as e: warn("unknown key.", culprit=(self.settings_file, "notifier", e))
def run(cls, command, args, settings, options): # read command line docopt(cls.USAGE, argv=[command] + args) # warn user about relative source directories. for src_dir in settings.src_dirs: if not src_dir.is_absolute(): warn('relative source directory.', culprit=src_dir) # run borg borg = settings.run_borg( cmd='init', args=[settings.destination()], emborg_opts=options, ) out = borg.stdout if out: output(out.rstrip())
def test_possess(): with messenger(stream_policy='header') as (msg, stdout, stderr, logfile): out = [ 'hey now!', 'hey now!', ] err = [ 'Aiko aiko all day', 'jockomo feeno na na nay', 'jockomo feena nay.', ] display(*out) warn(*err, sep=', ') assert msg.errors_accrued() == 0 assert errors_accrued(True) == 0 assert strip(stdout) == ' '.join(out) assert strip(stderr) == 'warning: ' + ', '.join(err)
def get_seed(cls): if cls._stealth_name: # In this case we are running using API rather than running from # command line and the account names was specified to get_account(). return cls._stealth_name # need to handle case where stdin/stdout is not available. # perhaps write generic password getter that supports both gui and tui. # Then have global option that indicates which should be used. # Separate name from seed. Only request seed when generating a password. import getpass try: name = getpass.getpass('account name: ') except EOFError: output() name = '' if not name: warn('null account name.') return name
def get_field(cls, name, key=None, default=False): """Get Field Value Return value from the account given a field name and key. """ value = cls.__dict__.get(name) if value is None: if default is False: raise Error("not found.", culprit=(cls.get_name(), cls.combine_name(name, key))) else: return default if key is None: if is_collection(value): choices = [] for k, v in Collection(value).items(): try: choices.append(" %s: %s" % (k, v.get_key())) except AttributeError: choices.append(" %s:" % k) raise Error( "composite value found, need key. Choose from:", *choices, sep="\n", culprit=name, is_collection=True, collection=value ) else: try: if is_collection(value): value = value[key] else: warn("not a composite value, key ignored.", culprit=name) key = None except (IndexError, KeyError, TypeError): raise Error("not found.", culprit=cls.combine_name(name, key)) # generate the value if needed try: value.generate(name, key, cls) except AttributeError as err: pass return value
def generate(self): comment(' creating key') keyname = self.keyname data = self.data servers = self.data.get('servers', []) opts = data['keygen-options'] account_name = data['abraxas-account'] if account_name: pw.get_account(account_name) passcode = pw.generate_password() description = fmt("{keyname} (created {date})") else: passcode = '' self.warning = 'This key is not protected with a passcode!' description = fmt("{keyname} (created {date} -- no passcode!)") # warn user if they have a key with no passcode and no restrictions for server in servers: server_data = servers[server] restrictions = server_data.get('restrictions') if not restrictions: warn( 'unprotected key being sent to', server, 'without restrictions.' ) args = ['-C', description, '-f', keyname] + opts.split() try: comment(' running:', 'ssh-keygen', *args) keygen = pexpect.spawn('ssh-keygen', args, timeout=None) keygen.expect('Enter passphrase.*: ') keygen.sendline(passcode) keygen.expect('Enter same passphrase again: ') keygen.sendline(passcode) keygen.expect(pexpect.EOF) keygen.close() except pexpect.ExceptionPexpect as err: fatal(err) # remove group/other permissions from keyfiles to_path(keyname).chmod(0o600) to_path(keyname + '.pub').chmod(0o600)
def display_field(self, account, field): name, key = account.split_name(field) is_secret = account.is_secret(name, key) try: value = account.get_field(name, key) tvalue = dedent(str(value)).strip() except Error as err: err.terminate() sep = ' ' if '\n' in tvalue: if is_secret: warn( 'secret contains newlines, will not be fully concealed.', culprit=key ) else: tvalue = indent(dedent(tvalue), get_setting('indent')).strip('\n') sep = '\n' # build output string label = account.combine_name(name, key) log('Writing to TTY:', label) try: alt_name = value.get_key() if alt_name: label += ' (%s)' % alt_name except AttributeError: pass text = LabelColor(label + ':') + sep + tvalue if is_secret: try: cursor.write(text) cursor.conceal() sleep(get_setting('display_time')) except KeyboardInterrupt: pass cursor.reveal() cursor.clear() else: output(text)