def test_username(self): # Calculate the client username for the given service or unit # to use. self.assertEqual(postgresql.username("hello", False, False), "juju_hello") self.assertEqual(postgresql.username("hello/0", False, False), "juju_hello") self.assertEqual(postgresql.username("hello", superuser=True, replication=False), "jujuadmin_hello") self.assertEqual(postgresql.username("hello/2", True, False), "jujuadmin_hello") self.assertEqual(postgresql.username("hello", False, True), "jujurepl_hello")
def test_username(self): # Calculate the client username for the given service or unit # to use. self.assertEqual(postgresql.username('hello', False, False), 'juju_hello') self.assertEqual(postgresql.username('hello/0', False, False), 'juju_hello') self.assertEqual( postgresql.username('hello', superuser=True, replication=False), 'jujuadmin_hello') self.assertEqual(postgresql.username('hello/2', True, False), 'jujuadmin_hello') self.assertEqual(postgresql.username('hello', False, True), 'jujurepl_hello')
def test_username_truncation(self): # Usernames need to be truncated to 63 characters, while remaining # unique. service = "X" * 70 too_long = "juju_{}".format(service) truncated = too_long[:31] + "d83abbe4d9ddcab942fe8fe92d387470" self.assertEqual(len(truncated), 63) self.assertEqual(postgresql.username(service, False, False), truncated)
def test_username_truncation(self): # Usernames need to be truncated to 63 characters, while remaining # unique. service = 'X' * 70 too_long = 'juju_{}'.format(service) truncated = too_long[:31] + 'd83abbe4d9ddcab942fe8fe92d387470' self.assertEqual(len(truncated), 63) self.assertEqual(postgresql.username(service, False, False), truncated)
def db_relation_master(rel): """The master generates credentials and negotiates resources.""" master = rel.local org_master = dict(master) # Pick one remote unit as representative. They should all converge. for remote in rel.values(): break # The requested database name, the existing database name, or use # the remote service name as a default. We no longer use the # relation id for the database name or usernames, as when a # database dump is restored into a new Juju environment we # are more likely to have matching service names than relation ids # and less likely to have to perform manual permission and ownership # cleanups. if "database" in remote: master["database"] = remote["database"] elif "database" not in master: master["database"] = remote.service superuser, replication = _credential_types(rel) if "user" not in master: user = postgresql.username(remote.service, superuser=superuser, replication=replication) password = get_client_password(user) if not password: hookenv.log("** Master waiting for {} password".format(user)) return master["user"] = user master["password"] = password # schema_user has never been documented and is deprecated. if not superuser: master["schema_user"] = user master["schema_password"] = password hookenv.log("** Master providing {} ({}/{})".format(rel, master["database"], master["user"])) # Reflect these settings back so the client knows when they have # taken effect. if not replication: master["roles"] = remote.get("roles") master["extensions"] = remote.get("extensions") # If things have changed, ping peers so they can remirror. if dict(master) != org_master: ping_standbys()
def set_client_passwords(): """The leader chooses passwords for client connections. Storing the passwords in the leadership settings is the most reliable way of distributing them to peers. """ raw = leadership.leader_get("client_passwords") pwds = json.loads(raw) if raw else {} rels = context.Relations() updated = False for relname in CLIENT_RELNAMES: for rel in rels[relname].values(): superuser, replication = _credential_types(rel) for remote in rel.values(): user = postgresql.username(remote.service, superuser=superuser, replication=replication) if user not in pwds: password = host.pwgen() pwds[user] = password updated = True if updated: leadership.leader_set(client_passwords=json.dumps(pwds, sort_keys=True)) reactive.set_state("postgresql.client.passwords_set")
def db_relation_master(rel): """The master generates credentials and negotiates resources.""" master = rel.local # Pick one remote unit as representative. They should all converge. for remote in rel.values(): break # The requested database name, the existing database name, or use # the remote service name as a default. We no longer use the # relation id for the database name or usernames, as when a # database dump is restored into a new Juju environment we # are more likely to have matching service names than relation ids # and less likely to have to perform manual permission and ownership # cleanups. if "database" in remote: master["database"] = remote["database"] elif "database" not in master: master["database"] = remote.service superuser, replication = _credential_types(rel) if "user" not in master: user = postgresql.username(remote.service, superuser=superuser, replication=replication) password = host.pwgen() master["user"] = user master["password"] = password # schema_user has never been documented and is deprecated. if not superuser: master["schema_user"] = user master["schema_password"] = password hookenv.log("** Master providing {} ({}/{})".format(rel, master["database"], master["user"])) # Reflect these settings back so the client knows when they have # taken effect. if not replication: master["roles"] = remote.get("roles") master["extensions"] = remote.get("extensions")
def upgrade_charm(): workloadstatus.status_set("maintenance", "Upgrading charm") rels = context.Relations() # The master is now appointed by the leader. if hookenv.is_leader(): master = replication.get_master() if not master: master = hookenv.local_unit() peer_rel = helpers.get_peer_relation() if peer_rel: for peer_relinfo in peer_rel.values(): if peer_relinfo.get("state") == "master": master = peer_relinfo.unit break hookenv.log("Discovered {} is the master".format(master)) leadership.leader_set(master=master) # The name of this crontab has changed. It will get regenerated. if os.path.exists("/etc/cron.d/postgresql"): hookenv.log("Removing old crontab") os.unlink("/etc/cron.d/postgresql") # Older generated usernames where generated from the relation id, # and really old ones contained random components. This made it # problematic to restore a database into a fresh environment, # because the new usernames would not match the old usernames and # done of the database permissions would match. We now generate # usernames using just the client service name, so restoring a # database into a fresh environment will work provided the service # names match. We want to update the old usernames in upgraded # services to the new format to improve their disaster recovery # story. for relname, superuser in [("db", False), ("db-admin", True)]: for client_rel in rels[relname].values(): hookenv.log("Migrating database users for {}".format(client_rel)) password = client_rel.local.get("password", host.pwgen()) old_username = client_rel.local.get("user") new_username = postgresql.username(client_rel.service, superuser, False) if old_username and old_username != new_username: migrate_user(old_username, new_username, password, superuser) client_rel.local["user"] = new_username client_rel.local["password"] = password old_username = client_rel.local.get("schema_user") if old_username and old_username != new_username: migrate_user(old_username, new_username, password, superuser) client_rel.local["schema_user"] = new_username client_rel.local["schema_password"] = password # Admin relations used to get 'all' published as the database name, # which was bogus. for client_rel in rels["db-admin"].values(): if client_rel.local.get("database") == "all": client_rel.local["database"] = client_rel.service # Reconfigure PostgreSQL and republish client relations. reactive.remove_state("postgresql.cluster.configured") reactive.remove_state("postgresql.client.published") # Don't recreate the cluster. reactive.set_state("postgresql.cluster.created") # Set the postgresql.replication.cloned flag, so we don't rebuild # standbys when upgrading the charm from a pre-reactive version. reactive.set_state("postgresql.replication.cloned") # Publish which node we are following peer_rel = helpers.get_peer_relation() if peer_rel and "following" not in peer_rel.local: following = unitdata.kv().get("postgresql.replication.following") if following is None and not replication.is_master(): following = replication.get_master() peer_rel.local["following"] = following # Ensure storage that was attached but ignored is no longer ignored. if not reactive.is_state("postgresql.storage.pgdata.attached"): if hookenv.storage_list("pgdata"): storage.attach() # Ensure client usernames and passwords match leader settings. for relname in ("db", "db-admin"): for rel in rels[relname].values(): del rel.local["user"] del rel.local["password"] # Ensure the configure version is cached. postgresql.version() # Skip checks for pre-existing databases, as that has already happened. reactive.set_state("postgresql.cluster.initial-check") # Reinstall support scripts reactive.remove_state("postgresql.cluster.support-scripts") # Ensure that systemd is managing the PostgreSQL process if host.init_is_systemd( ) and not reactive.is_flag_set("postgresql.upgrade.systemd"): reactive.set_flag("postgresql.upgrade.systemd") if reactive.is_flag_set("postgresql.cluster.is_running"): hookenv.log("Restarting PostgreSQL under systemd", hookenv.WARNING) reactive.clear_flag("postgresql.cluster.is_running") postgresql.stop_pgctlcluster() # Update the PGDG source, in case the signing key has changed. config = hookenv.config() if config["pgdg"]: service.add_pgdg_source()
def upgrade_charm(): workloadstatus.status_set('maintenance', 'Upgrading charm') rels = context.Relations() # The master is now appointed by the leader. if hookenv.is_leader(): master = replication.get_master() if not master: master = hookenv.local_unit() if rels.peer: for peer_relinfo in rels.peer.values(): if peer_relinfo.get('state') == 'master': master = peer_relinfo.unit break hookenv.log('Discovered {} is the master'.format(master)) leadership.leader_set(master=master) # The name of this crontab has changed. It will get regenerated. if os.path.exists('/etc/cron.d/postgresql'): hookenv.log('Removing old crontab') os.unlink('/etc/cron.d/postgresql') # Older generated usernames where generated from the relation id, # and really old ones contained random components. This made it # problematic to restore a database into a fresh environment, # because the new usernames would not match the old usernames and # done of the database permissions would match. We now generate # usernames using just the client service name, so restoring a # database into a fresh environment will work provided the service # names match. We want to update the old usernames in upgraded # services to the new format to improve their disaster recovery # story. for relname, superuser in [('db', False), ('db-admin', True)]: for client_rel in rels[relname].values(): hookenv.log('Migrating database users for {}'.format(client_rel)) password = client_rel.local.get('password', host.pwgen()) old_username = client_rel.local.get('user') new_username = postgresql.username(client_rel.service, superuser, False) if old_username and old_username != new_username: migrate_user(old_username, new_username, password, superuser) client_rel.local['user'] = new_username client_rel.local['password'] = password old_username = client_rel.local.get('schema_user') if old_username and old_username != new_username: migrate_user(old_username, new_username, password, superuser) client_rel.local['schema_user'] = new_username client_rel.local['schema_password'] = password # Admin relations used to get 'all' published as the database name, # which was bogus. for client_rel in rels['db-admin'].values(): if client_rel.local.get('database') == 'all': client_rel.local['database'] = client_rel.service # Reconfigure PostgreSQL and republish client relations. reactive.remove_state('postgresql.cluster.configured') reactive.remove_state('postgresql.client.published') # Don't recreate the cluster. reactive.set_state('postgresql.cluster.created') # Set the postgresql.replication.cloned flag, so we don't rebuild # standbys when upgrading the charm from a pre-reactive version. reactive.set_state('postgresql.replication.cloned')