def _expand_remote_dest(local_path, remote_path): if remote_path is None: if local_path is None: raise RuntimeError('one of local_path, remote_path is required') remote_path = local_path st = remote.lstat(remote_path) if st: # file exists, check if it is a link if S_ISLNK(st.st_mode): # normalize (dangling links will raise an exception) remote_path = remote.normalize(remote_path) # update stat st = remote.lstat(remote_path) # dir-expansion, since st is guaranteed not be a link if st and S_ISDIR(st.st_mode): if local_path is None: raise RemoteFailureError('Is a directory: {}'.format( remote_path)) # if it's a directory, correct path remote_path = remote.path.join(remote_path, remote.path.basename(local_path)) st = remote.lstat(remote_path) log.debug('Expanded remote_path to {!r}'.format(remote_path)) # ensure st is either non-existant, or a regular file if st and not S_ISREG(st.st_mode): raise RemoteFailureError('Not a regular file: {!r}'.format( remote_path)) return st, remote_path
def ensure_certificate(hostname): cert_rpath = remote.path.join(config['sslcert_cert_dir'], hostname + '.crt') chain_rpath = remote.path.join(config['sslcert_cert_dir'], hostname + '.chain.crt') key_rpath = remote.path.join(config['sslcert_key_dir'], hostname + '.pem') # first, ensure any certificate exists on the host. otherwise, # webservers like nginx will likely not start if not (remote.lstat(cert_rpath) and remote.lstat(key_rpath) and remote.lstat(chain_rpath)): log.debug('Remote certificate {}, key {}, chain {} not found'.format( cert_rpath, key_rpath, chain_rpath)) key, cert = generate_self_signed_cert(hostname) # FIXME: maybe use install cert here. fs.upload_string(key, key_rpath) fs.upload_string(cert, cert_rpath) fs.upload_string(cert, chain_rpath) return Changed( msg='No certificate {} / key {} / chain {} found. A self-signed ' 'certficate from a reputable snake-oil vendor was installed.'. format(cert_rpath, key_rpath, chain_rpath)) return Unchanged( 'Certificate for hostname {} already preset'.format(hostname))
def walk(top, topdown=True, onerror=None, followlinks=False): try: names = remote.listdir(top) except OSError as e: if onerror: onerror(e) return dirs, files = [], [] for name in names: fn = remote.path.join(top, name) st = remote.lstat(fn) if S_ISDIR(st.st_mode): dirs.append(name) else: files.append(name) if topdown: yield top, dirs, files for name in dirs: fn = remote.path.join(top, name) st = remote.lstat(fn) if followlinks or not S_ISLNK(st.st_mode): for rv in walk(fn, topdown, onerror, followlinks): yield rv if not topdown: yield top, dirs, files
def _expand_remote_dest(local_path, remote_path): if remote_path is None: if local_path is None: raise RuntimeError('one of local_path, remote_path is required') remote_path = local_path st = remote.lstat(remote_path) if st: # file exists, check if it is a link if S_ISLNK(st.st_mode): # normalize (dangling links will raise an exception) remote_path = remote.normalize(remote_path) # update stat st = remote.lstat(remote_path) # dir-expansion, since st is guaranteed not be a link if st and S_ISDIR(st.st_mode): if local_path is None: raise RemoteFailureError( 'Is a directory: {}'.format(remote_path)) # if it's a directory, correct path remote_path = remote.path.join(remote_path, remote.path.basename(local_path)) st = remote.lstat(remote_path) log.debug('Expanded remote_path to {!r}'.format(remote_path)) # ensure st is either non-existant, or a regular file if st and not S_ISREG(st.st_mode): raise RemoteFailureError( 'Not a regular file: {!r}'.format(remote_path)) return st, remote_path
def install_cert(cert, key, cert_name=None, key_name=None): cert_name = cert_name or os.path.basename(cert) key_name = key_name or os.path.basename(key) # small sanity check with open(cert) as f: if 'PRIVATE' in f.read(): raise ValueError( 'You seem to have passed a private key as a cert!') with open(key) as f: if 'PRIVATE' not in f.read(): raise ValueError( '{} does not seem to be a valid private key'.format(key)) # check if remote is reasonably secure cert_dir = config['sslcert_cert_dir'] cert_dir_st = remote.lstat(cert_dir) if not cert_dir_st: raise ConfigurationError('Remote SSL dir {} does not exist'.format( cert_dir)) key_dir = config['sslcert_key_dir'] key_dir_st = remote.lstat(key_dir) if not key_dir_st: raise ConfigurationError('Remote key dir {} does not exist'.format( key_dir)) SECURE_MODES = (0o700, 0o710) actual_mode = key_dir_st.st_mode & 0o777 if actual_mode not in SECURE_MODES: raise ConfigurationError( 'Mode of remote key dir {} is {:o}, should be one of {:o}'.format( key_dir, actual_mode, SECURE_MODES)) if key_dir_st.st_uid != 0: raise ConfigurationError( 'Remove key dir {} is not owned by root'.format(key_dir)) # we can safely upload the key and cert cert_rpath = remote.path.join(cert_dir, cert_name) key_rpath = remote.path.join(key_dir, key_name) changed = False changed |= fs.upload_file(cert, cert_rpath).changed changed |= fs.upload_file(key, key_rpath).changed changed |= fs.chmod(key_rpath, 0o600).changed if changed: return Changed( msg='Uploaded key pair {}/{}'.format(cert_name, key_name)) return Unchanged( msg='Key pair {}/{} already uploaded'.format(cert_name, key_name))
def symlink(src, dst): if dst.endswith('/'): raise NotImplementedError('Creating link inside directory not ' 'implemented') lst = remote.lstat(dst) if lst: if not S_ISLNK(lst.st_mode): raise RemotePathIsNotALinkError('Already exists and not a link: ' '{}'.format(dst)) # remote is a link rsrc = remote.readlink(dst) if rsrc == src: return Unchanged(msg='Unchanged link: {} -> {}'.format(dst, src)) # we need to update the link, unfortunately, this is often not possible # atomically remote.unlink(dst) remote.symlink(src, dst) return Changed(msg='Changed link: {} -> {} (previously -> {})' .format(dst, src, rsrc)) remote.symlink(src, dst) return Changed(msg='Created link: {} -> {}'.format(dst, src))
def remove_dir(remote_path, recursive=True): """Removes a remote directory. If the directory does not exist, does nothing. :param recursive: Makes ``remove_dir`` behave closer to ``rm -rf`` instead of ``rmdir``. """ st = remote.lstat(remote_path) if st is None: return Unchanged(msg=u'Directory already gone: {}'.format(remote_path)) # if it is not a directory, don't touch it if not S_ISDIR(st.st_mode): raise RemotePathIsNotADirectoryError(remote_path) if recursive: for dirpath, dirnames, filenames in walk(remote_path, topdown=False): for fn in filenames: remote.unlink(remote.path.join(dirpath, fn)) remote.rmdir(dirpath) else: remote.rmdir(dirpath) return Changed(msg=u'Removed directory: {}'.format(remote_path))
def init_authorized_keys(user='******', fix_permissions=True): ak_file = get_authorized_keys_file(user) ak_dir = remote.path.dirname(ak_file) changed = False # ensure the directory exists changed |= fs.create_dir(ak_dir, mode=AK_DIR_PERMS).changed if fix_permissions: changed |= fs.chmod(ak_dir, AK_DIR_PERMS).changed changed |= fs.chown(ak_dir, uid=user).changed # check if the authorized keys file exists if not remote.lstat(ak_file): changed |= fs.touch(ak_file).changed if fix_permissions: changed |= fs.chmod(ak_file, AK_FILE_PERMS).changed changed |= fs.chown(ak_dir, uid=user).changed # at this point, we have fixed permissions for file and dir, as well as # ensured they exist. however, they might still be owned by root if changed: return Changed(ak_file, msg='Changed permissions or owner on authorized keys') return Unchanged( ak_file, msg='authorized keys file has correct owner and permissions')
def touch(remote_path, mtime=None, atime=None): """Update mtime and atime of a path. Similar to running ``touch remote_path``. :param remote_path: Remote path whose times will get updated. :param mtime: New mtime. If ``None``, uses the current time. :param atime: New atime. Only used if ``mtime`` is not None. Defaults to ``mtime``. :return: Since it always updates the current time, calling this function will always result in a modification. """ # ensure the file exists if not remote.lstat(remote_path): with remote.file(remote_path, 'w') as out: out.write('') if mtime is None: remote.utime(remote_path, None) return Changed(msg=u'Touched {} to current time'.format(remote_path)) else: atime = atime if atime is not None else mtime remote.utime(remote_path, (atime, mtime)) return Changed(msg=u'Touched {} to mtime={}, atime={}'.format( remote_path, mtime, atime))
def init_authorized_keys(user='******', fix_permissions=True): ak_file = get_authorized_keys_file(user) ak_dir = remote.path.dirname(ak_file) changed = False # ensure the directory exists changed |= fs.create_dir(ak_dir, mode=AK_DIR_PERMS).changed if fix_permissions: changed |= fs.chmod(ak_dir, AK_DIR_PERMS).changed # check if the authorized keys file exists if not remote.lstat(ak_file): changed |= fs.touch(ak_file).changed if fix_permissions: changed |= fs.chmod(ak_file, AK_FILE_PERMS).changed # at this point, we have fixed permissions for file and dir, as well as # ensured they exist. however, they might still be owned by root if changed: return Changed(ak_file, msg='Changed permissions or owner on authorized keys') return Unchanged( ak_file, msg='authorized keys file has correct owner and permissions')
def chmod(remote_path, mode, recursive=False, executable=False): # FIXME: instead of executable, add parsing of rwxX-style modes # FIXME: add speedup by using local chmod xmode = mode if not executable else mode | 0o111 st = remote.lstat(remote_path) if mode > 0o777: raise ValueError('Modes above 0o777 are not supported') changed = False actual_mode = st.st_mode & 0o777 # if the target is a directory or already has at least one executable bit, # we apply the executable mode (see chmod manpage for details) correct_mode = (xmode if S_ISDIR(st.st_mode) or actual_mode & 0o111 else mode) if actual_mode != correct_mode: remote.chmod(remote_path, correct_mode) changed = True if recursive and S_ISDIR(st.st_mode): for rfn in remote.listdir(remote_path): changed |= chmod( remote.path.join(remote_path, rfn), mode, True, executable).changed if changed: return Changed( msg='Changed mode of {} to {:o}'.format(remote_path, mode)) return Unchanged(msg='Mode of {} already {:o}'.format(remote_path, mode))
def chmod(remote_path, mode, recursive=False, executable=False): # FIXME: instead of executable, add parsing of rwxX-style modes # FIXME: add speedup by using local chmod xmode = mode if not executable else mode | 0o111 st = remote.lstat(remote_path) if mode > 0o777: raise ValueError('Modes above 0o777 are not supported') changed = False actual_mode = st.st_mode & 0o777 # if the target is a directory or already has at least one executable bit, # we apply the executable mode (see chmod manpage for details) correct_mode = (xmode if S_ISDIR(st.st_mode) or actual_mode & 0o111 else mode) if actual_mode != correct_mode: remote.chmod(remote_path, correct_mode) changed = True if recursive and S_ISDIR(st.st_mode): for rfn in remote.listdir(remote_path): changed |= chmod( remote.path.join(remote_path, rfn), mode, True, executable).changed if changed: return Changed(msg='Changed mode of {} to {:o}'.format( remote_path, mode)) return Unchanged(msg='Mode of {} already {:o}'.format(remote_path, mode))
def symlink(src, dst): if dst.endswith('/'): raise NotImplementedError('Creating link inside directory not ' 'implemented') lst = remote.lstat(dst) if lst: if not S_ISLNK(lst.st_mode): raise RemotePathIsNotALinkError('Already exists and not a link: ' '{}'.format(dst)) # remote is a link rsrc = remote.readlink(dst) if rsrc == src: return Unchanged(msg='Unchanged link: {} -> {}'.format(dst, src)) # we need to update the link, unfortunately, this is often not possible # atomically remote.unlink(dst) remote.symlink(src, dst) return Changed(msg='Changed link: {} -> {} (previously -> {})'.format( dst, src, rsrc)) remote.symlink(src, dst) return Changed(msg='Created link: {} -> {}'.format(dst, src))
def regenerate_host_keys(mark='/etc/ssh/host_keys_regenerated'): if mark: if remote.lstat(mark): return Unchanged(msg='Hostkeys have already been regenerated') key_names = [ '/etc/ssh/ssh_host_ecdsa_key', '/etc/ssh/ssh_host_ed25519_key', '/etc/ssh/ssh_host_rsa_key', '/etc/ssh/ssh_host_dsa_key', ] def collect_fingerprints(): fps = '' for key in key_names: if remote.lstat(key): fps += proc.run(['ssh-keygen', '-l', '-f', key])[0] return fps old_fps = collect_fingerprints() # remove old keys for key in key_names: fs.remove_file(key) fs.remove_file(key + '.pub') # generate new ones proc.run(['dpkg-reconfigure', 'openssh-server']) # restart openssh systemd.restart_unit('ssh.service') new_fps = collect_fingerprints() # mark host keys as new fs.touch(mark) return Changed( msg='Regenerated SSH host keys.\n' 'Old fingerprints:\n{}\nNew fingerprints:\n{}\n'.format( util.indent(' ', old_fps), util.indent(' ', new_fps)))
def edit(remote_path, create=True): with volatile.file() as tmp: created = False if create and not remote.lstat(remote_path): tmp.write('') created = True else: tmp.write(remote.file(remote_path, 'rb').read()) tmp.close() try: ef = EditableFile(tmp.name) yield ef except Exception: raise else: if created or ef.modified: upload_file(ef.name, remote_path).changed ef.changed = True else: ef.changed = False
def collect_fingerprints(): fps = '' for key in key_names: if remote.lstat(key): fps += proc.run(['ssh-keygen', '-l', '-f', key])[0] return fps
def run(): log.warning('Running testing module. Do not run this on a real machine!') log.debug('Testing popen') proc = remote.popen(['uname']) stdout, stderr = proc.communicate() assert 'Linux' == stdout.strip() log.debug('Testing getcwd()') assert '/home/vagrant' == remote.getcwd() log.debug('Testing chdir()') remote.chdir('/') assert '/' == remote.getcwd() remote.chdir('/home/vagrant') # create a sample file TESTFN = 'testfile' TESTDN = 'TESTDIR' log.debug('Testing file') with remote.file(TESTFN, mode='w') as out: out.write('test') log.debug('Testing chmod') remote.chmod(TESTFN, 0732) log.debug('Testing mkdir') # FIXME: umask? # FIXME: on exists/conflict? remote.mkdir(TESTDN, 0700) log.debug('Testing listdir') assert TESTFN in remote.listdir('.') assert TESTDN in remote.listdir('.') log.debug('Testing rmdir') remote.rmdir(TESTDN) # FIXME: can't test chown without root access log.debug('Testing normalize') assert '/home' == remote.normalize('./..') log.debug('Testing symlink') remote.symlink('to', 'from') log.debug('Testing lstat') remote.lstat('from') log.debug('Testing readlink') assert remote.readlink('/home/vagrant/from') == 'to' log.debug('Testing rename') remote.rename('from', 'from2') assert remote.readlink('/home/vagrant/from2') == 'to' log.debug('Testing unlink') remote.unlink('/home/vagrant/from2') log.debug('Testing stat') s = remote.stat(TESTFN) assert s.st_uid == 1000 assert s.st_gid == 1000 remote.unlink(TESTFN)
def install_cert(cert, key, cert_name=None, key_name=None): """Installs an SSL certificate with including key on the remote Certificate filenames are unchanged, per default they will be installed in `/etc/ssl`, with the corresponding keys at `/etc/ssl/private`.""" cert_name = cert_name or os.path.basename(cert) key_name = key_name or os.path.basename(key) # small sanity check with open(cert) as f: if 'PRIVATE' in f.read(): raise ValueError( 'You seem to have passed a private key as a cert!') with open(key) as f: if 'PRIVATE' not in f.read(): raise ValueError( '{} does not seem to be a valid private key'.format(key)) # check if remote is reasonably secure cert_dir = config['sslcert_cert_dir'] cert_dir_st = remote.lstat(cert_dir) if not cert_dir_st: raise ConfigurationError( 'Remote SSL dir {} does not exist'.format(cert_dir)) key_dir = config['sslcert_key_dir'] key_dir_st = remote.lstat(key_dir) if not key_dir_st: raise ConfigurationError( 'Remote key dir {} does not exist'.format(key_dir)) SECURE_MODES = (0o700, 0o710) actual_mode = key_dir_st.st_mode & 0o777 if actual_mode not in SECURE_MODES: raise ConfigurationError( 'Mode of remote key dir {} is {:o}, should be one of {:o}'.format( key_dir, actual_mode, SECURE_MODES)) if key_dir_st.st_uid != 0: raise ConfigurationError( 'Remove key dir {} is not owned by root'.format(key_dir)) # we can safely upload the key and cert cert_rpath = remote.path.join(cert_dir, cert_name) key_rpath = remote.path.join(key_dir, key_name) changed = False changed |= fs.upload_file(cert, cert_rpath).changed changed |= fs.upload_file(key, key_rpath).changed changed |= fs.chmod(key_rpath, 0o640).changed changed |= fs.chown(key_rpath, uid='root', gid='ssl-cert').changed if changed: return Changed( msg='Uploaded key pair {}/{}'.format(cert_name, key_name)) return Unchanged( msg='Key pair {}/{} already uploaded'.format(cert_name, key_name))
def upload_file(local_path, remote_path=None, follow_symlink=True, create_parent=False): """Uploads a local file to a remote and if does not exist or differs from the local version, uploads it. To avoid having to transfer the file one or more times if unchanged, different methods for verification are available. These can be configured using the ``fs_remote_file_verify`` configuration variable. :param local_path: Local file to upload. If it is a symbolic link, it will be resolved first. :param remote_path: Remote name for the file. If ``None``, same as ``local_path``. If it points to a directory, the file will be uploaded to the directory. Symbolic links not pointing to a directory are an error. :param return: ``False`` if no upload was necessary, ``True`` otherwise. """ st, remote_path = _expand_remote_dest(local_path, remote_path) lst = os.stat(local_path) if follow_symlink else os.lstat(local_path) verifier = Verifier._by_short_name(config['fs_remote_file_verify'])() uploader = Uploader._by_short_name(config['fs_remote_file_upload'])() if lst is None: raise ConfigurationError( 'Local file {!r} does not exist'.format(local_path)) if S_ISLNK(lst.st_mode): # local file is a link rst = remote.lstat(remote_path) if rst: if not S_ISLNK(rst.st_mode): # remote file is not a link, unlink it remote.unlink(remote_path) elif remote.readlink(remote_path) != os.readlink(local_path): # non matching links remote.unlink(remote_path) else: # links pointing to the same target return Unchanged( msg='Symbolink link up-to-date: {}'.format(remote_path)) remote.symlink(os.readlink(local_path), remote_path) return Changed(msg='Created remote link: {}'.format(remote_path)) if not st or not verifier.verify_file(st, local_path, remote_path): if create_parent: create_dir(remote.path.dirname(remote_path)) uploader.upload_file(local_path, remote_path) if config.get_bool('fs_update_mtime'): times = (lst.st_mtime, lst.st_mtime) remote.utime(remote_path, times) log.debug('Updated atime/mtime: {}'.format(times)) return Changed(msg='Upload {} -> {}'.format(local_path, remote_path)) return Unchanged(msg='File up-to-date: {}'.format(remote_path))
def upload_file(local_path, remote_path=None, follow_symlink=True, create_parent=False): """Uploads a local file to a remote and if does not exist or differs from the local version, uploads it. To avoid having to transfer the file one or more times if unchanged, different methods for verification are available. These can be configured using the ``fs_remote_file_verify`` configuration variable. :param local_path: Local file to upload. If it is a symbolic link, it will be resolved first. :param remote_path: Remote name for the file. If ``None``, same as ``local_path``. If it points to a directory, the file will be uploaded to the directory. Symbolic links not pointing to a directory are an error. :param return: ``False`` if no upload was necessary, ``True`` otherwise. """ st, remote_path = _expand_remote_dest(local_path, remote_path) lst = os.stat(local_path) if follow_symlink else os.lstat(local_path) verifier = Verifier._by_short_name(config['fs_remote_file_verify'])() uploader = Uploader._by_short_name(config['fs_remote_file_upload'])() if lst is None: raise ConfigurationError('Local file {!r} does not exist'.format( local_path)) if S_ISLNK(lst.st_mode): # local file is a link rst = remote.lstat(remote_path) if rst: if not S_ISLNK(rst.st_mode): # remote file is not a link, unlink it remote.unlink(remote_path) elif remote.readlink(remote_path) != os.readlink(local_path): # non matching links remote.unlink(remote_path) else: # links pointing to the same target return Unchanged( msg='Symbolink link up-to-date: {}'.format(remote_path)) remote.symlink(os.readlink(local_path), remote_path) return Changed(msg='Created remote link: {}'.format(remote_path)) if not st or not verifier.verify_file(st, local_path, remote_path): if create_parent: create_dir(remote.path.dirname(remote_path)) uploader.upload_file(local_path, remote_path) if config.get_bool('fs_update_mtime'): times = (lst.st_mtime, lst.st_mtime) remote.utime(remote_path, times) log.debug('Updated atime/mtime: {}'.format(times)) return Changed(msg='Upload {} -> {}'.format(local_path, remote_path)) return Unchanged(msg='File up-to-date: {}'.format(remote_path))