Пример #1
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
Пример #2
0
    def publish_private_key(self):
        keyname = self.keyname
        data = self.data
        clients = self.data.get('clients', [])
        prov = '.provisional' if self.trial_run else ''

        # copy key pair to remote client
        for client in sorted(clients):
            if self.update and client not in self.update:
                continue
            if client in self.skip:
                continue
            narrate('    publishing key pair to', client)
            client_data = clients[client]

            # delete any pre-existing provisional files
            # the goal here is to leave a clean directory when not trial-run
            try:
                run_sftp(client, [
                    fmt('rm .ssh/{keyname}.provisional'),
                    fmt('rm .ssh/{keyname}.pub.provisional'),
                ])
            except OSError as err:
                pass

            # now upload the new files
            try:
                run_sftp(client, [
                    fmt('put -p {keyname} .ssh/{keyname}{prov}'),
                    fmt('put -p {keyname}.pub .ssh/{keyname}.pub{prov}'),
                ])
            except OSError as err:
                error(os_error(err))
Пример #3
0
    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))
Пример #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 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)
Пример #6
0
    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)
Пример #7
0
    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
            )
Пример #8
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)
Пример #9
0
def main():
    version = f"{__version__} ({__released__})"
    cmdline = docopt(__doc__, version=version)
    quiet = cmdline["--quiet"]
    problem = False
    use_color = Color.isTTY() and not cmdline["--no-color"]
    passes = Color("green", enable=use_color)
    fails = Color("red", enable=use_color)
    if cmdline["--verbose"]:
        overdue_message = verbose_overdue_message
    else:
        overdue_message = terse_overdue_message

    # prepare to create logfile
    log = to_path(DATA_DIR, OVERDUE_LOG_FILE) if OVERDUE_LOG_FILE else False
    if log:
        data_dir = to_path(DATA_DIR)
        if not data_dir.exists():
            try:
                # data dir does not exist, create it
                data_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
            except OSError as e:
                warn(os_error(e))
                log = False

    with Inform(flush=True, quiet=quiet, logfile=log, version=version):

        # read the settings file
        try:
            settings_file = PythonFile(CONFIG_DIR, OVERDUE_FILE)
            settings = settings_file.run()
        except Error as e:
            e.terminate()

        # gather needed settings
        default_maintainer = settings.get("default_maintainer")
        default_max_age = settings.get("default_max_age", 28)
        dumper = settings.get("dumper", f"{username}@{hostname}")
        repositories = settings.get("repositories")
        root = settings.get("root")

        # process repositories table
        backups = []
        if is_str(repositories):
            for line in repositories.split("\n"):
                line = line.split("#")[0].strip()  # discard comments
                if not line:
                    continue
                backups.append([c.strip() for c in line.split("|")])
        else:
            for each in repositories:
                backups.append([
                    each.get("host"),
                    each.get("path"),
                    each.get("maintainer"),
                    each.get("max_age"),
                ])

        def send_mail(recipient, subject, message):
            if cmdline["--mail"]:
                if cmdline['--verbose']:
                    display(f"Reporting to {recipient}.\n")
                mail_cmd = ["mailx", "-r", dumper, "-s", subject, recipient]
                Run(mail_cmd, stdin=message, modes="soeW0")

        # check age of repositories
        for host, path, maintainer, max_age in backups:
            maintainer = default_maintainer if not maintainer else maintainer
            max_age = float(max_age) if max_age else default_max_age
            try:
                path = to_path(root, path)
                if path.is_dir():
                    paths = list(path.glob("index.*"))
                    if not paths:
                        raise Error("no sentinel file found.", culprit=path)
                    if len(paths) > 1:
                        raise Error("too many sentinel files.",
                                    *paths,
                                    sep="\n    ")
                    path = paths[0]
                mtime = arrow.get(path.stat().st_mtime)
                delta = now - mtime
                age = 24 * delta.days + delta.seconds / 3600
                report = age > max_age
                overdue = ' -- overdue' if report else ''
                color = fails if report else passes
                if report or not cmdline["--no-passes"]:
                    display(color(fmt(overdue_message)))

                if report:
                    problem = True
                    subject = f"backup of {host} is overdue"
                    msg = fmt(mail_overdue_message)
                    send_mail(maintainer, subject, msg)
            except OSError as e:
                problem = True
                msg = os_error(e)
                error(msg)
                if maintainer:
                    send_mail(
                        maintainer,
                        f"{get_prog_name()} error",
                        error_message.format(msg),
                    )
            except Error as e:
                problem = True
                e.report()
                if maintainer:
                    send_mail(
                        maintainer,
                        f"{get_prog_name()} error",
                        error_message.format(str(e)),
                    )
        terminate(problem)