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")
Exemple #2
0
 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)
Exemple #4
0
 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)
Exemple #5
0
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()
Exemple #6
0
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")
Exemple #7
0
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")
Exemple #8
0
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()
Exemple #9
0
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')