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 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 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 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 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_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_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