예제 #1
0
    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)
예제 #2
0
    def verify(self):
        if self.bypass:
            narrate('skipping post update connection test for', self.server)
            return

        comment('%s: post update connection test.' % (self.server))
        test_access(self.server)
예제 #3
0
    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
예제 #4
0
def run_sftp(server, cmds):
    cmd = ['sftp', '-q', '-b', '-', server]
    comment(fmt('    sftp {server}:'), '; '.join(cmds))
    try:
        Run(cmd, stdin='\n'.join(cmds), modes='sOEW')
    except KeyboardInterrupt:
        display('Continuing')
예제 #5
0
 def converter(cls, to, data):
     try:
         # SYMBOL is not unique
         #return UnitConversion(to, (cls.SYMBOL, cls.UNITS), data[cls.UNITS][to[-1]])
         return UnitConversion(to, cls.UNITS, data[cls.UNITS][to[-1]])
     except KeyError as e:
         comment(f'missing price in {e}.', culprit=cls.UNITS)
예제 #6
0
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
예제 #7
0
def clean(host):
    try:
        narrate(fmt('Cleaning {host}.'))
        run_sftp(host, ['rm .ssh/*.provisional'])
    except OSError as err:
        if 'no such file or directory' in str(err).lower():
            comment(os_error(err))
        else:
            error('cannot connect.', culprit=host)
예제 #8
0
파일: key.py 프로젝트: KenKundert/sshdeploy
    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
            )
예제 #9
0
    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.")
예제 #10
0
파일: key.py 프로젝트: KenKundert/sshdeploy
    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)
예제 #11
0
def test_access(host):
    try:
        narrate(fmt('Testing connection to {host}.'))
        payload = fmt('test payload for {host}')
        ref = to_path('.ref')
        test = to_path('.test')
        ref.write_text(payload)
        rm(test)
        run_sftp(host, [
            fmt('put {ref}'),
            fmt('get {ref} {test}'),
            fmt('rm {ref}')
        ])
        if test.read_text() == payload:
            comment('connection successful.', culprit=host)
        else:
            error('cannot connect.', culprit=host)
    except OSError as err:
        error('cannot connect.', culprit=host)
    rm(ref, test)
예제 #12
0
    def publish_passcode(self):
        passcommand = self.value('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":
            comment("passphrase is not available, encryption disabled.")
            return {}
        raise Error("Cannot determine the encryption passphrase.")
예제 #13
0
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)
    if not config_file.exists():
        # have not yet initialized this account
        return
    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 = 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 PasswordError:
        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 PasswordError:
        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 PasswordError:
        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() if not k.startswith('__')
            })
        except PasswordError:
            pass

    # Set the user-selected colors
    Config['_label_color'] = Color(color=get_setting('label_color'),
                                   scheme=get_setting('color_scheme'),
                                   enable=Color.isTTY())
    Config['_highlight_color'] = Color(color=get_setting('highlight_color'),
                                       scheme=get_setting('color_scheme'),
                                       enable=Color.isTTY())
예제 #14
0
    def run(cls, command, args, settings, options):
        # read command line
        cmdline = docopt(cls.USAGE, argv=[command] + args)
        archive = cmdline["--archive"]
        date = cmdline["--date"]

        # 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
        borg = settings.run_borg(
            cmd="list",
            args=[settings.destination(archive)],
            emborg_opts=options,
        )
        out = borg.stdout

        # define available formats
        formats = dict(
            name="{path}",
            date="{day} {date} {time} {path}",
            size="{Size:<5.2r} {path}",
            owner="{owner:<8} {path}",
            group="{group:<8} {path}",
            long="{Size:<5.2r} {date} {time} {path}",
            full=
            "{permissions:<10} {owner:<6} {group:<6} {size:>8} {Date:YYMMDD HH:mm} {path}",
        )
        user_formats = settings.manifest_formats
        if user_formats:
            unknown = user_formats.keys() - formats.keys()
            if unknown:
                warn("unknown formats:",
                     ", ".join(unknown),
                     culprit="manifest_formats")
            formats.update(user_formats)

        # process sort options
        if cmdline["--sort-by-name"]:
            fmt = "name"

            def get_key(columns):
                return columns[7]

        elif cmdline["--sort-by-date"]:
            fmt = "date"

            def get_key(columns):
                date_time = " ".join(columns[5:7])
                return arrow.get(date_time, "YYYY-MM-DD HH:mm:ss")

        elif cmdline["--sort-by-size"]:
            fmt = "size"

            def get_key(columns):
                return int(columns[3])

        elif cmdline["--sort-by-owner"]:
            fmt = "owner"

            def get_key(columns):
                return columns[1]

        elif cmdline["--sort-by-group"]:
            fmt = "group"

            def get_key(columns):
                return columns[2]

        else:
            fmt = None
            get_key = None

        # process format options
        if cmdline["--name-only"]:
            fmt = "name"
        elif cmdline["--long"]:
            fmt = "long"
        elif cmdline["--full"]:
            fmt = "full"

        # sort the output
        lines = [l.split(maxsplit=7) for l in out.rstrip().splitlines()]
        if get_key:
            lines = sorted(lines, key=get_key)
        if cmdline["--reverse-sort"]:
            lines.reverse()

        # echo borg output if no formatting is necessary
        if not fmt:
            output(out)
            return

        # import QuantiPhy
        try:
            from quantiphy import Quantity

            Quantity.set_prefs(spacer="")
        except ImportError:
            comment("Could not import QuantiPhy.")

            def Quantity(value, units):
                return value

        # generate formatted output
        for columns in lines:
            output(
                formats[fmt].format(
                    permissions=columns[0],
                    owner=columns[1],
                    group=columns[2],
                    size=columns[3],
                    Size=Quantity(columns[3], "B"),
                    Date=arrow.get(" ".join(columns[5:7]),
                                   "YYYY-MM-DD HH:mm:ss"),
                    day=columns[4].rstrip(","),
                    date=columns[5],
                    time=columns[6],
                    path=columns[7],
                ),
                sep="\n",
            )
예제 #15
0
def main():
    try:
        # Read command line {{{1
        cmdline = docopt(__doc__)
        keys = cmdline["--keys"].split(",") if cmdline["--keys"] else []
        update = cmdline["--update"].split(",") if cmdline["--update"] else []
        skip = cmdline["--skip"].split(",") if cmdline["--skip"] else []
        Inform(
            narrate=cmdline["--narrate"] or cmdline["--verbose"],
            verbose=cmdline["--verbose"],
            logfile=".sshdeploy.log",
            prog_name=False,
            flush=True,
            version=__version__,
        )
        if keys and not cmdline["--trial-run"]:
            fatal(
                "Using the --keys option results in incomplete authorized_keys files.",
                "It may only be used for testing purposes.",
                "As such, --trial-run must also be specified when using --keys.",
                sep="\n",
            )

        # Generated detailed help {{{1
        if cmdline["manual"]:
            from pkg_resources import resource_string

            try:
                Run(cmd=["less"], modes="soeW0", stdin=resource_string("src", "manual.rst").decode("utf8"))
            except OSError as err:
                error(os_error(err))
            terminate()

        # Read config file {{{1
        try:
            config_file = cmdline.get("--config-file")
            config_file = config_file if config_file else "sshdeploy.conf"
            contents = to_path(config_file).read_text()
        except OSError as err:
            fatal(os_error(err))
        code = compile(contents, config_file, "exec")
        config = {}
        try:
            exec(code, config)
        except Exception as err:
            fatal(err)

        # Move into keydir {{{1
        keydir = cmdline["--keydir"]
        keydir = to_path(keydir if keydir else "keys-" + date)
        if cmdline["generate"]:
            comment("creating key directory:", keydir)
            rm(keydir)
            mkdir(keydir)
            cd(keydir)
        elif cmdline["distribute"]:
            cd(keydir)

        # determine default values for key options
        defaults = {}
        for name, default in [
            ("keygen-options", DefaultKeygenOpts),
            ("abraxas-account", DefaultAbraxasAccount),
            ("remote-include-filename", DefaultRemoteIncludeFilename),
        ]:
            defaults[name] = config.get(name, default)

        # Generate keys {{{1
        if cmdline["generate"]:
            for keyname in sorted(config["keys"].keys()):
                data = config["keys"][keyname]
                if keys and keyname not in keys:
                    # user did not request this key
                    continue

                # get default values for missing key options
                for option in defaults:
                    data[option] = data.get(option, defaults[option])

                # generate the key
                key = Key(keyname, data, update, skip, cmdline["--trial-run"])
                key.generate()

        # Publish keys {{{1
        elif cmdline["distribute"]:
            for keyname in sorted(config["keys"].keys()):
                data = config["keys"][keyname]
                if keys and keyname not in keys:
                    continue  # user did not request this key

                # get default values for missing key options
                for option in defaults:
                    data[option] = data.get(option, defaults[option])

                # publish the key pair to clients
                key = Key(keyname, data, update, skip, cmdline["--trial-run"])
                key.publish_private_key()
                key.gather_public_keys()

            # publish authorized_keys files to servers {{{1
            if cmdline["distribute"]:
                for each in sorted(AuthKeys.known):
                    authkey = AuthKeys.known[each]
                    authkey.publish()
                    authkey.verify()

        # Process hosts {{{1
        elif cmdline["test"] or cmdline["clean"] or cmdline["hosts"]:
            hosts = set()
            for keyname, data in config["keys"].items():
                if keys and keyname not in keys:
                    continue  # user did not request this key

                # add servers to list of hosts
                for server, options in data["servers"].items():
                    if update and server not in update or server in skip:
                        continue
                    if "bypass" not in options:
                        hosts.add(server)

                # add clients to list of hosts
                for client in data["clients"].keys():
                    if update and client not in update or client in skip:
                        continue
                    hosts.add(client)

            # process the hosts
            if cmdline["test"]:
                # test host
                for host in sorted(hosts):
                    test_access(host)
            elif cmdline["clean"]:
                # clean host
                for host in sorted(hosts):
                    clean(host)
            else:
                # list hosts
                for host in sorted(hosts):
                    display(host)

    except OSError as err:
        error(os_error(err))
    except KeyboardInterrupt:
        display("Killed by user")
    done()
예제 #16
0
def run(cmd, stdin=None, modes=None):
    comment('    running:', *cmd)
    Run(cmd, stdin=stdin, modes=modes)