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)
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)
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()
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
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
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()
def remove(self): try: self.backup() except Exception as msg: log.error(msg) try: os.remove(self.filepath) except OSError: pass
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()
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)
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()
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()
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
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 )
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))
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)
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
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)
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)
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
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
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
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()
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
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
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
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
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
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()
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
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()
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()