Example #1
0
 def error_handler_with_conversion(e):
     # We can’t use log.exception() because the traceback is no longer available.
     # So the three cases in dbus_handle_exceptions amount to just this.
     if not isinstance(e, DBusException):
         log.error("{0}: {1}".format(type(e), str(e)))
         e = DBusException(str(e))
     error_handler(e)
Example #2
0
def main():
    args = parse_cmdline()

    setup_logging(args)
    log.debug1("Arguments: %s" % sys.argv)

    # Read the password from stdin
    user_pass = input(False)

    # Check for valid database and user names
    # We restrict this to having only letters, numbers
    # and underscores, for safety against SQL injection
    identifier = re.compile(r"^[^\d\W]\w*\Z")

    if not identifier.match(args.user):
        log.error("The user name was not a valid identifier.")
        sys.exit(1)

    # Connect to the local database via 'peer'
    conn = psycopg2.connect(database=args.database)
    conn.autocommit = True
    log.info1("Connected to local database '%s'" % args.database)
    cur = conn.cursor()


    # Construct the SQL statement
    sql_msg = ("ALTER ROLE %s WITH ENCRYPTED PASSWORD" %
               (args.user) + " %(pwd)s;")
    log.info1("Executing: %s" % sql_msg)
    log.debug10("Password: [%s]", user_pass)

    # Submit the request
    cur.execute(sql_msg, {'pwd': user_pass})

    sys.exit(0)
Example #3
0
 def error_handler_with_conversion(e):
     # We can’t use log.exception() because the traceback is no longer available.
     # So the three cases in dbus_handle_exceptions amount to just this.
     if not isinstance(e, DBusException):
         log.error("{0}: {1}".format(type(e), str(e)))
         e = DBusException(str(e))
     error_handler(e)
Example #4
0
def main():
    args = parse_cmdline()

    setup_logging(args)
    log.debug1("Arguments: %s" % sys.argv)

    # Read the password from stdin
    user_pass = input(False)

    # Check for valid database and user names
    # We restrict this to having only letters, numbers
    # and underscores, for safety against SQL injection
    identifier = re.compile(r"^[^\d\W]\w*\Z")

    if not identifier.match(args.user):
        log.error("The user name was not a valid identifier.")
        sys.exit(1)

    # Connect to the local database via 'peer'
    conn = psycopg2.connect(database=args.database)
    conn.autocommit = True
    log.info1("Connected to local database '%s'" % args.database)
    cur = conn.cursor()

    # Construct the SQL statement
    sql_msg = ("ALTER ROLE %s WITH ENCRYPTED PASSWORD" % (args.user) +
               " %(pwd)s;")
    log.info1("Executing: %s" % sql_msg)
    log.debug10("Password: [%s]", user_pass)

    # Submit the request
    cur.execute(sql_msg, {'pwd': user_pass})

    sys.exit(0)
Example #5
0
def run_server(debug_gc=False, persistent=False):
    """ Main function for rolekit server. Handles D-Bus and GLib mainloop.
    """
    service = None
    if debug_gc:
        from pprint import pformat
        import gc
        gc.enable()
        gc.set_debug(gc.DEBUG_LEAK)

        gc_timeout = 10
        def gc_collect():
            gc.collect()
            if len(gc.garbage) > 0:
                print("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
                      ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n")
                print("GARBAGE OBJECTS (%d):\n" % len(gc.garbage))
                for x in gc.garbage:
                    print(type(x),"\n  ",)
                    print(pformat(x))
                print("\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
                      "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n")
            GLib.timeout_add_seconds(gc_timeout, gc_collect)

    try:
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        bus = dbus.SystemBus()
        name = dbus.service.BusName(DBUS_INTERFACE, bus=bus)
        service = RoleD(name, DBUS_PATH, persistent=persistent)

        mainloop = GLib.MainLoop()
        slip.dbus.service.set_mainloop(mainloop)
        if debug_gc:
            GLib.timeout_add_seconds(gc_timeout, gc_collect)

        # use unix_signal_add if available, else unix_signal_add_full
        if hasattr(GLib, 'unix_signal_add'):
            unix_signal_add = GLib.unix_signal_add
        else:
            unix_signal_add = GLib.unix_signal_add_full

        unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGHUP,
                        sighup, None)
        unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM,
                        sigterm, mainloop)

        mainloop.run()

    except KeyboardInterrupt as e:
        pass

    except SystemExit as e:
        log.error("Raising SystemExit in run_server")

    except Exception as e:
        log.error("Exception %s: %s", e.__class__.__name__, str(e))

    if service:
        service.stop()
Example #6
0
def readfile(filename):
    try:
        with open(filename, "r") as f:
            line = "".join(f.readlines())
    except Exception as e:
        log.error('Failed to read file "%s": %s' % (filename, e))
        return None
    return line
Example #7
0
def writefile(filename, line):
    try:
        with open(filename, "w") as f:
            f.write(line)
    except Exception as e:
        log.error('Failed to write to file "%s": %s' % (filename, e))
        return False
    return True
Example #8
0
def readfile(filename):
    try:
        with open(filename, "r") as f:
            line = "".join(f.readlines())
    except Exception as e:
        log.error('Failed to read file "%s": %s' % (filename, e))
        return None
    return line
Example #9
0
def writefile(filename, line):
    try:
        with open(filename, "w") as f:
            f.write(line)
    except Exception as e:
        log.error('Failed to write to file "%s": %s' % (filename, e))
        return False
    return True
Example #10
0
    def __init__(self, parent, name, type_name, directory, settings,
                 *args, **kwargs):
        """The DBUS_INTERFACE_ROLE_INSTANCE implementation.

        :param parent: The DBusRole Object this is attached to
        :param name: Instance name
        :param type_name: Role name
        :param directory: FIXME: unused???
        :param settings: RoleSettings for the role
        :param path: (Implicit in *args) FIXME: unused???
        """
        super(RoleBase, self).__init__(*args, **kwargs)
        self._path = args[0]
        self._parent = parent
        self._name = name
        self._escaped_name = dbus_label_escape(name)
        self._type = type_name
        self._escaped_type = dbus_label_escape(type_name)
        self._log_prefix = "role.%s.%s" % (self._escaped_type,
                                           self._escaped_name)
        self._directory = directory
        self._settings = settings
        # TODO: place target_unit in settings
        self.target_unit = "role-%s-%s.target" % (self._type, self.get_name())

        # No loaded self._settings, set state to NASCENT
        if not "state" in self._settings:
            self._settings["state"] = NASCENT

        # Check role instance state if role instance is in READY_TO_START or
        # RUNNING state
        if self._settings["state"] in [ READY_TO_START, RUNNING ]:
            try:
                state = target_unit_state(self.target_unit)
            except Exception as e:
                log.error("Getting information about the unit target failed: %s", e)
            else:
                # Update state:
                #
                #  Old instance   | systemd unit | New instance
                #  state          | target state | state
                # ----------------+--------------+----------------
                #  RUNNING        | inactive     | READY_TO_START
                #  READY_TO_START | active       | RUNNING

                if state == "inactive" and self._settings["state"] == RUNNING:
                    log.warning("'%s' is inactive, moving to %s state.",
                                self.target_unit, READY_TO_START)
                    self.change_state(READY_TO_START, write=True)
                elif state == "active" and \
                     self._settings["state"] == READY_TO_START:
                    log.warning("'%s' is active, moving to %s state.",
                                self.target_unit, RUNNING)
                    self.change_state(RUNNING, write=True)

        self.timeout_restart()
Example #11
0
    def remove(self):
        try:
            self.backup()
        except Exception as msg:
            log.error(msg)

        try:
            os.remove(self.filepath)
        except OSError:
            pass
Example #12
0
def handle_exceptions(func, *args, **kwargs):
    """Decorator to handle exceptions and log them. Used if not connected
    to D-Bus.
    """
    try:
        return func(*args, **kwargs)
    except RolekitError as error:
        log.error("{0}: {1}".format(type(error).__name__, str(error)))
    except Exception:
        log.exception()
Example #13
0
    def remove(self):
        try:
            self.backup()
        except Exception as msg:
            log.error(msg)

        try:
            os.remove(self.filepath)
        except OSError:
            pass
Example #14
0
    def check_values(self, values):
        # Check key value pairs for the properties
        values = dbus_to_python(values)

        for x in values:
            if x in self._DEFAULTS:
                if x in self._READONLY_SETTINGS:
                    raise RolekitError(READONLY_SETTING, x)
                # use _check_property method from derived or parent class
                self._check_property(x, values[x])
            else:
                log.error("Unknown property: %s" % x)
                raise RolekitError(UNKNOWN_SETTING, x)
Example #15
0
    def __init__(self, role, name, directory, *args, **kwargs):
        """The DBUS_INTERFACE_ROLE implementation

        :param role: RoleBase descendant
        :param name: Role name
        :param directory: FIXME: unused???
        :param path: (Implicit in *args) FIXME: unused???
        """
        super(DBusRole, self).__init__(*args, **kwargs)
        self._path = args[0]
        self._role = role
        self._name = name
        self._escaped_name = dbus_label_escape(name)
        self._directory = directory
        self._instances = {}

        # create instances for stored instance settings

        path = "%s/%s" % (ETC_ROLEKIT_ROLES, self._name)
        if os.path.exists(path) and os.path.isdir(path):
            for name in sorted(os.listdir(path)):
                if not name.endswith(".json"):
                    continue
                instance = name[:-5]
                log.debug1("Loading '%s' instance '%s'", self._name, instance)

                settings = RoleSettings(self._name, instance)
                try:
                    settings.read()
                except ValueError as e:
                    log.error("Failed to load '%s' instance '%s': %s",
                              self._name, instance, e)
                    continue

                instance_escaped_name = dbus_label_escape(instance)
                if instance_escaped_name in self._instances:
                    raise RolekitError(NAME_CONFLICT, instance_escaped_name)

                role = self._role(self,
                                  instance,
                                  self._name,
                                  self._directory,
                                  settings,
                                  self._path,
                                  "%s/%s/%s" %
                                  (DBUS_PATH_ROLES, self._escaped_name,
                                   instance_escaped_name),
                                  persistent=self.persistent)
                self._instances[instance_escaped_name] = role

        self.timeout_restart()
Example #16
0
    def __init__(self, role, name, directory, *args, **kwargs):
        """The DBUS_INTERFACE_ROLE implementation

        :param role: RoleBase descendant
        :param name: Role name
        :param directory: FIXME: unused???
        :param path: (Implicit in *args) FIXME: unused???
        """
        super(DBusRole, self).__init__(*args, **kwargs)
        self._path = args[0]
        self._role = role
        self._name = name
        self._escaped_name = dbus_label_escape(name)
        self._directory = directory
        self._instances = {}

        # create instances for stored instance settings

        path = "%s/%s" % (ETC_ROLEKIT_ROLES, self._name)
        if os.path.exists(path) and os.path.isdir(path):
            for name in sorted(os.listdir(path)):
                if not name.endswith(".json"):
                    continue
                instance = name[:-5]
                log.debug1("Loading '%s' instance '%s'", self._name, instance)

                settings = RoleSettings(self._name, instance)
                try:
                    settings.read()
                except ValueError as e:
                    log.error("Failed to load '%s' instance '%s': %s", self._name, instance, e)
                    continue

                instance_escaped_name = dbus_label_escape(instance)
                if instance_escaped_name in self._instances:
                    raise RolekitError(NAME_CONFLICT, instance_escaped_name)

                role = self._role(
                    self,
                    instance,
                    self._name,
                    self._directory,
                    settings,
                    self._path,
                    "%s/%s/%s" % (DBUS_PATH_ROLES, self._escaped_name, instance_escaped_name),
                    persistent=self.persistent,
                )
                self._instances[instance_escaped_name] = role

        self.timeout_restart()
Example #17
0
    def start(self):
        """ starts rolekit """
        log.debug1("start()")

        try:
            os.makedirs(ETC_ROLEKIT_ROLES)
        except OSError as e:
            if e.errno == errno.EEXIST:
                if not os.path.isdir(ETC_ROLEKIT_ROLES):
                    log.fatal("'%s' is not a directory.", e.strerror)
            else:
                log.fatal("Failed to create '%s': %s", e.strerror)
                raise
        else:
            log.info1("Created missing '%s'.", ETC_ROLEKIT_ROLES)

        path = ROLEKIT_ROLES

        if not os.path.exists(path) or not os.path.isdir(path):
            log.error("Role directory '%s' does not exist.", path)
            return

        for name in sorted(os.listdir(path)):
            directory = "%s/%s" % (path, name)
            if not os.path.isdir(directory):
                continue

            if not os.path.exists(os.path.join(directory, "role.py")):
                continue

            log.debug1("Loading role '%s'", name)
            escaped_name = dbus_label_escape(name)

            try:
                if os.path.exists(os.path.join(directory, "role.py")):
                    mod = imp.load_source(name, "%s/role.py" % directory)

                # get Role from module
                role = getattr(mod, "Role")

                # create role object that contains the role instance class
                obj = DBusRole(role,
                               name,
                               directory,
                               self._path,
                               "%s/%s" % (DBUS_PATH_ROLES, escaped_name),
                               persistent=self.persistent)

                if obj in self._roles:
                    log.error("Duplicate role '%s'", obj.name)
                else:
                    self._roles.append(obj)
            except RolekitError as msg:
                log.error("Failed to load role '%s': %s", name, msg)
                continue
            except Exception as msg:
                log.error("Failed to load role '%s':", name)
                log.exception()
                continue
Example #18
0
    def get_dbus_property(self, prop):
        if prop == "name":
            return dbus.String(self.get_property(prop))
        elif prop == "DEFAULTS":
            ret = dbus.Dictionary(signature="sv")
            for x in self._role._DEFAULTS:
                try:
                    ret[x] = self._role.get_dbus_property(self._role, x)
                except Exception as e:
                    log.error("role.%s.DEFAULTS(): Failed to get/convert property '%s'", self._escaped_name, x)
                    pass
            return ret

        raise dbus.exceptions.DBusException(
            "org.freedesktop.DBus.Error.AccessDenied: " "Property '%s' isn't exported (or may not exist)" % prop
        )
Example #19
0
def dbus_handle_exceptions(func, *args, **kwargs):
    """Decorator to handle exceptions, log and convert into DBusExceptions.

    :Raises DBusException: on any exception raised by the decorated function.
    """
    # Keep this in sync with async.start_with_dbus_callbacks()
    try:
        return func(*args, **kwargs)
    except RolekitError as error:
        log.error(str(error))
        raise DBusException(str(error))
    except DBusException:
        # only log DBusExceptions once, pass it through
        raise
    except Exception as e:
        log.exception()
        raise DBusException(str(e))
Example #20
0
    def get_dbus_property(self, prop):
        if prop == "name":
            return dbus.String(self.get_property(prop))
        elif prop == "DEFAULTS":
            ret = dbus.Dictionary(signature="sv")
            for x in self._role._DEFAULTS:
                try:
                    ret[x] = self._role.get_dbus_property(self._role, x)
                except Exception as e:
                    log.error("{}.DEFAULTS: Failed to get/convert "
                              "property '{}': {}".format(
                                  self._log_prefix, x, e))
            return ret

        raise dbus.exceptions.DBusException(
            "org.freedesktop.DBus.Error.AccessDenied: "
            "Property '%s' isn't exported (or may not exist)" % prop)
Example #21
0
    def start(self):
        """ starts rolekit """
        log.debug1("start()")

        try:
            os.makedirs(ETC_ROLEKIT_ROLES)
        except OSError as e:
            if e.errno == errno.EEXIST:
                if not os.path.isdir(ETC_ROLEKIT_ROLES):
                    log.fatal("'%s' is not a directory.", e.strerror)
            else:
                log.fatal("Failed to create '%s': %s", e.strerror)
                raise
        else:
            log.info1("Created missing '%s'.", ETC_ROLEKIT_ROLES)

        path = ROLEKIT_ROLES

        if not os.path.exists(path) or not os.path.isdir(path):
            log.error("Role directory '%s' does not exist.", path)
            return

        for name in sorted(os.listdir(path)):
            directory = "%s/%s" % (path, name)
            if not os.path.isdir(directory):
                continue

            if not os.path.exists(os.path.join(directory, "role.py")):
                continue

            log.debug1("Loading role '%s'", name)
            escaped_name = dbus_label_escape(name)

            try:
                if os.path.exists(os.path.join(directory, "role.py")):
                    mod = imp.load_source(name, "%s/role.py" % directory)

                    # get Role from module
                    role = getattr(mod, "Role")

                    # create role object that contains the role instance class
                    obj = DBusRole(role, name, directory, self.busname,
                                    "%s/%s" % (DBUS_PATH_ROLES, escaped_name),
                                   persistent=self.persistent)

                    if obj in self._roles:
                        log.error("Duplicate role '%s'", obj.get_name())
                    else:
                        self._roles.append(obj)
            except RolekitError as msg:
                log.error("Failed to load role '%s': %s", name, msg)
                continue
            except Exception as msg:
                log.error("Failed to load role '%s':", name)
                log.exception()
                continue
Example #22
0
    def get_dbus_property(self, prop):
        if prop == "name":
            return dbus.String(self.get_property(prop))
        elif prop == "DEFAULTS":
            ret = dbus.Dictionary(signature="sv")
            for x in self._role._DEFAULTS:
                try:
                    ret[x] = self._role.get_dbus_property(self._role, x)
                except Exception as e:
                    log.error(
                        "role.%s.DEFAULTS(): Failed to get/convert property '%s'",
                        self._escaped_name, x)
                    pass
            return ret

        raise dbus.exceptions.DBusException(
            "org.freedesktop.DBus.Error.AccessDenied: "
            "Property '%s' isn't exported (or may not exist)" % prop)
Example #23
0
    def get_dbus_property(self, prop):
        if prop == "name":
            return dbus.String(self.get_property(prop))
        elif prop == "DEFAULTS":
            ret = dbus.Dictionary(signature = "sv")
            for x in self._role._DEFAULTS:
                try:
                    ret[x] = self._role.get_dbus_property(self._role, x)
                except Exception as e:
                    log.error(
                        "{}.DEFAULTS: Failed to get/convert "
                        "property '{}': {}".format(
                            self._log_prefix, x, e))
            return ret

        raise dbus.exceptions.DBusException(
            "org.freedesktop.DBus.Error.AccessDenied: "
            "Property '%s' isn't exported (or may not exist)" % prop)
Example #24
0
    def input_handler(unused_fd, condition, unused_data):
        finished = True
        if (condition & (GLib.IOCondition.ERR | GLib.IOCondition.NVAL)) != 0:
            log.error("Unexpected input handler state %s" % condition)
        else:
            assert (condition &
                    (GLib.IOCondition.IN | GLib.IOCondition.HUP)) != 0
            # Note that HUP and IN can happen at the same time, so don’t
            # explicitly test for HUP.
            try:
                chunk = fd.read()
            except IOError as e:
                log.error("Error reading subprocess output: %s" % e)
            else:
                if len(chunk) > 0:
                    output_chunks.append(chunk.decode('utf-8'))

                    # Log the input at the requested level
                    lines = (linebuf[0] + chunk.decode('utf-8')).split('\n')
                    for line in lines[:-1]:
                        try:
                            msg = line.encode(errors='backslashreplace')
                        except UnicodeError:
                            # Line contains non-ASCII content that
                            # cannot be escaped. Log it as base64.
                            msg = line.encode(encoding='base64')
                        log_fn(msg)
                    linebuf[0] = lines[-1]

                    # Continue until there's no more data to be had
                    finished = False

        if finished:
            fd.close()
            future.set_result("".join(output_chunks))
            return False
        return True
Example #25
0
    def input_handler(unused_fd, condition, unused_data):
        finished = True
        if (condition & (GLib.IOCondition.ERR | GLib.IOCondition.NVAL)) != 0:
            log.error("Unexpected input handler state %s" % condition)
        else:
            assert (condition & (GLib.IOCondition.IN | GLib.IOCondition.HUP)) != 0
            # Note that HUP and IN can happen at the same time, so don’t
            # explicitly test for HUP.
            try:
                chunk = fd.read()
            except IOError as e:
                log.error("Error reading subprocess output: %s" % e)
            else:
                if len(chunk) > 0:
                    output_chunks.append(chunk.decode('utf-8'))

                    # Log the input at the requested level
                    lines = (linebuf[0] + chunk.decode('utf-8')).split('\n')
                    for line in lines[:-1]:
                        try:
                            msg = line.encode(errors='backslashreplace')
                        except UnicodeError:
                            # Line contains non-ASCII content that
                            # cannot be escaped. Log it as base64.
                            msg = line.encode(encoding='base64')
                        log_fn(msg)
                    linebuf[0] = lines[-1];

                    # Continue until there's no more data to be had
                    finished = False

        if finished:
            fd.close()
            future.set_result("".join(output_chunks))
            return False
        return True
Example #26
0
    def do_deploy_async(self, values, sender=None):
        log.debug9("TRACE do_deploy_async(databaseserver)")
        # Do the magic
        #
        # In case of error raise an exception

        first_instance = True

        # Check whether this is the first instance of the database
        for value in self._parent.get_instances().values():
            if ('databaseserver' == value._type and self._name != value._name
                    and self.get_state() in deployed_states):
                first_instance = False
                break

        # First, check for all mandatory arguments
        if 'database' not in values:
            raise RolekitError(INVALID_VALUE, "Database name unset")

        if 'owner' not in values:
            # We'll default to db_owner
            values['owner'] = "db_owner"

        # We will assume the owner is new until adding them fails
        new_owner = True

        # Determine if a password was passed in, so we know whether to
        # suppress it from the settings list later.
        if 'password' in values:
            password_provided = True
        else:
            password_provided = False

        if 'postgresql_conf' not in values:
            values['postgresql_conf'] = self._settings['postgresql_conf']

        if 'pg_hba_conf' not in values:
            values['pg_hba_conf'] = self._settings['pg_hba_conf']

        # Get the UID and GID of the 'postgres' user
        try:
            self.pg_uid = pwd.getpwnam('postgres').pw_uid
        except KeyError:
            raise RolekitError(MISSING_ID,
                               "Could not retrieve UID for postgress user")

        try:
            self.pg_gid = grp.getgrnam('postgres').gr_gid
        except KeyError:
            raise RolekitError(MISSING_ID,
                               "Could not retrieve GID for postgress group")

        if first_instance:
            # Initialize the database on the filesystem
            initdb_args = ["/usr/bin/postgresql-setup", "--initdb"]

            log.debug2("TRACE: Initializing database")
            result = yield async .subprocess_future(initdb_args)
            if result.status:
                # If this fails, it may be just that the filesystem
                # has already been initialized. We'll log the message
                # and continue.
                log.debug1("INITDB: %s" % result.stdout)

        # Now we have to start the service to set everything else up
        # It's safe to start an already-running service, so we'll
        # just always make this call, particularly in case other instances
        # exist but aren't running.
        log.debug2("TRACE: Starting postgresql.service unit")
        try:
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.StartUnit(
                    "postgresql.service", "replace")
                job_handler.register_job(job_path)
                log.debug2("TRACE: unit start job registered")

                job_results = yield job_handler.all_jobs_done_future()

                log.debug2("TRACE: unit start job concluded")

                if any([
                        x for x in job_results.values()
                        if x not in ("skipped", "done")
                ]):
                    details = ", ".join(
                        ["%s: %s" % item for item in job_results.items()])
                    log.error("Starting services failed: {}".format(details))
                    raise RolekitError(
                        COMMAND_FAILED,
                        "Starting services failed: %s" % details)
        except Exception as e:
            log.error("Error received starting unit: {}".format(e))
            raise

        # Next we create the owner
        log.debug2("TRACE: Creating owner of new database")
        createuser_args = ["/usr/bin/createuser", values['owner']]
        result = yield async .subprocess_future(createuser_args,
                                                uid=self.pg_uid,
                                                gid=self.pg_gid)

        if result.status:
            # If the subprocess returned non-zero, the user probably already exists
            # (such as when we're using db_owner). If the caller was trying to set
            # a password, they probably didn't realize this, so we need to throw
            # an exception.
            log.info1("User {} already exists in the database".format(
                values['owner']))

            if password_provided:
                raise RolekitError(INVALID_SETTING,
                                   "Cannot set password on pre-existing user")

            # If no password was specified, we'll continue
            new_owner = False

        # If no password was requested, generate a random one here
        if not password_provided:
            values['password'] = generate_password()

        log.debug2("TRACE: Creating new database")
        createdb_args = [
            "/usr/bin/createdb", values['database'], "-O", values['owner']
        ]
        result = yield async .subprocess_future(createdb_args,
                                                uid=self.pg_uid,
                                                gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, raise an exception
            raise RolekitError(COMMAND_FAILED,
                               "Creating database failed: %d" % result.status)

        # Next, set the password on the owner
        # We'll skip this phase if the the user already existed
        if new_owner:
            log.debug2("TRACE: Setting password for database owner")
            pwd_args = [
                ROLEKIT_ROLES + "/databaseserver/tools/rk_db_setpwd.py",
                "--database", values['database'], "--user", values['owner']
            ]
            result = yield async .subprocess_future(pwd_args,
                                                    stdin=values['password'],
                                                    uid=self.pg_uid,
                                                    gid=self.pg_gid)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                log.error("Setting owner password failed: {}".format(
                    result.status))
                raise RolekitError(
                    COMMAND_FAILED,
                    "Setting owner password failed: %d" % result.status)

            # If this password was provided by the user, don't save it to
            # the settings for later retrieval. That could be a security
            # issue
            if password_provided:
                values.pop("password", None)
        else:  # Not a new owner
            # Never save the password to settings for an existing owner
            log.debug2("TRACE: Owner already exists, not setting password")
            values.pop("password", None)

        if first_instance:
            # Then update the server configuration to accept network
            # connections.
            # edit postgresql.conf to add listen_addresses = '*'
            log.debug2("TRACE: Opening access to external addresses")
            sed_args = [
                "/bin/sed", "-e",
                "s@^[#]listen_addresses\W*=\W*'.*'@listen_addresses = '\*'@",
                "-i.rksave", values['postgresql_conf']
            ]
            result = yield async .subprocess_future(sed_args)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                raise RolekitError(
                    COMMAND_FAILED,
                    "Changing listen_addresses in '%s' failed: %d" %
                    (values['postgresql_conf'], result.status))

            # Edit pg_hba.conf to allow 'md5' auth on IPv4 and
            # IPv6 interfaces.
            sed_args = [
                "/bin/sed", "-e", "s@^host@#host@", "-e",
                '/^local/a # Use md5 method for all connections', "-e",
                '/^local/a host    all             all             all                     md5',
                "-i.rksave", values['pg_hba_conf']
            ]

            result = yield async .subprocess_future(sed_args)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                raise RolekitError(
                    COMMAND_FAILED,
                    "Changing all connections to use md5 method in '%s' failed: %d"
                    % (values['pg_hba_conf'], result.status))

            # Restart the postgresql server to accept the new configuration
            log.debug2("TRACE: Restarting postgresql.service unit")
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.RestartUnit(
                    "postgresql.service", "replace")
                job_handler.register_job(job_path)

                job_results = yield job_handler.all_jobs_done_future()
                if any([
                        x for x in job_results.values()
                        if x not in ("skipped", "done")
                ]):
                    details = ", ".join(
                        ["%s: %s" % item for item in job_results.items()])
                    raise RolekitError(
                        COMMAND_FAILED,
                        "Restarting service failed: %s" % details)

        # Create the systemd target definition
        #
        # We use all of BindsTo, Requires and RequiredBy so we can ensure that
        # all database instances are started and stopped together, since
        # they're really all a single daemon service.
        #
        # The intention here is that starting or stopping any role instance or
        # the main postgresql server will result in the same action happening
        # to all roles. This way, rolekit maintains an accurate view of what
        # instances are running and can communicate that to anyone registered
        # to listen for notifications.

        target = {
            'Role': 'databaseserver',
            'Instance': self.get_name(),
            'Description': "Database Server Role - %s" % self.get_name(),
            'BindsTo': ['postgresql.service'],
            'Requires': ['postgresql.service'],
            'RequiredBy': ['postgresql.service'],
            'After': ['syslog.target', 'network.target']
        }

        log.debug2("TRACE: Database server deployed")

        yield target
Example #27
0
    def __init__(self, role, name, directory, *args, **kwargs):
        """The DBUS_INTERFACE_ROLE implementation

        :param role: RoleBase descendant
        :param name: Role name
        :param directory: FIXME: unused???
        :param path: (Implicit in *args) FIXME: unused???
        """
        super(DBusRole, self).__init__(*args, **kwargs)
        self.busname = args[0]
        self.path = args[1]
        self._role = role
        self._name = name
        self._escaped_name = dbus_label_escape(name)
        self._log_prefix = "role.%s" % self._escaped_name
        self._directory = directory
        self._instances = {}

        # create instances for stored instance settings

        path = "%s/%s" % (ETC_ROLEKIT_ROLES, self.get_name())
        if os.path.exists(path) and os.path.isdir(path):
            for name in sorted(os.listdir(path)):
                if not name.endswith(".json"):
                    continue
                instance = name[:-5]
                log.debug1("Loading '%s' instance '%s'", self.get_name(),
                           instance)

                settings = RoleSettings(self.get_name(), instance)
                try:
                    settings.read()
                except ValueError as e:
                    log.error("Failed to load '%s' instance '%s': %s",
                              self.get_name(), instance, e)
                    continue

                instance_escaped_name = dbus_label_escape(instance)
                if instance_escaped_name in self._instances:
                    raise RolekitError(NAME_CONFLICT, instance_escaped_name)

                role = self._role(self,
                                  instance,
                                  self.get_name(),
                                  self._directory,
                                  settings,
                                  self.busname,
                                  "%s/%s/%s" %
                                  (DBUS_PATH_ROLES, self._escaped_name,
                                   instance_escaped_name),
                                  persistent=self.persistent)

                # During roled startup (the only time this function should be
                # called), if any role is in a transitional state, it can only
                # mean that roled was terminated while it was still supposed
                # to be doing something.
                # Always set the state to ERROR here if it's in a transitional state,
                # otherwise we won't be able to clean it up.
                if role._settings["state"] in TRANSITIONAL_STATES:
                    role.change_state(state=ERROR,
                                      write=True,
                                      error="roled terminated unexpectedly")

                self._instances[instance_escaped_name] = role

        self.timeout_restart()
Example #28
0
    def do_deploy_async(self, values, sender=None):
        log.debug9("TRACE do_deploy_async(databaseserver)")
        # Do the magic
        #
        # In case of error raise an exception

        first_instance = True

        # Check whether this is the first instance of the database
        for value in self._parent.get_instances().values():
            if ('databaseserver' == value._type and
                        self._name != value._name and
                        self.get_state() in deployed_states):
                first_instance = False
                break

        # First, check for all mandatory arguments
        if 'database' not in values:
            raise RolekitError(INVALID_VALUE, "Database name unset")

        if 'owner' not in values:
            # We'll default to db_owner
            values['owner'] = "db_owner"

        # We will assume the owner is new until adding them fails
        new_owner = True

        # Determine if a password was passed in, so we know whether to
        # suppress it from the settings list later.
        if 'password' in values:
            password_provided = True
        else:
            password_provided = False

        if 'postgresql_conf' not in values:
            values['postgresql_conf'] = self._settings['postgresql_conf']

        if 'pg_hba_conf' not in values:
            values['pg_hba_conf'] = self._settings['pg_hba_conf']

        # Get the UID and GID of the 'postgres' user
        try:
            self.pg_uid = pwd.getpwnam('postgres').pw_uid
        except KeyError:
            raise RolekitError(MISSING_ID, "Could not retrieve UID for postgress user")

        try:
            self.pg_gid = grp.getgrnam('postgres').gr_gid
        except KeyError:
            raise RolekitError(MISSING_ID, "Could not retrieve GID for postgress group")

        if first_instance:
            # Initialize the database on the filesystem
            initdb_args = ["/usr/bin/postgresql-setup", "--initdb"]

            log.debug2("TRACE: Initializing database")
            result = yield async.subprocess_future(initdb_args)
            if result.status:
                # If this fails, it may be just that the filesystem
                # has already been initialized. We'll log the message
                # and continue.
                log.debug1("INITDB: %s" % result.stdout)

        # Now we have to start the service to set everything else up
        # It's safe to start an already-running service, so we'll
        # just always make this call, particularly in case other instances
        # exist but aren't running.
        log.debug2("TRACE: Starting postgresql.service unit")
        try:
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.StartUnit("postgresql.service", "replace")
                job_handler.register_job(job_path)
                log.debug2("TRACE: unit start job registered")


                job_results = yield job_handler.all_jobs_done_future()

                log.debug2("TRACE: unit start job concluded")

                if any([x for x in job_results.values() if x not in ("skipped", "done")]):
                    details = ", ".join(["%s: %s" % item for item in job_results.items()])
                    log.error("Starting services failed: {}".format(details))
                    raise RolekitError(COMMAND_FAILED, "Starting services failed: %s" % details)
        except Exception as e:
            log.error("Error received starting unit: {}".format(e))
            raise


        # Next we create the owner
        log.debug2("TRACE: Creating owner of new database")
        createuser_args = ["/usr/bin/createuser", values['owner']]
        result = yield async.subprocess_future(createuser_args,
                                               uid=self.pg_uid,
                                               gid=self.pg_gid)

        if result.status:
            # If the subprocess returned non-zero, the user probably already exists
            # (such as when we're using db_owner). If the caller was trying to set
            # a password, they probably didn't realize this, so we need to throw
            # an exception.
            log.info1("User {} already exists in the database".format(
                      values['owner']))

            if password_provided:
                raise RolekitError(INVALID_SETTING,
                                   "Cannot set password on pre-existing user")

            # If no password was specified, we'll continue
            new_owner = False


        # If no password was requested, generate a random one here
        if not password_provided:
            values['password'] = generate_password()

        log.debug2("TRACE: Creating new database")
        createdb_args = ["/usr/bin/createdb", values['database'],
                         "-O", values['owner']]
        result = yield async.subprocess_future(createdb_args,
                                               uid=self.pg_uid,
                                               gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, raise an exception
            raise RolekitError(COMMAND_FAILED,
                               "Creating database failed: %d" % result.status)

        # Next, set the password on the owner
        # We'll skip this phase if the the user already existed
        if new_owner:
            log.debug2("TRACE: Setting password for database owner")
            pwd_args = [ROLEKIT_ROLES + "/databaseserver/tools/rk_db_setpwd.py",
                        "--database", values['database'],
                        "--user", values['owner']]
            result = yield async.subprocess_future(pwd_args,
                                                   stdin=values['password'],
                                                   uid=self.pg_uid,
                                                   gid=self.pg_gid)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                log.error("Setting owner password failed: {}".format(result.status))
                raise RolekitError(COMMAND_FAILED,
                                   "Setting owner password failed: %d" %
                                   result.status)

            # If this password was provided by the user, don't save it to
            # the settings for later retrieval. That could be a security
            # issue
            if password_provided:
                values.pop("password", None)
        else: # Not a new owner
            # Never save the password to settings for an existing owner
            log.debug2("TRACE: Owner already exists, not setting password")
            values.pop("password", None)

        if first_instance:
            # Then update the server configuration to accept network
            # connections.
            # edit postgresql.conf to add listen_addresses = '*'
            log.debug2("TRACE: Opening access to external addresses")
            sed_args = [ "/bin/sed",
                         "-e", "s@^[#]listen_addresses\W*=\W*'.*'@listen_addresses = '\*'@",
                         "-i.rksave", values['postgresql_conf'] ]
            result = yield async.subprocess_future(sed_args)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                raise RolekitError(COMMAND_FAILED,
                                   "Changing listen_addresses in '%s' failed: %d" %
                                   (values['postgresql_conf'], result.status))

            # Edit pg_hba.conf to allow 'md5' auth on IPv4 and
            # IPv6 interfaces.
            sed_args = [ "/bin/sed",
                         "-e", "s@^host@#host@",
                         "-e", '/^local/a # Use md5 method for all connections',
                         "-e", '/^local/a host    all             all             all                     md5',
                         "-i.rksave", values['pg_hba_conf'] ]

            result = yield async.subprocess_future(sed_args)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                raise RolekitError(COMMAND_FAILED,
                                   "Changing all connections to use md5 method in '%s' failed: %d" %
                                   (values['pg_hba_conf'], result.status))

            # Restart the postgresql server to accept the new configuration
            log.debug2("TRACE: Restarting postgresql.service unit")
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.RestartUnit("postgresql.service", "replace")
                job_handler.register_job(job_path)

                job_results = yield job_handler.all_jobs_done_future()
                if any([x for x in job_results.values() if x not in ("skipped", "done")]):
                    details = ", ".join(["%s: %s" % item for item in job_results.items()])
                    raise RolekitError(COMMAND_FAILED, "Restarting service failed: %s" % details)

        # Create the systemd target definition
        #
        # We use all of BindsTo, Requires and RequiredBy so we can ensure that
        # all database instances are started and stopped together, since
        # they're really all a single daemon service.
        #
        # The intention here is that starting or stopping any role instance or
        # the main postgresql server will result in the same action happening
        # to all roles. This way, rolekit maintains an accurate view of what
        # instances are running and can communicate that to anyone registered
        # to listen for notifications.

        target = {'Role': 'databaseserver',
                  'Instance': self.get_name(),
                  'Description': "Database Server Role - %s" %
                                 self.get_name(),
                  'BindsTo': ['postgresql.service'],
                  'Requires': ['postgresql.service'],
                  'RequiredBy': ['postgresql.service'],
                  'After': ['syslog.target', 'network.target']}

        log.debug2("TRACE: Database server deployed")

        yield target
Example #29
0
    def do_decommission_async(self, force=False, sender=None):
        # Do the magic
        #
        # In case of error raise an exception

        # Get the UID and GID of the 'postgres' user
        try:
            self.pg_uid = pwd.getpwnam('postgres').pw_uid
        except KeyError:
            raise RolekitError(MISSING_ID, "Could not retrieve UID for postgress user")

        try:
            self.pg_gid = grp.getgrnam('postgres').gr_gid
        except KeyError:
            raise RolekitError(MISSING_ID, "Could not retrieve GID for postgress group")

        # Check whether this is the last instance of the database
        last_instance = True
        for value in self._parent.get_instances().values():
            # Check if there are any other instances of databaseserver
            # We have to exclude our own instance name since it hasn't
            # been removed yet.
            if 'databaseserver' == value._type and self._name != value._name:
                last_instance = False
                break

        # The postgresql service must be running to remove
        # the database and owner
        with SystemdJobHandler() as job_handler:
            job_path = job_handler.manager.StartUnit("postgresql.service", "replace")
            job_handler.register_job(job_path)

            job_results = yield job_handler.all_jobs_done_future()
            if any([x for x in job_results.values() if x not in ("skipped", "done")]):
                details = ", ".join(["%s: %s" % item for item in job_results.items()])
                raise RolekitError(COMMAND_FAILED, "Starting services failed: %s" % details)

        # Drop the database
        dropdb_args = ["/usr/bin/dropdb",
                       "-w", "--if-exists",
                       self._settings['database']]
        result = yield async.subprocess_future(dropdb_args,
                                               uid=self.pg_uid,
                                               gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, raise an exception
            raise RolekitError(COMMAND_FAILED,
                               "Dropping database failed: %d" % result.status)

        # Drop the owner
        dropuser_args = ["/usr/bin/dropuser",
                         "-w", "--if-exists",
                         self._settings['owner']]
        result = yield async.subprocess_future(dropuser_args,
                                               uid=self.pg_uid,
                                               gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, the user may
            # still be there. This is probably due to the owner
            # having privileges on other instances. This is non-fatal.
            log.error("Dropping owner failed: %d" % result.status)

        # If this is the last instance, restore the configuration
        if last_instance:
            try:
                os.rename("%s.rksave" % self._settings['pg_hba_conf'],
                          self._settings['pg_hba_conf'])
                os.rename("%s.rksave" % self._settings['postgresql_conf'],
                          self._settings['postgresql_conf'])
            except:
                log.error("Could not restore pg_hba.conf and/or postgresql.conf. "
                          "Manual intervention required")
                # Not worth stopping here.

            # Since this is the last instance, turn off the postgresql service
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.StopUnit("postgresql.service", "replace")
                job_handler.register_job(job_path)

                job_results = yield job_handler.all_jobs_done_future()
                if any([x for x in job_results.values() if x not in ("skipped", "done")]):
                    details = ", ".join(["%s: %s" % item for item in job_results.items()])
                    raise RolekitError(COMMAND_FAILED, "Stopping services failed: %s" % details)

        # Decommissioning complete
        yield None
Example #30
0
    def do_deploy_async(self, values, sender=None):
        log.debug9("TRACE do_deploy_async(databaseserver)")
        # Do the magic
        #
        # In case of error raise an exception

        first_instance = True

        # Check whether this is the first instance of the database
        for value in self._parent.get_instances().values():
            if (
                "databaseserver" == value.get_type()
                and self.get_name() != value.get_name()
                and self.get_state() in deployed_states
            ):
                first_instance = False
                break

        # If the database name wasn't specified
        if "database" not in values:
            # Use the instance name if it was manually specified
            if self.get_name()[0].isalpha():
                values["database"] = self.get_name()
            else:
                # Either it was autogenerated or begins with a
                # non-alphabetic character; prefix it with db_
                values["database"] = "db_%s" % self.get_name()

        if "owner" not in values:
            # We'll default to db_owner
            values["owner"] = "db_owner"

        # We will assume the owner is new until adding them fails
        new_owner = True

        # Determine if a password was passed in, so we know whether to
        # suppress it from the settings list later.
        if "password" in values:
            password_provided = True
        else:
            password_provided = False

        if "postgresql_conf" not in values:
            values["postgresql_conf"] = self._settings["postgresql_conf"]

        if "pg_hba_conf" not in values:
            values["pg_hba_conf"] = self._settings["pg_hba_conf"]

        # Get the UID and GID of the 'postgres' user
        try:
            self.pg_uid = pwd.getpwnam("postgres").pw_uid
        except KeyError:
            raise RolekitError(MISSING_ID, "Could not retrieve UID for postgres user")

        try:
            self.pg_gid = grp.getgrnam("postgres").gr_gid
        except KeyError:
            raise RolekitError(MISSING_ID, "Could not retrieve GID for postgres group")

        if first_instance:
            # Initialize the database on the filesystem
            initdb_args = ["/usr/bin/postgresql-setup", "--initdb"]

            log.debug2("TRACE: Initializing database")
            result = yield async.subprocess_future(initdb_args)
            if result.status:
                # If this fails, it may be just that the filesystem
                # has already been initialized. We'll log the message
                # and continue.
                log.debug1("INITDB: %s" % result.stdout)

        # Now we have to start the service to set everything else up
        # It's safe to start an already-running service, so we'll
        # just always make this call, particularly in case other instances
        # exist but aren't running.
        log.debug2("TRACE: Starting postgresql.service unit")
        try:
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.StartUnit("postgresql.service", "replace")
                job_handler.register_job(job_path)
                log.debug2("TRACE: unit start job registered")

                job_results = yield job_handler.all_jobs_done_future()

                log.debug2("TRACE: unit start job concluded")

                if any([x for x in job_results.values() if x not in ("skipped", "done")]):
                    details = ", ".join(["%s: %s" % item for item in job_results.items()])
                    log.error("Starting services failed: {}".format(details))
                    raise RolekitError(COMMAND_FAILED, "Starting services failed: %s" % details)
        except Exception as e:
            log.error("Error received starting unit: {}".format(e))
            raise

        # Next we create the owner
        log.debug2("TRACE: Creating owner of new database")
        createuser_args = ["/usr/bin/createuser", values["owner"]]
        result = yield async.subprocess_future(createuser_args, uid=self.pg_uid, gid=self.pg_gid)

        if result.status:
            # If the subprocess returned non-zero, the user probably already exists
            # (such as when we're using db_owner). If the caller was trying to set
            # a password, they probably didn't realize this, so we need to throw
            # an exception.
            log.info1("User {} already exists in the database".format(values["owner"]))

            if password_provided:
                raise RolekitError(INVALID_SETTING, "Cannot set password on pre-existing user")

            # If no password was specified, we'll continue
            new_owner = False

        # If no password was requested, generate a random one here
        if not password_provided:
            values["password"] = generate_password()

        log.debug2("TRACE: Creating new database")
        createdb_args = ["/usr/bin/createdb", values["database"], "-O", values["owner"]]
        result = yield async.subprocess_future(createdb_args, uid=self.pg_uid, gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, raise an exception
            raise RolekitError(COMMAND_FAILED, "Creating database failed: %d" % result.status)

        # Next, set the password on the owner
        # We'll skip this phase if the the user already existed
        if new_owner:
            log.debug2("TRACE: Setting password for database owner")
            pwd_args = [
                ROLEKIT_ROLES + "/databaseserver/tools/rk_db_setpwd.py",
                "--database",
                values["database"],
                "--user",
                values["owner"],
            ]
            result = yield async.subprocess_future(pwd_args, stdin=values["password"], uid=self.pg_uid, gid=self.pg_gid)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                log.error("Setting owner password failed: {}".format(result.status))
                raise RolekitError(COMMAND_FAILED, "Setting owner password failed: %d" % result.status)

            # If this password was provided by the user, don't save it to
            # the settings for later retrieval. That could be a security
            # issue
            if password_provided:
                values.pop("password", None)
        else:  # Not a new owner
            # Never save the password to settings for an existing owner
            log.debug2("TRACE: Owner already exists, not setting password")
            values.pop("password", None)

        if first_instance:
            # Then update the server configuration to accept network
            # connections.
            log.debug2("TRACE: Opening access to external addresses")

            # edit postgresql.conf to add listen_addresses = '*'
            conffile = values["postgresql_conf"]
            bakfile = conffile + ".rksave"

            try:
                linkfile(conffile, bakfile)

                with open(conffile) as f:
                    conflines = f.readlines()

                tweaking_rules = [
                    {
                        "regex": r"^\s*#?\s*listen_addresses\s*=.*",
                        "replace": r"listen_addresses = '*'",
                        "append_if_missing": True,
                    }
                ]

                overwrite_safely(conffile, "".join(_tweak_lines(conflines, tweaking_rules)))
            except Exception as e:
                log.fatal("Couldn't write {!r}: {}".format(conffile, e))
                # At this point, conffile is unmodified, otherwise
                # overwrite_safely() would have succeeded
                try:
                    os.unlink(bakfile)
                except Exception as x:
                    if not (isinstance(x, OSError) and x.errno == errno.ENOENT):
                        log.error("Couldn't remove {!r}: {}".format(bakfile, x))

                raise RolekitError(
                    COMMAND_FAILED, "Opening access to external addresses in '{}'" "failed: {}".format(conffile, e)
                )

            # Edit pg_hba.conf to allow 'md5' auth on IPv4 and
            # IPv6 interfaces.
            conffile = values["pg_hba_conf"]
            bakfile = conffile + ".rksave"

            try:
                linkfile(conffile, bakfile)

                with open(conffile) as f:
                    conflines = f.readlines()

                tweaking_rules = [
                    {"regex": r"^\s*host((?:\s.*)$)", "replace": r"#host\1"},
                    {
                        "regex": r"^\s*local(?:\s.*|)$",
                        "append": "# Use md5 method for all connections\nhost    all             all             all                     md5",
                    },
                ]

                overwrite_safely(conffile, "".join(_tweak_lines(conflines, tweaking_rules)))
            except Exception as e:
                log.fatal("Couldn't write {!r}: {}".format(conffile, e))
                # At this point, conffile is unmodified, otherwise
                # overwrite_safely() would have succeeded
                try:
                    os.unlink(bakfile)
                except Exception as x:
                    if not (isinstance(x, OSError) and x.errno == errno.ENOENT):
                        log.error("Couldn't remove {!r}: {}".format(bakfile, x))

                # Restore previous postgresql.conf from the backup
                conffile = values["postgresql_conf"]
                bakfile = conffile + ".rksave"
                try:
                    os.rename(bakfile, conffile)
                except Exception as x:
                    log.error("Couldn't restore {!r} from backup {!r}: {}".format(conffile, bakfile, x))

                raise RolekitError(
                    COMMAND_FAILED,
                    "Changing all connections to use md5 method in '{}'" "failed: {}".format(values["pg_hba_conf"], e),
                )

            # Restart the postgresql server to accept the new configuration
            log.debug2("TRACE: Restarting postgresql.service unit")
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.RestartUnit("postgresql.service", "replace")
                job_handler.register_job(job_path)

                job_results = yield job_handler.all_jobs_done_future()
                if any([x for x in job_results.values() if x not in ("skipped", "done")]):
                    details = ", ".join(["%s: %s" % item for item in job_results.items()])
                    raise RolekitError(COMMAND_FAILED, "Restarting service failed: %s" % details)

        # Create the systemd target definition
        target = RoleDeploymentValues(self.get_type(), self.get_name(), "Database Server")
        target.add_required_units(["postgresql.service"])

        log.debug2("TRACE: Database server deployed")

        yield target
Example #31
0
def subprocess_future(args, stdin=None, uid=None, gid=None):
    """Start a subprocess and return a future used to wait for it to finish.

    :param args: A sequence of program arguments (see subprocess.Popen())
    :param stdin: A string containing one or more lines of stdin input to
    pass to the child process.
    :param uid: If specified, this must be a numerical UID that the subprocess
    will run under. If it is used, gid must also be specified.
    :param gid: If specified, this must be a numerical UID that the subprocess
    will run under. If it is used, uid must also be specified.
    :return: a future for an object with the members status, stdout and stderr,
    representing waitpid()-like status, stdout output and stderr output,
    respectively.
    """
    log.debug9("subprocess: {0}".format(args))

    def demote(user_uid, user_gid):
        """
        Pass the function 'set_ids' to preexec_fn, rather than just calling
        setuid and setgid. This will change the ids for that subprocess only.
        We have to contstruct a callable that requires no arguments in order
        to pass it to preexec_fn.
        """

        # Look up the username for an initgroups call
        # This is not a perfect solution, as it is
        # possible (though not recommended) that the UID
        # may match more than one username (such as aliases)
        # This approach will use only whichever name the
        # system deems is canonical for this UID.
        username = pwd.getpwuid(user_uid).pw_name

        def set_ids():
            os.setregid(user_gid, user_gid)
            os.initgroups(username, user_gid)
            os.setreuid(user_uid, user_uid)

        return set_ids

    if (uid is None) != (gid is None):
        # If one or the other is specified, but not both,
        # throw an error.
        raise RolekitError(INVALID_SETTING)

    if (uid is not None):
        # The UID and GID are both set
        # Impersonate this UID and GID in the subprocess
        preexec_fn = demote(uid, gid)
    else:
        preexec_fn = None

    try:
        process = subprocess.Popen(args,
                                   close_fds=True,
                                   stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE,
                                   preexec_fn=preexec_fn)
    except OSError as e:
        if e.errno is errno.EPERM:
            # Could not change users prior to executing the subprocess
            log.error("Insufficient privileges to impersonate UID/GID %s/%s" %
                      (uid, gid))
        raise

    # Send the input data if needed.
    if stdin:
        process.stdin.write(stdin.encode('utf-8'))
    process.stdin.close()

    # The three partial results.
    stdout_future = _fd_output_future(process.stdout, log.debug1)
    stderr_future = _fd_output_future(process.stderr, log.error)
    waitpid_future = Future()

    def child_exited(unused_pid, status):
        waitpid_future.set_result(status)
        # GLib has retrieved the process status and freed the PID. Ask the
        # subprocess.Popen object to wait for the process as well; we know this
        # will fail, but it prevents the subprocess module from calling
        # waitpid() on that freed PID in some indeterminate time in the future,
        # where it might take over an unrelated process.  At this point we are
        # technically calling waitpid() on an unallocated PID, which is
        # generally racy, but we don’t have any concurrently running threads
        # creating subprocesses under our hands, so we should be OK.
        process.wait()

    GLib.child_watch_add(GLib.PRIORITY_DEFAULT, process.pid, child_exited)

    # Resolve the returned future when all partial results are resolved.
    future = Future()

    def check_if_done(unused_future):
        if (waitpid_future.done() and stdout_future.done()
                and stderr_future.done()):
            r = _AsyncSubprocessResult(status=waitpid_future.result(),
                                       stdout=stdout_future.result(),
                                       stderr=stderr_future.result())
            future.set_result(r)

    for f in (waitpid_future, stdout_future, stderr_future):
        f.add_done_callback(check_if_done)

    return future
Example #32
0
    def do_deploy_async(self, values, sender=None):
        log.debug9("TRACE do_deploy_async(databaseserver)")
        # Do the magic
        #
        # In case of error raise an exception

        first_instance = True

        # Check whether this is the first instance of the database
        for value in self._parent.get_instances().values():
            if ('databaseserver' == value.get_type()
                    and self.get_name() != value.get_name()
                    and self.get_state() in deployed_states):
                first_instance = False
                break

        # If the database name wasn't specified
        if 'database' not in values:
            # Use the instance name if it was manually specified
            if self.get_name()[0].isalpha():
                values['database'] = self.get_name()
            else:
                # Either it was autogenerated or begins with a
                # non-alphabetic character; prefix it with db_
                values['database'] = "db_%s" % self.get_name()

        if 'owner' not in values:
            # We'll default to db_owner
            values['owner'] = "db_owner"

        # We will assume the owner is new until adding them fails
        new_owner = True

        # Determine if a password was passed in, so we know whether to
        # suppress it from the settings list later.
        if 'password' in values:
            password_provided = True
        else:
            password_provided = False

        if 'postgresql_conf' not in values:
            values['postgresql_conf'] = self._settings['postgresql_conf']

        if 'pg_hba_conf' not in values:
            values['pg_hba_conf'] = self._settings['pg_hba_conf']

        # Get the UID and GID of the 'postgres' user
        try:
            self.pg_uid = pwd.getpwnam('postgres').pw_uid
        except KeyError:
            raise RolekitError(MISSING_ID,
                               "Could not retrieve UID for postgres user")

        try:
            self.pg_gid = grp.getgrnam('postgres').gr_gid
        except KeyError:
            raise RolekitError(MISSING_ID,
                               "Could not retrieve GID for postgres group")

        if first_instance:
            # Initialize the database on the filesystem
            initdb_args = ["/usr/bin/postgresql-setup", "--initdb"]

            log.debug2("TRACE: Initializing database")
            result = yield async .subprocess_future(initdb_args)
            if result.status:
                # If this fails, it may be just that the filesystem
                # has already been initialized. We'll log the message
                # and continue.
                log.debug1("INITDB: %s" % result.stdout)

        # Now we have to start the service to set everything else up
        # It's safe to start an already-running service, so we'll
        # just always make this call, particularly in case other instances
        # exist but aren't running.
        log.debug2("TRACE: Starting postgresql.service unit")
        try:
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.StartUnit(
                    "postgresql.service", "replace")
                job_handler.register_job(job_path)
                log.debug2("TRACE: unit start job registered")

                job_results = yield job_handler.all_jobs_done_future()

                log.debug2("TRACE: unit start job concluded")

                if any([
                        x for x in job_results.values()
                        if x not in ("skipped", "done")
                ]):
                    details = ", ".join(
                        ["%s: %s" % item for item in job_results.items()])
                    log.error("Starting services failed: {}".format(details))
                    raise RolekitError(
                        COMMAND_FAILED,
                        "Starting services failed: %s" % details)
        except Exception as e:
            log.error("Error received starting unit: {}".format(e))
            raise

        # Next we create the owner
        log.debug2("TRACE: Creating owner of new database")
        createuser_args = ["/usr/bin/createuser", values['owner']]
        result = yield async .subprocess_future(createuser_args,
                                                uid=self.pg_uid,
                                                gid=self.pg_gid)

        if result.status:
            # If the subprocess returned non-zero, the user probably already exists
            # (such as when we're using db_owner). If the caller was trying to set
            # a password, they probably didn't realize this, so we need to throw
            # an exception.
            log.info1("User {} already exists in the database".format(
                values['owner']))

            if password_provided:
                raise RolekitError(INVALID_SETTING,
                                   "Cannot set password on pre-existing user")

            # If no password was specified, we'll continue
            new_owner = False

        # If no password was requested, generate a random one here
        if not password_provided:
            values['password'] = generate_password()

        log.debug2("TRACE: Creating new database")
        createdb_args = [
            "/usr/bin/createdb", values['database'], "-O", values['owner']
        ]
        result = yield async .subprocess_future(createdb_args,
                                                uid=self.pg_uid,
                                                gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, raise an exception
            raise RolekitError(COMMAND_FAILED,
                               "Creating database failed: %d" % result.status)

        # Next, set the password on the owner
        # We'll skip this phase if the the user already existed
        if new_owner:
            log.debug2("TRACE: Setting password for database owner")
            pwd_args = [
                ROLEKIT_ROLES + "/databaseserver/tools/rk_db_setpwd.py",
                "--database", values['database'], "--user", values['owner']
            ]
            result = yield async .subprocess_future(pwd_args,
                                                    stdin=values['password'],
                                                    uid=self.pg_uid,
                                                    gid=self.pg_gid)

            if result.status:
                # If the subprocess returned non-zero, raise an exception
                log.error("Setting owner password failed: {}".format(
                    result.status))
                raise RolekitError(
                    COMMAND_FAILED,
                    "Setting owner password failed: %d" % result.status)

            # If this password was provided by the user, don't save it to
            # the settings for later retrieval. That could be a security
            # issue
            if password_provided:
                values.pop("password", None)
        else:  # Not a new owner
            # Never save the password to settings for an existing owner
            log.debug2("TRACE: Owner already exists, not setting password")
            values.pop("password", None)

        if first_instance:
            # Then update the server configuration to accept network
            # connections.
            log.debug2("TRACE: Opening access to external addresses")

            # edit postgresql.conf to add listen_addresses = '*'
            conffile = values['postgresql_conf']
            bakfile = conffile + ".rksave"

            try:
                linkfile(conffile, bakfile)

                with open(conffile) as f:
                    conflines = f.readlines()

                tweaking_rules = [{
                    'regex': r"^\s*#?\s*listen_addresses\s*=.*",
                    'replace': r"listen_addresses = '*'",
                    'append_if_missing': True
                }]

                overwrite_safely(
                    conffile, "".join(_tweak_lines(conflines, tweaking_rules)))
            except Exception as e:
                log.fatal("Couldn't write {!r}: {}".format(conffile, e))
                # At this point, conffile is unmodified, otherwise
                # overwrite_safely() would have succeeded
                try:
                    os.unlink(bakfile)
                except Exception as x:
                    if not (isinstance(x, OSError)
                            and x.errno == errno.ENOENT):
                        log.error("Couldn't remove {!r}: {}".format(
                            bakfile, x))

                raise RolekitError(
                    COMMAND_FAILED,
                    "Opening access to external addresses in '{}'"
                    "failed: {}".format(conffile, e))

            # Edit pg_hba.conf to allow 'md5' auth on IPv4 and
            # IPv6 interfaces.
            conffile = values['pg_hba_conf']
            bakfile = conffile + ".rksave"

            try:
                linkfile(conffile, bakfile)

                with open(conffile) as f:
                    conflines = f.readlines()

                tweaking_rules = [{
                    'regex': r"^\s*host((?:\s.*)$)",
                    'replace': r"#host\1"
                }, {
                    'regex':
                    r"^\s*local(?:\s.*|)$",
                    'append':
                    "# Use md5 method for all connections\nhost    all             all             all                     md5"
                }]

                overwrite_safely(
                    conffile, "".join(_tweak_lines(conflines, tweaking_rules)))
            except Exception as e:
                log.fatal("Couldn't write {!r}: {}".format(conffile, e))
                # At this point, conffile is unmodified, otherwise
                # overwrite_safely() would have succeeded
                try:
                    os.unlink(bakfile)
                except Exception as x:
                    if not (isinstance(x, OSError)
                            and x.errno == errno.ENOENT):
                        log.error("Couldn't remove {!r}: {}".format(
                            bakfile, x))

                # Restore previous postgresql.conf from the backup
                conffile = values['postgresql_conf']
                bakfile = conffile + ".rksave"
                try:
                    os.rename(bakfile, conffile)
                except Exception as x:
                    log.error(
                        "Couldn't restore {!r} from backup {!r}: {}".format(
                            conffile, bakfile, x))

                raise RolekitError(
                    COMMAND_FAILED,
                    "Changing all connections to use md5 method in '{}'"
                    "failed: {}".format(values['pg_hba_conf'], e))

            # Restart the postgresql server to accept the new configuration
            log.debug2("TRACE: Restarting postgresql.service unit")
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.RestartUnit(
                    "postgresql.service", "replace")
                job_handler.register_job(job_path)

                job_results = yield job_handler.all_jobs_done_future()
                if any([
                        x for x in job_results.values()
                        if x not in ("skipped", "done")
                ]):
                    details = ", ".join(
                        ["%s: %s" % item for item in job_results.items()])
                    raise RolekitError(
                        COMMAND_FAILED,
                        "Restarting service failed: %s" % details)

        # Create the systemd target definition
        target = RoleDeploymentValues(self.get_type(), self.get_name(),
                                      "Database Server")
        target.add_required_units(['postgresql.service'])

        log.debug2("TRACE: Database server deployed")

        yield target
Example #33
0
def run_server(debug_gc=False, persistent=False):
    """ Main function for rolekit server. Handles D-Bus and GLib mainloop.
    """
    service = None
    if debug_gc:
        from pprint import pformat
        import gc
        gc.enable()
        gc.set_debug(gc.DEBUG_LEAK)

        gc_timeout = 10

        def gc_collect():
            gc.collect()
            if len(gc.garbage) > 0:
                print("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
                      ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n")
                print("GARBAGE OBJECTS (%d):\n" % len(gc.garbage))
                for x in gc.garbage:
                    print(
                        type(x),
                        "\n  ",
                    )
                    print(pformat(x))
                print("\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
                      "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n")
            id = GLib.timeout_add_seconds(gc_timeout, gc_collect)

    try:
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        bus = dbus.SystemBus()
        name = dbus.service.BusName(DBUS_INTERFACE, bus=bus)
        service = RoleD(name, DBUS_PATH, persistent=persistent)

        mainloop = GLib.MainLoop()
        slip.dbus.service.set_mainloop(mainloop)
        if debug_gc:
            id = GLib.timeout_add_seconds(gc_timeout, gc_collect)

        # use unix_signal_add if available, else unix_signal_add_full
        if hasattr(GLib, 'unix_signal_add'):
            unix_signal_add = GLib.unix_signal_add
        else:
            unix_signal_add = GLib.unix_signal_add_full

        unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGHUP, sighup, None)
        unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, sigterm, mainloop)

        mainloop.run()

    except KeyboardInterrupt as e:
        pass

    except SystemExit as e:
        log.error("Raising SystemExit in run_server")

    except Exception as e:
        log.error("Exception %s: %s", e.__class__.__name__, str(e))

    if service:
        service.stop()
Example #34
0
    def do_decommission_async(self, force=False, sender=None):
        # Do the magic
        #
        # In case of error raise an exception

        # Get the UID and GID of the 'postgres' user
        try:
            self.pg_uid = pwd.getpwnam('postgres').pw_uid
        except KeyError:
            raise RolekitError(MISSING_ID,
                               "Could not retrieve UID for postgres user")

        try:
            self.pg_gid = grp.getgrnam('postgres').gr_gid
        except KeyError:
            raise RolekitError(MISSING_ID,
                               "Could not retrieve GID for postgres group")

        # Check whether this is the last instance of the database
        last_instance = True
        for value in self._parent.get_instances().values():
            # Check if there are any other instances of databaseserver
            # We have to exclude our own instance name since it hasn't
            # been removed yet.
            if 'databaseserver' == value.get_type() and \
               self.get_name() != value.get_name():
                last_instance = False
                break

        # The postgresql service must be running to remove
        # the database and owner
        with SystemdJobHandler() as job_handler:
            job_path = job_handler.manager.StartUnit("postgresql.service",
                                                     "replace")
            job_handler.register_job(job_path)

            job_results = yield job_handler.all_jobs_done_future()
            if any([
                    x for x in job_results.values()
                    if x not in ("skipped", "done")
            ]):
                details = ", ".join(
                    ["%s: %s" % item for item in job_results.items()])
                raise RolekitError(COMMAND_FAILED,
                                   "Starting services failed: %s" % details)

        # Drop the database
        dropdb_args = [
            "/usr/bin/dropdb", "-w", "--if-exists", self._settings['database']
        ]
        result = yield async .subprocess_future(dropdb_args,
                                                uid=self.pg_uid,
                                                gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, raise an exception
            raise RolekitError(COMMAND_FAILED,
                               "Dropping database failed: %d" % result.status)

        # Drop the owner
        dropuser_args = [
            "/usr/bin/dropuser", "-w", "--if-exists", self._settings['owner']
        ]
        result = yield async .subprocess_future(dropuser_args,
                                                uid=self.pg_uid,
                                                gid=self.pg_gid)
        if result.status:
            # If the subprocess returned non-zero, the user may
            # still be there. This is probably due to the owner
            # having privileges on other instances. This is non-fatal.
            log.error("Dropping owner failed: %d" % result.status)

        # If this is the last instance, restore the configuration
        if last_instance:
            try:
                os.rename("%s.rksave" % self._settings['pg_hba_conf'],
                          self._settings['pg_hba_conf'])
                os.rename("%s.rksave" % self._settings['postgresql_conf'],
                          self._settings['postgresql_conf'])
            except:
                log.error(
                    "Could not restore pg_hba.conf and/or postgresql.conf. "
                    "Manual intervention required")
                # Not worth stopping here.

            # Since this is the last instance, turn off the postgresql service
            with SystemdJobHandler() as job_handler:
                job_path = job_handler.manager.StopUnit(
                    "postgresql.service", "replace")
                job_handler.register_job(job_path)

                job_results = yield job_handler.all_jobs_done_future()
                if any([
                        x for x in job_results.values()
                        if x not in ("skipped", "done")
                ]):
                    details = ", ".join(
                        ["%s: %s" % item for item in job_results.items()])
                    raise RolekitError(
                        COMMAND_FAILED,
                        "Stopping services failed: %s" % details)

        # Decommissioning complete
        yield None
Example #35
0
    def installFirewall(self):
        """install firewall"""
        log.debug1("%s.installFirewall()", self._log_prefix)

        # are there any firewall settings to apply?
        if len(self._settings["firewall"]["services"]) + \
           len(self._settings["firewall"]["ports"]) < 1:
            return

        # create firewall client
        fw = FirewallClient()
        log.debug2("TRACE: Firewall client created")

        # Make sure firewalld is running by getting the
        # default zone
        try:
            default_zone = fw.getDefaultZone()
        except DBusException:
            # firewalld is not running
            log.error("Firewalld is not running or rolekit cannot access it")
            raise

        # save changes to the firewall
        try:
            fw_changes = self._settings["firewall-changes"]
        except KeyError:
            fw_changes = { }

        log.debug2("TRACE: Checking for zones: {}".format(self._settings))

        try:
            zones = self._settings["firewall_zones"]
        except KeyError:
            zones = []

        # if firewall_zones setting is empty, use default zone
        if len(zones) < 1:
            zones = [ default_zone ]

        log.debug2("TRACE: default zone {}".format(zones[0]))

        for zone in zones:
            log.debug2("TRACE: Processing zone {0}".format(zone))
            # get permanent zone settings, run-time settings do not need a
            # special treatment
            z_perm = fw.config().getZoneByName(zone).getSettings()

            for service in self._settings["firewall"]["services"]:
                try:
                    fw.addService(zone, service, 0)
                except Exception as e:
                    if not "ALREADY_ENABLED" in str(e):
                        raise
                else:
                    fw_changes.setdefault(zone, {}).setdefault("services", {}).setdefault(service, []).append("runtime")

                if not z_perm.queryService(service):
                    z_perm.addService(service)
                    fw_changes.setdefault(zone, {}).setdefault("services", {}).setdefault(service, []).append("permanent")

            for port_proto in self._settings["firewall"]["ports"]:
                port, proto = port_proto.split("/")

                try:
                    fw.addPort(zone, port, proto, 0)
                except Exception as e:
                    if not "ALREADY_ENABLED" in str(e):
                        raise
                else:
                    fw_changes.setdefault(zone, {}).setdefault("ports", {}).setdefault(port_proto, []).append("runtime")

                if not z_perm.queryPort(port, proto):
                    z_perm.addPort(port, proto)
                    fw_changes.setdefault(zone, {}).setdefault("ports", {}).setdefault(port_proto, []).append("permanent")

            fw.config().getZoneByName(zone).update(z_perm)

        self._settings["firewall-changes"] = fw_changes
        self._settings.write()
Example #36
0
    def __init__(self, role, name, directory, *args, **kwargs):
        """The DBUS_INTERFACE_ROLE implementation

        :param role: RoleBase descendant
        :param name: Role name
        :param directory: FIXME: unused???
        :param path: (Implicit in *args) FIXME: unused???
        """
        super(DBusRole, self).__init__(*args, **kwargs)
        self.busname = args[0]
        self.path = args[1]
        self._role = role
        self._name = name
        self._escaped_name = dbus_label_escape(name)
        self._log_prefix = "role.%s" % self._escaped_name
        self._directory = directory
        self._instances = { }

        # create instances for stored instance settings

        path = "%s/%s" % (ETC_ROLEKIT_ROLES, self.get_name())
        if os.path.exists(path) and os.path.isdir(path):
            for name in sorted(os.listdir(path)):
                if not name.endswith(".json"):
                    continue
                instance = name[:-5]
                log.debug1("Loading '%s' instance '%s'", self.get_name(),
                           instance)

                settings = RoleSettings(self.get_name(), instance)
                try:
                    settings.read()
                except ValueError as e:
                    log.error("Failed to load '%s' instance '%s': %s",
                              self.get_name(), instance, e)
                    continue

                instance_escaped_name = dbus_label_escape(instance)
                if instance_escaped_name in self._instances:
                    raise RolekitError(NAME_CONFLICT, instance_escaped_name)

                role = self._role(self, instance, self.get_name(),
                                  self._directory, settings, self.busname,
                                  "%s/%s/%s" % (DBUS_PATH_ROLES,
                                                self._escaped_name,
                                                instance_escaped_name),
                                  persistent=self.persistent)

                # During roled startup (the only time this function should be
                # called), if any role is in a transitional state, it can only
                # mean that roled was terminated while it was still supposed
                # to be doing something.
                # Always set the state to ERROR here if it's in a transitional state,
                # otherwise we won't be able to clean it up.
                if role._settings["state"] in TRANSITIONAL_STATES:
                    role.change_state(state=ERROR,
                                      write=True,
                                      error="roled terminated unexpectedly")

                self._instances[instance_escaped_name] = role

        self.timeout_restart()
Example #37
0
def subprocess_future(args, stdin=None, uid=None, gid=None):
    """Start a subprocess and return a future used to wait for it to finish.

    :param args: A sequence of program arguments (see subprocess.Popen())
    :param stdin: A string containing one or more lines of stdin input to
    pass to the child process.
    :param uid: If specified, this must be a numerical UID that the subprocess
    will run under. If it is used, gid must also be specified.
    :param gid: If specified, this must be a numerical UID that the subprocess
    will run under. If it is used, uid must also be specified.
    :return: a future for an object with the members status, stdout and stderr,
    representing waitpid()-like status, stdout output and stderr output,
    respectively.
    """
    log.debug9("subprocess: {0}".format(args))

    def demote(user_uid, user_gid):
        """
        Pass the function 'set_ids' to preexec_fn, rather than just calling
        setuid and setgid. This will change the ids for that subprocess only.
        We have to contstruct a callable that requires no arguments in order
        to pass it to preexec_fn.
        """

        # Look up the username for an initgroups call
        # This is not a perfect solution, as it is
        # possible (though not recommended) that the UID
        # may match more than one username (such as aliases)
        # This approach will use only whichever name the
        # system deems is canonical for this UID.
        username = pwd.getpwuid(user_uid).pw_name

        def set_ids():
            os.setregid(user_gid, user_gid)
            os.initgroups(username, user_gid)
            os.setreuid(user_uid, user_uid)
        return set_ids

    if (uid is None) != (gid is None):
        # If one or the other is specified, but not both,
        # throw an error.
        raise RolekitError(INVALID_SETTING)

    if (uid is not None):
        # The UID and GID are both set
        # Impersonate this UID and GID in the subprocess
        preexec_fn = demote(uid, gid)
    else:
        preexec_fn = None

    try:
        process = subprocess.Popen(args, close_fds=True,
                                   stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE,
                                   preexec_fn=preexec_fn)
    except OSError as e:
        if e.errno is errno.EPERM:
            # Could not change users prior to executing the subprocess
            log.error("Insufficient privileges to impersonate UID/GID %s/%s" %
                      (uid, gid))
        raise

    # Send the input data if needed.
    if stdin:
        process.stdin.write(stdin.encode('utf-8'))
    process.stdin.close()

    # The three partial results.
    stdout_future = _fd_output_future(process.stdout, log.debug1)
    stderr_future = _fd_output_future(process.stderr, log.error)
    waitpid_future = Future()

    def child_exited(unused_pid, status):
        waitpid_future.set_result(status)
        # GLib has retrieved the process status and freed the PID. Ask the
        # subprocess.Popen object to wait for the process as well; we know this
        # will fail, but it prevents the subprocess module from calling
        # waitpid() on that freed PID in some indeterminate time in the future,
        # where it might take over an unrelated process.  At this point we are
        # technically calling waitpid() on an unallocated PID, which is
        # generally racy, but we don’t have any concurrently running threads
        # creating subprocesses under our hands, so we should be OK.
        process.wait()
    GLib.child_watch_add(GLib.PRIORITY_DEFAULT, process.pid, child_exited)

    # Resolve the returned future when all partial results are resolved.
    future = Future()
    def check_if_done(unused_future):
        if (waitpid_future.done() and stdout_future.done() and
            stderr_future.done()):
            r = _AsyncSubprocessResult(status=waitpid_future.result(),
                                       stdout=stdout_future.result(),
                                       stderr=stderr_future.result())
            future.set_result(r)
    for f in (waitpid_future, stdout_future, stderr_future):
        f.add_done_callback(check_if_done)

    return future