예제 #1
0
파일: systemd.py 프로젝트: mbr/remand
def start_unit(unit_name):
    state = get_unit_state(unit_name)
    if 'ActiveState' in state and state['ActiveState'] == 'active' and state['SubState'] == 'running':
        return Unchanged(msg='{} already running'.format(unit_name))

    proc.run([config['cmd_systemctl'], 'start', unit_name])
    return Changed(msg='Started {}'.format(unit_name))
예제 #2
0
파일: systemd.py 프로젝트: mbr/remand
def stop_unit(unit_name):
    state = get_unit_state(unit_name)
    if state['ActiveState'] == 'stopped':
        return Unchanged(msg='{} already stopped'.format(unit_name))

    proc.run([config['cmd_systemctl'], 'stop', unit_name])
    return Changed(msg='Stopped {}'.format(unit_name))
예제 #3
0
파일: systemd.py 프로젝트: mbr/remand
def disable_unit(unit_name):
    state = get_unit_state(unit_name)
    if state.get('UnitFileState') == 'disabled':
        return Unchanged(msg='{} already disabled'.format(unit_name))

    proc.run([config['cmd_systemctl'], 'disable', unit_name])
    return Changed(msg='Disabled {}'.format(unit_name))
예제 #4
0
파일: __init__.py 프로젝트: mbr/remand
def chown(remote_path, uid=None, gid=None, recursive=False):
    new_owner = ':'

    # no-op
    if uid is None and gid is None:
        return

    if uid is not None:
        new_owner = str(uid) + new_owner
    if gid is not None:
        new_owner += str(gid)

    cmd = [config['cmd_chown']]

    if recursive:
        cmd.append('-R')

    cmd.append('-c')  # FIXME: on BSDs, we need -v here?

    cmd.append(new_owner)
    cmd.append(remote_path)

    stdout, _, _ = proc.run(cmd)

    if stdout.strip():
        return Changed(msg='Changed ownership of {} to {}'.format(
            remote_path, new_owner))

    return Unchanged(msg='Ownership of {} already {}'.format(
        remote_path, new_owner))
예제 #5
0
def add_repo(distribution,
             components=['main'],
             site='http://deb.debian.org/debian',
             src=False,
             arch=[],
             name=None):
    comps = ' '.join(components)

    options = ''
    if arch:
        options = ' [ arch={} ]'.format(','.join(arch))

    line = '{}{} {} {} {}\n'.format(
        'deb-src' if src else 'deb',
        options,
        site,
        distribution,
        comps,
    )

    if name is None:
        name = '{}_{}{}'.format(distribution, '_'.join(components), ''
                                if not src else '-sources')

    path = remote.path.join(config['apt_sources_list_d'], name + '.list')
    upload = fs.upload_string(line, path, create_parent=True)

    if upload.changed:
        info_update_timestamp().mark_stale()
        return Changed(msg='Added apt repository: {}'.format(line))
    return Unchanged(msg='Already present: {}'.format(line))
예제 #6
0
def remove_packages(pkgs, check_first=True, purge=False, max_age=3600):
    if check_first and not set(pkgs).intersection(
            set(info_installed_packages().keys())):
        return Unchanged(msg='Not installed: {}'.format(' '.join(pkgs)))

    update(max_age)

    args = [config['cmd_apt_get']]

    args.extend([
        'remove' if not purge else 'purge',
        '--quiet',
        '--yes',  # FIXME: options below don't work. why?
        # '--option', 'Dpkg::Options::="--force-confdef"',
        # '--option', 'Dpkg::Options::="--force-confold"'
    ])
    args.extend(pkgs)
    proc.run(
        args, extra_env={
            'DEBIAN_FRONTEND': 'noninteractive',
        })

    info_installed_packages.invalidate_cache()

    return Changed(msg='{} {}'.format('Removed' if not purge else 'Purged',
                                      ' '.join(pkgs)))
예제 #7
0
파일: __init__.py 프로젝트: mbr/remand
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))
예제 #8
0
def install_private_key(key_file,
                        user='******',
                        key_type='rsa',
                        target_path=None):

    if target_path is None:
        # FIXME: auto-determine key type if None
        if key_type not in ('rsa', ):
            raise NotImplementedError('Key type {} not supported')

        fn = 'id_' + key_type
        target_path = remote.path.join(info['posix.users'][user].home, '.ssh',
                                       fn)

    changed = False

    # blocked: SSH transport does not suppoort
    # with remote.umasked(0o777 - KEY_FILE_PERMS):
    changed |= fs.create_dir(remote.path.dirname(target_path),
                             mode=AK_DIR_PERMS).changed

    changed |= fs.upload_file(key_file, target_path).changed
    changed |= fs.chmod(target_path, mode=KEY_FILE_PERMS).changed

    if changed:
        return Changed(msg='Installed private key {}'.format(target_path))
    return Unchanged(
        msg='Private key {} already installed'.format(target_path))
예제 #9
0
파일: __init__.py 프로젝트: mbr/remand
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))
예제 #10
0
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')
예제 #11
0
파일: __init__.py 프로젝트: mbr/remand
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))
예제 #12
0
def dpkg_install(paths, check=True):
    if not hasattr(paths, 'keys'):
        pkgs = {}

        # determine package names from filenames. ideally, we would open the
        # package here and check
        for p in paths:
            fn = os.path.basename(p)
            try:
                name, version, tail = fn.split('_', 3)
                pkgs[(name, version)] = p
            except ValueError:
                raise ValueError(
                    'Could not determine package version from '
                    'package filename {}. Please rename the .deb '
                    'to standard debian convention '
                    '(name_version_arch.deb) or supply a specific '
                    'version by passing a dictionary parameter.'.format(fn))

    # log names
    log.debug('Package names: ' + ', '.join('{} -> {}'.format(k, v)
                                            for k, v in pkgs.items()))

    if check:
        missing = []
        installed = info_installed_packages()

        for name, version in pkgs:
            if name not in installed or not installed[name].eq_version(version):
                missing.append((name, version))
    else:
        missing = pkgs.keys()

    log.debug('Installing packages: {}'.format(missing))

    if not missing:
        return Unchanged('Packages {!r} already installed'.format(pkgs.keys()))

    # FIXME: see above
    info_installed_packages.invalidate_cache()

    with fs.remote_tmpdir() as rtmp:
        # upload packages to be installed
        pkg_files = []
        for idx, key in enumerate(missing):
            tmpdest = remote.path.join(rtmp, str(idx) + '.deb')
            fs.upload_file(pkgs[key], tmpdest)
            pkg_files.append(tmpdest)

        # install in a single dpkg install line
        # FIXME: add debconf default and such (same as apt)
        args = [config['cmd_dpkg'], '-i']
        args.extend(pkg_files)
        proc.run(
            args, extra_env={
                'DEBIAN_FRONTEND': 'noninteractive',
            })

    return Changed(msg='Installed packages {!r}'.format(missing))
예제 #13
0
def update(max_age=3600):
    if max_age < 0:
        return Unchanged(msg='apt update disabled (max_age < 0).')

    ts = info_update_timestamp()

    if max_age:
        age = ts.get_age()
        if age < max_age:
            return Unchanged(
                msg='apt cache is only {:.0f} minutes old, not updating'
                .format(age / 60))

    proc.run([config['cmd_apt_get'], 'update'])

    # modify update stamp
    ts.mark_current()

    return Changed(msg='apt cache updated')
예제 #14
0
파일: systemd.py 프로젝트: mbr/remand
def enable_unit(unit_name, check_first=False):
    if check_first:
        state = get_unit_state(unit_name)
        # we use 'WantedBy' as a guess whether or not the service is enabled
        # when UnitFileState is not available (SysV init or older systemd)
        ufs = state.get('UnitFileState')
        if ufs == 'enabled' or ufs is None and 'WantedBy' in state:
            return Unchanged(msg='{} already enabled'.format(unit_name))

    proc.run([config['cmd_systemctl'], 'enable', unit_name])
    return Changed(msg='Enabled {}'.format(unit_name))
예제 #15
0
파일: __init__.py 프로젝트: mbr/remand
def remove_file(remote_path):
    """Removes a remote file, as long as it is a file or a symbolic link.

    :param remote_path: Remote file to remote.
    """
    try:
        remote.unlink(remote_path)
    except RemoteFileDoesNotExistError:
        return Unchanged(msg=u'File already gone: {}'.format(remote_path))

    return Changed(msg=u'Removed: {}'.format(remote_path))
예제 #16
0
def grant_me_root(my_key='~/.ssh/id_rsa.pub', unlock_root=True):
    c = set_authorized_keys([os.path.expanduser(my_key)], user='******').changed

    if unlock_root:
        # we need to unlock the root user, otherwise a login is not possible
        # via ssh
        c |= posix.set_unlocked_no_password(['root']).changed

    if c:
        return Changed(msg='Granted root ssh login')
    return Unchanged(msg='Already have root login')
예제 #17
0
def dpkg_add_architecture(arch):
    archs = [info_dpkg_architecture()] + info_dpkg_foreign_architectures()

    if arch in archs:
        return Unchanged(msg='Architecture already enabled: {}'.format(arch))

    proc.run([config['cmd_dpkg'], '--add-architecture', arch])

    # invalidate caches
    info_dpkg_foreign_architectures.invalidate_cache()
    info_update_timestamp().mark_stale()
    return Changed(msg='New architecture added: {}'.format(arch))
예제 #18
0
def useradd(name,
            groups=[],
            user_group=True,
            comment=None,
            home=None,
            create_home=None,
            system=False,
            shell=None):
    cmd = [config['cmd_useradd']]

    gs = groups[:]

    if gs:
        if not user_group:
            cmd.extend(('-g', gs.pop(0)))

    if gs:
        cmd.extend(('-G', ','.join(groups)))

    if user_group is True:
        cmd.append('-U')
    elif user_group is False:
        cmd.append('-N')

    if comment is not None:
        cmd.extend(('-c', comment))

    if home is not None:
        cmd.extend(('-d', home))

    if create_home is False:
        cmd.append('-M')
    elif create_home is True:
        cmd.append('-m')

    if shell:
        cmd.extend(('-s', shell))

    if system:
        cmd.append('-r')

    cmd.append(name)

    stdout, stderr, returncode = proc.run(cmd,
                                          status_ok=(0, 9),
                                          status_meaning=_USERADD_STATUS_CODES)

    if returncode == 9:
        # FIXME: should check if user is up-to-date (home, etc)
        return Unchanged(msg='User {} already exists'.format(name))

    info_users.invalidate_cache()
    return Changed(msg='Created user {}'.format(name))
예제 #19
0
파일: raspi_init.py 프로젝트: mbr/remand
def expand_root_fs():
    dev_size, _, _ = proc.run(['fdisk', '-s', '/dev/mmcblk0'])
    p1_size, _, _ = proc.run(['fdisk', '-s', '/dev/mmcblk0p1'])
    p2_size, _, _ = proc.run(['fdisk', '-s', '/dev/mmcblk0p2'])
    free_space = (int(dev_size) - int(p1_size) - int(p2_size)) * 512

    if free_space <= 4 * 1024 * 1024:
        return Unchanged(
            msg='Free space is <= 4M. Not expanding root filesystem')
    else:
        # fixme: run fdisk and resize2fs instead of raspi-config?
        proc.run(['raspi-config', '--expand-rootfs'])
        return Changed(msg='Expanded root filesystem')
예제 #20
0
파일: systemd.py 프로젝트: mbr/remand
def install_unit_file(unit_file, reload=True):
    base, ext = os.path.splitext(unit_file)

    if ext not in UNIT_EXTS:
        raise ValueError('unit_file should be one of {}'.format(UNIT_EXTS))

    remote_unit = os.path.join(config['systemd_unit_dir'],
                               os.path.basename(unit_file))

    if fs.upload_file(unit_file, remote_unit).changed:
        if reload:
            daemon_reload()
        return Changed(msg='Installed {}'.format(remote_unit))

    return Unchanged(msg='{} already installed'.format(remote_unit))
예제 #21
0
def set_password(name, password=None, hashed=False, salt=None):
    if salt is None:
        salt = hashlib.sha1(os.urandom(64)).hexdigest()

    if not hashed:
        # hash using sha256
        hash = crypt(password, "$6$" + salt)
    else:
        hash = password

    # at this point, we have a hashed password
    with fs.edit('/etc/shadow', create=False) as shadow:
        new_lines = []

        for line in shadow.lines():
            entry = ShadowEntry.from_line(line)

            if entry.name == name:
                # we need to check if the password already matches (with a
                # different salt)

                pwc = entry.password_encrypted

                need_update = True

                if hashed:
                    # we were supplied a hashed password, simply compare the
                    # hashes
                    need_update = (pwc != password)
                else:
                    # not hashed
                    if '$' in pwc:
                        lead = pwc[:pwc.rindex('$')]
                        need_update = (crypt(password, lead) != pwc)

                if need_update:
                    entry = entry._replace(password_encrypted=hash)
                    new_lines.append(entry.to_line())
                    continue

            new_lines.append(line)

        shadow.set_lines(new_lines)

    if shadow.changed:
        return Changed(msg='Updated password for user {}'.format(name))

    return Unchanged(msg='Password for {} already set'.format(name))
예제 #22
0
def query_cache(pkgs):
    stdout, _, _ = proc.run([config['cmd_apt_cache'], 'show'] + list(pkgs))
    pkgs = OrderedDict()
    for dump in stdout.split('\n\n'):
        # skip empty lines
        if not dump or dump.isspace():
            continue
        try:
            pkg_info = Deb822(dump)
        except ValueError:
            log.debug(dump)
            raise RemoteFailureError('Error parsing Deb822 info.')

        pkgs[pkg_info['Package']] = pkg_info

    return Unchanged(pkgs)
예제 #23
0
파일: systemd.py 프로젝트: mbr/remand
def install_network_file(network_file, reload=True):
    base, ext = os.path.splitext(network_file)

    if ext not in NETWORK_EXTS:
        raise ValueError(
            'network_file should be one of {}'.format(NETWORK_EXTS))

    remote_network = os.path.join(config['systemd_network_dir'],
                                  os.path.basename(network_file))

    if fs.upload_file(network_file, remote_network).changed:
        if reload:
            daemon_reload()
        return Changed(msg='Installed {}'.format(remote_network))

    return Unchanged(msg='{} already installed'.format(remote_network))
예제 #24
0
def install_hostkeys(base_dir):
    results = []

    for key_type in ('ecdsa', 'ed25519', 'rsa'):
        fn = 'ssh_host_' + key_type + '_key'
        pub_fn = fn + '.pub'
        sk = os.path.join(base_dir, fn)
        pk = os.path.join(base_dir, pub_fn)

        results.append(fs.upload_file(sk, '/etc/ssh/' + fn))
        results.append(fs.upload_file(pk, '/etc/ssh/' + pub_fn))

    if util.any_changed(*results):
        systemd.reload_unit('ssh.service')
        return Changed(msg='Installed SSH hostkeys from {}'.format(base_dir))

    return Unchanged(msg='SSH hostkeys already installed')
예제 #25
0
파일: systemd.py 프로젝트: mbr/remand
def _ensure_unit(service_name, upload_func, enable, auto_restart):
    # FIXME: we also support sockets!
    # assert service_name.endswith('.service')

    changed = upload_func().changed

    # FIXME: check if restart was successful?
    if auto_restart and changed:
        restart_unit(service_name)

    if enable:
        changed |= enable_unit(service_name).changed

    if changed:
        return Changed(
            msg='Unit {} updated and restarted'.format(service_name))

    return Unchanged(
        msg='Unit {} already up to date and running'.format(service_name))
예제 #26
0
파일: raspi_init.py 프로젝트: mbr/remand
def enable_systemd():
    changed = False
    changed |= apt.install_packages(['systemd']).changed

    with fs.edit('/boot/cmdline.txt', create=False) as e:
        flag = 'init=/bin/systemd'
        lines = e.lines()
        assert len(lines) == 1

        if flag not in lines[0]:
            lines[0] += ' ' + flag
            e.set_lines(lines)

    changed |= e.changed

    if changed:
        return Changed(msg='Installed systemd')

    return Unchanged(msg='systemd already active')
예제 #27
0
def userdel(name, remove_home=False, force=False):
    cmd = [config['cmd_userdel']]

    if remove_home:
        cmd.append('-r')

    if force:
        cmd.append('-f')

    cmd.append(name)

    stdout, stderr, returncode = proc.run(cmd,
                                          status_ok=(0, 6),
                                          status_meaning=_USERDEL_STATUS_CODES)

    if returncode == 6:
        return Unchanged(msg='User {} does not exist'.format(name))

    info_users.invalidate_cache()
    return Changed(msg='Removed user {}'.format(name))
예제 #28
0
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)))
예제 #29
0
def auto_remove(max_age=3600):
    update(max_age)  # FIXME: make max_age become a config setting, add a
    #        with_config context manager

    args = [config['cmd_apt_get']]
    args.extend([
        'autoremove',
        '--quiet',
        '--yes',
    ])
    stdout, _, _ = proc.run(
        args, extra_env={
            'DEBIAN_FRONTEND': 'noninteractive',
        })

    if '0 to remove' in stdout:
        return Unchanged(msg='No packages auto-removed')

    info_installed_packages.invalidate_cache()

    return Changed(msg='Some packages were auto-removed')
예제 #30
0
파일: __init__.py 프로젝트: mbr/remand
def create_dir(path, mode=0777):
    """Ensure that a directory exists at path. Parent directories are created
    if needed.

    :param path: Directory to create if it does not exist.
    :param mode: Mode for newly created parent directories.
    :param return: ``False`` if the path existed, ``True`` otherwise.
    """
    npath = remote.path.normpath(path)

    st = remote.stat(path)

    if not st:
        head, tail = remote.path.split(path)
        if tail and head:
            # create parent directories
            create_dir(head, mode)
        remote.mkdir(npath, mode)
        return Changed(msg='Created directory: {}'.format(path))

    return Unchanged('Already exists: {}'.format(path))