Beispiel #1
0
    def systemctl_edit(self, name, override):
        """systemctl edit ``name``.

        Works like command ``systemctl edit name``. Creates directory ``/etc/systemd/system/${name}.d``
        and creates file ``override.conf`` inside it with contents from string override.

        Args:
            name: Name of systemd service to edit.
            override: Which text place inside ``override.conf`` file.
                Leading and trailing whitespace chars are stripped from override.

        Returns:
            True if file ``override.conf`` for service ``name`` changed, False otherwise.
        """
        if override is None:
            override = ''
        if not isinstance(override, str):
            PossibleRuntimeError("Override must be string type.")
        if '/' in name:
            PossibleRuntimeError(f"Invalid unit name '{name}'")
        if not name.endswith('.service'):
            name = name + '.service'
        override_dir = '/etc/systemd/system/' + name + '.d'
        override_conf = os.path.join(override_dir, 'override.conf')
        override = strip(override)
        if override:
            changed1 = self.create_directory(override_dir)
            changed2 = self.put(override, override_conf)
        else:
            changed1 = self.remove_file(override_conf)
            changed2 = self.remove_directory(override_dir)
        return changed1 or changed2
Beispiel #2
0
 def insert_line_editor(text):
     regex = re.compile(anchor_pattern)
     text_lines = text.split('\n')
     line_already_inserted = False
     anchor_lines = 0
     for line in text_lines:
         match = regex.match(line)
         if match:
             anchor_lines += 1
         if line == line_to_insert:
             line_already_inserted = True
     if anchor_lines == 0:
         raise PossibleRuntimeError("insert_line: anchor pattern '%s' not found." % anchor_pattern)
     elif anchor_lines > 1:
         raise PossibleRuntimeError("insert_line: anchor pattern '%s' found %d times, must be only one." % (anchor_pattern, anchor_lines))
     out = list()
     for line in text_lines:
         match = regex.match(line)
         if match and not line_already_inserted:
             if insert_type == 'before':
                 out.append(line_to_insert)
                 out.append(line)
                 continue
             else:  # insert_type == 'after':
                 out.append(line)
                 out.append(line_to_insert)
                 continue
         else:
             out.append(line)
     return '\n'.join(out)
Beispiel #3
0
 def ini_section_editor(text):  # pylint: disable=too-many-locals
     pattern = r'^\s*\[(.*)\]\s*$'
     regex = re.compile(pattern)
     current_section_name = None
     current_section_text = list()
     section_content = dict()
     section_order = list()
     for line in text.split('\n'):
         match = regex.match(line)
         if match:
             new_section_name = match.group(1)
             if new_section_name in section_content or new_section_name == current_section_name:
                 raise PossibleRuntimeError("edit_ini_section: bad ini file, section '[%s]' duplicated in file." % new_section_name)
             section_content[current_section_name] = current_section_text
             section_order.append(current_section_name)
             current_section_name = new_section_name
             current_section_text = list()
         else:
             current_section_text.append(line)
     section_content[current_section_name] = current_section_text
     section_order.append(current_section_name)
     if section_name_to_edit in section_content:
         old_text = '\n'.join(section_content[section_name_to_edit])
         changed, new_text = _apply_editors(old_text, *editors)
         if changed:
             section_content[section_name_to_edit] = new_text.split('\n')
     else:
         raise PossibleRuntimeError("edit_ini_section: section '[%s]' not found." % section_name_to_edit)
     out = list()
     for section_name in section_order:
         if section_name is not None:
             out.append('[' + section_name + ']')
         section_text = section_content[section_name]
         out.extend(section_text)
     return '\n'.join(out)
Beispiel #4
0
 def get(self, remote_filename, default_value=None, *, as_bytes=False):
     if not os.path.isabs(remote_filename):
         raise PossibleRuntimeError(
             f"Remote filename must be absolute: {remote_filename}")
     if not self.is_file(remote_filename):
         if default_value is None:
             raise PossibleFileNotFound(
                 f"Remote file does not exist: {remote_filename}")
         else:
             return default_value
     fd, temp_filename = tempfile.mkstemp(suffix='.tmp',
                                          prefix='possible-',
                                          dir='/tmp')
     try:
         returncode, stdout_bytes, stderr_bytes = self.ssh.get(
             remote_filename, temp_filename)
         if returncode != 0:
             raise PossibleRuntimeError(
                 f"Unexpected returncode '{returncode}'\ncommand: get({remote_filename})\nstdout: {stdout_bytes}\nstderr: {stderr_bytes}"
             )
         temp_file = os.fdopen(fd, mode="rb")
         content = temp_file.read()
         temp_file.close()
         if as_bytes:
             return content
         else:
             return to_text(content)
     finally:
         os.remove(temp_filename)
Beispiel #5
0
 def put(self, content, remote_filename, *, mode='0644'):
     if not isinstance(mode, str) or not mode.isnumeric():
         raise PossibleRuntimeError(f"Mode must be string, like '0644'.")
     if not os.path.isabs(remote_filename):
         raise PossibleRuntimeError(
             f"Remote filename must be absolute: {remote_filename}")
     if self.is_file(remote_filename):
         local_content = to_bytes(content)
         remote_content = self.get(remote_filename, as_bytes=True)
         if local_content == remote_content:
             return False
     fd, temp_filename = tempfile.mkstemp(suffix='.tmp',
                                          prefix='possible-',
                                          dir='/tmp')
     try:
         temp_file = os.fdopen(fd, mode='wb')
         temp_file.write(to_bytes(content))
         temp_file.close()
         os.chmod(temp_filename, int(mode, 8))
         returncode, stdout_bytes, stderr_bytes = self.ssh.put(
             temp_filename, remote_filename)
         if returncode != 0:
             raise PossibleRuntimeError(
                 f"Unexpected returncode '{returncode}'\ncommand: put('{content}', {remote_filename})\nstdout: {stdout_bytes}\nstderr: {stderr_bytes}"
             )
         else:
             return True
     finally:
         os.remove(temp_filename)
Beispiel #6
0
 def chmod(self, remote_filename, *, mode='0644'):
     if not isinstance(mode, str) or not mode.isnumeric():
         raise PossibleRuntimeError(f"Mode must be string, like '0644'.")
     if not os.path.isabs(remote_filename):
         raise PossibleRuntimeError(
             f"Remote filename must be absolute: {remote_filename}")
     stdout = self.run('chmod --changes ' + mode + ' -- ' +
                       remote_filename).stdout
     changed = stdout != ""
     return changed
Beispiel #7
0
def local_run(command, *, stdin=None, can_fail=False):
    old_cwd = os.getcwd()
    os.chdir(runtime.config.files)
    try:
        command = command.replace("$FILES", str(runtime.config.files))
        command = ["/bin/bash", "-c", command]
        p = subprocess.Popen(command,
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        try:
            stdout_bytes, stderr_bytes = p.communicate(to_bytes(stdin),
                                                       LOCAL_COMMAND_TIMEOUT)
        except subprocess.TimeoutExpired:
            p.kill()
            stdout_bytes, stderr_bytes = p.communicate()
        result = Result(p.returncode, stdout_bytes, stderr_bytes)
        if result or can_fail:
            return result
        else:
            raise PossibleRuntimeError(
                f"Unexpected returncode '{p.returncode}'\nlocal command: {command}\nstdout: {stdout_bytes}\nstderr: {stderr_bytes}"
            )
    finally:
        os.chdir(old_cwd)
Beispiel #8
0
def strip(text):
    r"""Strip text helper function.

    Strip all empty lines from begin and end of text. Also strip all whitespace characters from begin and end of each line.
    Preserves '\\n' character at last line of text. Not preservers indent whitespaces characters at the begin on each line.

    Args:
        text: Text to strip, must be string or None.

    Returns:
        Text after strip. It is always string even if argument was None.

    Raises:
        :class:`~exceptions.PossibleRuntimeError`: When error occurred.
    """
    if text is None:
        text = ''
    if not isinstance(text, str):
        raise PossibleRuntimeError('strip: string expected')
    if not text:
        return text
    lines = list()
    text = text.strip() + '\n'
    for line in text.split('\n'):
        line = line.strip()
        lines.append(line)
    text = '\n'.join(lines)
    return text
Beispiel #9
0
 def decorator(func):
     task_name = func.__name__.replace('_', '-')
     if task_name not in runtime.tasks:
         runtime.tasks[task_name] = func
     else:
         raise PossibleRuntimeError(f"Task '{task_name}' already defined.")
     return func
Beispiel #10
0
 def __init__(self, hostname):
     if hostname not in runtime.inventory.hosts:
         raise PossibleRuntimeError(f"Host '{hostname}' not found.")
     self.max_hostname_len = len(max(runtime.hosts, key=len))
     self.host = runtime.inventory.hosts[hostname]
     self.ssh = SSH(self.host)
     self.hostname = hostname
Beispiel #11
0
 def chown(self, remote_filename, *, owner='root', group='root'):
     if not os.path.isabs(remote_filename):
         raise PossibleRuntimeError(
             f"Remote filename must be absolute: {remote_filename}")
     stdout = self.run('chown --changes ' + owner.strip() + ':' +
                       group.strip() + ' -- ' + remote_filename).stdout
     changed = stdout != ""
     return changed
Beispiel #12
0
 def fatal(self, *args, **kwargs):
     if not runtime.config.args.quiet:
         print(f"{self.hostname:{self.max_hostname_len}} & FATAL ERROR!!!",
               *args,
               file=sys.stdout,
               flush=True,
               **kwargs)
         raise PossibleRuntimeError(*args)
Beispiel #13
0
 def sysctl(self, line):
     line = line.strip()
     if '\n' in line:
         raise PossibleRuntimeError(
             f"sysctl setting must be one line string")
     if '=' not in line:
         raise PossibleRuntimeError(
             f"sysctl setting must be in form 'name = value'")
     name, value = line.split('=')
     name = name.strip()
     value = value.strip()
     line = f"\n{name} = {value}\n\n"
     filename = f"/etc/sysctl.d/{name}.conf"
     changed = self.put(line, filename)
     if changed:
         self.name(f"tune {name} = {value}")
         self.run(f"sysctl -p {filename}")
     return changed
Beispiel #14
0
def edit_ini_section(section_name_to_edit, *editors):
    """Edit ini section text editor.

    Apply all editors from list ``editors`` to section named ``section_name_to_edit``.
    ``editors`` is any combination of **line** editors: :func:`~insert_line`, :func:`~delete_line`, :func:`~replace_line` and so on.

    Args:
        section_name_to_edit: Name of section to edit, must be in form '[section_name]'.
        editors: List of editors to apply for selected ini section.

    Returns:
        closure function, which acts as text editor, parameterized by :func:`~edit_ini_section` arguments.

    Raises:
        :class:`~exceptions.SystemExit`: When error occurred.
    """
    if section_name_to_edit is not None:
        if section_name_to_edit[0] != '[' or section_name_to_edit[-1] != ']':
            raise PossibleRuntimeError("edit_ini_section: section name must be in form [section_name]")
        section_name_to_edit = section_name_to_edit[1:-1]

    def ini_section_editor(text):  # pylint: disable=too-many-locals
        pattern = r'^\s*\[(.*)\]\s*$'
        regex = re.compile(pattern)
        current_section_name = None
        current_section_text = list()
        section_content = dict()
        section_order = list()
        for line in text.split('\n'):
            match = regex.match(line)
            if match:
                new_section_name = match.group(1)
                if new_section_name in section_content or new_section_name == current_section_name:
                    raise PossibleRuntimeError("edit_ini_section: bad ini file, section '[%s]' duplicated in file." % new_section_name)
                section_content[current_section_name] = current_section_text
                section_order.append(current_section_name)
                current_section_name = new_section_name
                current_section_text = list()
            else:
                current_section_text.append(line)
        section_content[current_section_name] = current_section_text
        section_order.append(current_section_name)
        if section_name_to_edit in section_content:
            old_text = '\n'.join(section_content[section_name_to_edit])
            changed, new_text = _apply_editors(old_text, *editors)
            if changed:
                section_content[section_name_to_edit] = new_text.split('\n')
        else:
            raise PossibleRuntimeError("edit_ini_section: section '[%s]' not found." % section_name_to_edit)
        out = list()
        for section_name in section_order:
            if section_name is not None:
                out.append('[' + section_name + ']')
            section_text = section_content[section_name]
            out.extend(section_text)
        return '\n'.join(out)
    return ini_section_editor
Beispiel #15
0
 def add_to_authorized_keys(
         self,
         local_public_keys_filename,
         username,
         remote_authorized_keys_filename="~/.ssh/authorized_keys"):
     if os.path.isabs(local_public_keys_filename):
         raise PossibleRuntimeError(
             f"Local public keys filename must be relative: {local_public_keys_filename}"
         )
     if remote_authorized_keys_filename.startswith('~'):
         home_directory = self.user_home_directory(username).rstrip('/')
         remote_authorized_keys_filename = remote_authorized_keys_filename.lstrip(
             '~')
         remote_authorized_keys_filename = home_directory + remote_authorized_keys_filename
     if not os.path.isabs(remote_authorized_keys_filename):
         raise PossibleRuntimeError(
             f"Remote filename must be absolute: {remote_authorized_keys_filename}"
         )
     dirname = os.path.dirname(remote_authorized_keys_filename)
     if not self.is_directory(dirname):
         self.run(f"mkdir {dirname}")
         if os.path.basename(dirname) == '.ssh':
             self.chown(dirname, owner=username, group=username)
             self.chmod(dirname, mode="0700")
         else:  # shared dir for authorized keys
             self.chown(dirname, owner="root", group="root")
             self.chmod(dirname, mode="0755")
     old_content = self.get(remote_authorized_keys_filename, "")
     new_content = old_content
     keys = self.read(local_public_keys_filename)
     for key in keys.split("\n"):
         key = key.strip()
         if not key:
             continue
         new_content = edit(new_content,
                            append_line(key, insert_empty_line_before=True))
     if new_content != old_content:
         self.put(new_content, remote_authorized_keys_filename, mode="0600")
         self.chown(remote_authorized_keys_filename,
                    owner=username,
                    group=username)
         return True
     else:
         return False
Beispiel #16
0
 def run(self, command, *, stdin=None, can_fail=False):
     returncode, stdout_bytes, stderr_bytes = self.ssh.run(command,
                                                           stdin=stdin)
     result = Result(returncode, stdout_bytes, stderr_bytes)
     if result or can_fail:
         return result
     else:
         raise PossibleRuntimeError(
             f"Unexpected returncode '{returncode}'\ncommand: {command}\nstdout: {stdout_bytes}\nstderr: {stderr_bytes}"
         )
Beispiel #17
0
 def _file_transport_command(self, in_path, out_path, action):
     if not os.path.isabs(in_path):
         raise PossibleRuntimeError(f"File name must be absolute, not '{in_path}'")
     if not os.path.isabs(out_path):
         raise PossibleRuntimeError(f"File name must be absolute, not '{out_path}'")
     # scp require square brackets for IPv6 addresses,
     # but accept them for hostnames and IPv4 addresses too.
     host = '[%s]' % self.host
     if action == 'get':
         cmd = self._build_command('scp', u'{0}:{1}'.format(host, shlex.quote(in_path)), out_path)
     else:
         cmd = self._build_command('scp', in_path, u'{0}:{1}'.format(host, shlex.quote(out_path)))
     debug.print(f"SCP command: {cmd}")
     (returncode, stdout, stderr) = self._run(cmd, stdin=None)
     if returncode == 0:
         return (returncode, stdout, stderr)
     elif returncode == 255:
         raise PossibleError("Failed to connect to the host %s via scp" % (to_text(stderr)))
     else:
         raise PossibleError("Failed to transfer file %s to %s:\n%s\n%s" %
                             (to_text(in_path), to_text(out_path), to_text(stdout), to_text(stderr)))
Beispiel #18
0
def _apply_editors(old_text, *editors):
    if not editors:
        raise PossibleRuntimeError("editors can't be empty.")
    text = old_text
    for editor in editors:
        text = editor(text)
    text_after_first_pass = text
    for editor in editors:
        text = editor(text)
    text_after_second_pass = text
    if text_after_first_pass != text_after_second_pass:
        debug.print("="*80)
        debug.print(f"0 run: >>>{old_text}<<<")
        debug.print("="*80)
        debug.print(f"1 run: >>>{text_after_first_pass}<<<")
        debug.print("="*80)
        debug.print(f"2 run: >>>{text_after_second_pass}<<<")
        debug.print("="*80)
        raise PossibleRuntimeError("editors is not idempotent.")
    new_text = text_after_second_pass
    changed = new_text != old_text
    return changed, new_text
Beispiel #19
0
def allow(*args):
    def decorator(func):
        task_name = func.__name__.replace('_', '-')
        if task_name not in runtime.permissions:
            runtime.permissions[task_name] = set()
        runtime.permissions[task_name].update(args)
        return func

    if len(args) == 1 and callable(args[0]):
        task_name = args[0].__name__.replace('_', '-')
        raise PossibleRuntimeError(
            f"Task '{task_name}': @allow list required.")
    else:
        return decorator
Beispiel #20
0
 def reboot(self, *, wait_seconds=180, reboot_command="reboot"):
     assert wait_seconds > 30
     old_uptime = self.run('stat --printf="%y" /proc/1/cmdline').stdout
     self.run(reboot_command, can_fail=True)
     while wait_seconds > 0:
         time.sleep(1)
         result = self.run('stat --printf="%y" /proc/1/cmdline',
                           can_fail=True)
         if result:
             current_uptime = result.stdout
             if old_uptime != current_uptime:
                 return
         wait_seconds = wait_seconds - 1
     raise PossibleRuntimeError(f"Reboot host {self.hostname} failed.")
Beispiel #21
0
 def copy(self, local_filename, remote_filename, *, mode='0644'):
     if os.path.isabs(local_filename):
         raise PossibleRuntimeError(
             f"Local filename must be relative: {local_filename}")
     local_filename = str(runtime.config.files / local_filename)
     if not os.path.isabs(remote_filename):
         raise PossibleRuntimeError(
             f"Remote filename must be absolute: {remote_filename}")
     if self.is_file(remote_filename):
         local_file = open(local_filename, mode="rb")
         local_content = local_file.read()
         local_file.close()
         remote_content = self.get(remote_filename, as_bytes=True)
         if local_content == remote_content:
             changed = self.chmod(remote_filename, mode=mode)
             return changed
     returncode, stdout_bytes, stderr_bytes = self.ssh.put(
         local_filename, remote_filename)
     if returncode != 0:
         raise PossibleRuntimeError(
             f"Unexpected returncode '{returncode}'\ncommand: copy({local_filename}, {remote_filename})\nstdout: {stdout_bytes}\nstderr: {stderr_bytes}"
         )
     self.chmod(remote_filename, mode=mode)
     return True
Beispiel #22
0
def group(arg):
    def decorator(func):
        task_name = func.__name__.replace('_', '-')
        if task_name not in runtime.groups:
            runtime.groups[task_name] = arg
        else:
            raise PossibleRuntimeError(
                f"Task '{task_name}': @group already defined.")
        return func

    if callable(arg):
        task_name = arg.__name__.replace('_', '-')
        raise PossibleRuntimeError(
            f"Task '{task_name}': @group name required.")
    else:
        return decorator
Beispiel #23
0
    def remove_file(self, remote_filename):
        """Remove remote file.

        Args:
            remote_filename: Remote file name, must be absolute.

        Returns:
            True if file removed, False if file not exists.
        """
        if not os.path.isabs(remote_filename):
            raise PossibleRuntimeError(
                f"Remote filename must be absolute: {remote_filename}")
        changed = self.run(
            f'if [ -f {remote_filename} ] ; then rm -f -- {remote_filename} ; echo removed ; fi'
        ).stdout == 'removed'
        return changed
Beispiel #24
0
    def user_home_directory(self, name):
        """get user home directory

        Args:
            name: user name

        Returns:
            Home directory if user exists or None if user not exists.
        """
        passwd = self.run("getent passwd").stdout
        for line in passwd.split("\n"):
            line = line.strip()
            if not line:
                continue
            if line.split(":")[0] == name:
                return line.split(":")[5]
        raise PossibleRuntimeError(f"User '{name}' not found.")
Beispiel #25
0
 def read(self, local_filename, default_value=None, *, as_bytes=False):
     if os.path.isabs(local_filename):
         raise PossibleRuntimeError(
             f"Local filename must be relative: {local_filename}")
     local_filename = str(runtime.config.files / local_filename)
     if not os.path.isfile(local_filename):
         if default_value is None:
             raise PossibleFileNotFound(
                 f"Local file does not exist: {local_filename}")
         else:
             return default_value
     local_file = open(local_filename, "rb")
     content = local_file.read()
     local_file.close()
     if as_bytes:
         return content
     else:
         return to_text(content)
Beispiel #26
0
    def remove_directory(self, remote_dirname):
        """Remove remote directory.

        .. note::
            Remote directory must be empty. Recursive deletion of non-empty directories is not supported.

        Args:
            remote_dirname: Remote directory name, must be absolute.

        Returns:
            True if directory removed, False if directory already not exists.
        """
        if not os.path.isabs(remote_dirname):
            raise PossibleRuntimeError(
                f"Remote dirname must be absolute: {remote_dirname}")
        changed = self.run(
            f'if [ -d {remote_dirname} ] ; then rmdir -- {remote_dirname} ; echo removed ; fi'
        ).stdout == 'removed'
        return changed
Beispiel #27
0
    def create_directory(self, remote_dirname):
        """Create remote directory.

        .. note::
            Directory created only if no file exists with name ``remote_dirname``. Existing file will not be deleted.

        Args:
            remote_dirname: Remote directory name, must be absolute.

        Returns:
            True if directory created, False if directory already exists.
        """
        if not os.path.isabs(remote_dirname):
            raise PossibleRuntimeError(
                f"Remote dirname must be absolute: {remote_dirname}")
        changed = self.run(
            f'if [ ! -d {remote_dirname} ] ; then mkdir -- {remote_dirname} ; echo created ; fi'
        ).stdout == 'created'
        return changed
Beispiel #28
0
def insert_line(line_to_insert, **kwargs):
    """Insert line editor.

    Inserts ``line_to_insert`` before or after specified line.
    One and only one keyword argument expected: ``before`` or ``after``

    Args:
        line_to_insert: Line to insert in text.
        before: Anchor pattern, before which text should be inserted.
        after: Anchor pattern, after which text should be inserted.

    Returns:
        closure function, which acts as text editor, parameterized by :func:`~insert_line` arguments.

    Raises:
        :class:`~exceptions.SystemExit`: When error occurred.
    """
    insert_type = None
    anchor_pattern = None
    for name in sorted(kwargs):
        if insert_type is None:
            if name == 'before' or name == 'after':
                insert_type = name
                anchor_pattern = _full_line(kwargs[name])
            else:
                raise PossibleRuntimeError("insert_line: unknown insert_type '%s'" % name)
        else:
            raise PossibleRuntimeError("insert_line: already defined insert_type '%s', unexpected '%s'" % (insert_type, name))
    if insert_type is None:
        raise PossibleRuntimeError("insert_line: must be defined 'before' or 'after' argument.")

    def insert_line_editor(text):
        regex = re.compile(anchor_pattern)
        text_lines = text.split('\n')
        line_already_inserted = False
        anchor_lines = 0
        for line in text_lines:
            match = regex.match(line)
            if match:
                anchor_lines += 1
            if line == line_to_insert:
                line_already_inserted = True
        if anchor_lines == 0:
            raise PossibleRuntimeError("insert_line: anchor pattern '%s' not found." % anchor_pattern)
        elif anchor_lines > 1:
            raise PossibleRuntimeError("insert_line: anchor pattern '%s' found %d times, must be only one." % (anchor_pattern, anchor_lines))
        out = list()
        for line in text_lines:
            match = regex.match(line)
            if match and not line_already_inserted:
                if insert_type == 'before':
                    out.append(line_to_insert)
                    out.append(line)
                    continue
                else:  # insert_type == 'after':
                    out.append(line)
                    out.append(line_to_insert)
                    continue
            else:
                out.append(line)
        return '\n'.join(out)
    return insert_line_editor
Beispiel #29
0
 def decorator_with_arguments(func):
     task_name = func.__name__.replace('_', '-')
     raise PossibleRuntimeError(
         f"Task '{task_name}': @task can't have arguments.")