def init_config_states():
    import yaml
    from charmhelpers.core import hookenv
    from charms.reactive import set_state
    from charms.reactive import toggle_state

    config = hookenv.config()

    config_defaults = {}
    config_defs = {}
    config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
    if os.path.exists(config_yaml):
        with open(config_yaml) as fp:
            config_defs = yaml.safe_load(fp).get('options', {})
            config_defaults = {
                key: value.get('default')
                for key, value in config_defs.items()
            }
    for opt in config_defs.keys():
        if config.changed(opt):
            set_state('config.changed')
            set_state('config.changed.{}'.format(opt))
        toggle_state('config.set.{}'.format(opt), config.get(opt))
        toggle_state('config.default.{}'.format(opt),
                     config.get(opt) == config_defaults[opt])
    hookenv.atexit(clear_config_states)
Example #2
0
def main():
    # Modify the behavior of the PostgreSQL package installation
    # before any packages are installed. We do this here, rather than
    # in handlers, so that extra_packages declared by the operator
    # don't drag in the PostgreSQL packages as dependencies before
    # the environment tweaks have been made.
    if (not reactive.is_state('apt.installed.postgresql-common') and
            not reactive.is_state('postgresql.cluster.inhibited')):
        generate_locale()
        inhibit_default_cluster_creation()
        install_postgresql_packages()
        install_extra_packages()  # Deprecated extra-packages option

    # Don't trust this state from the last hook. Daemons may have
    # crashed and servers rebooted since then.
    if reactive.is_state('postgresql.cluster.created'):
        try:
            reactive.toggle_state('postgresql.cluster.is_running',
                                  postgresql.is_running())
        except subprocess.CalledProcessError as x:
            if not reactive.is_state('workloadstatus.blocked'):
                status_set('blocked',
                           'Local PostgreSQL cluster is corrupt: {}'
                           ''.format(x.stderr))

    # Reconfigure PostgreSQL. While we don't strictly speaking need
    # to do this every hook, we do need to do this almost every hook,
    # since even things like the number of peers or number of clients
    # can affect minimum viable configuration settings.
    reactive.remove_state('postgresql.cluster.configured')

    log_states()  # Debug noise.
Example #3
0
def init_config_states():
    from charmhelpers.core import hookenv
    from charms.reactive import set_state
    from charms.reactive import toggle_state
    config = hookenv.config()
    for opt in config.keys():
        if config.changed(opt):
            set_state('config.changed')
            set_state('config.changed.{}'.format(opt))
        toggle_state('config.set.{}'.format(opt), config[opt])
    hookenv.atexit(clear_config_states)
Example #4
0
 def test_toggle_state(self):
     reactive.toggle_state('foo', True)
     reactive.toggle_state('foo', True)
     reactive.toggle_state('bar', False)
     reactive.toggle_state('bar', False)
     assert reactive.is_state('foo')
     assert not reactive.is_state('bar')
Example #5
0
 def test_toggle_state(self):
     reactive.toggle_state('foo', True)
     reactive.toggle_state('foo', True)
     reactive.toggle_state('bar', False)
     reactive.toggle_state('bar', False)
     assert reactive.is_state('foo')
     assert not reactive.is_state('bar')
Example #6
0
def main():
    if not (reactive.is_state("postgresql.cluster.created") or reactive.is_state("postgresql.cluster.initial-check")):
        # We need to check for existance of an existing database,
        # before the main PostgreSQL package has been installed.
        # If there is one, abort rather than risk destroying data.
        # We need to do this here, as the apt layer may pull in
        # the main PostgreSQL package through dependencies, per
        # lp:1749284
        if os.path.exists(postgresql.postgresql_conf_path()):
            hookenv.status_set(
                "blocked",
                "PostgreSQL config from previous install found at {}".format(postgresql.postgresql_conf_path()),
            )
        elif os.path.exists(postgresql.data_dir()):
            hookenv.status_set(
                "blocked",
                "PostgreSQL database from previous install found at {}".format(postgresql.postgresql.data_dir()),
            )
        else:
            hookenv.log("No pre-existing PostgreSQL database found")
            reactive.set_state("postgresql.cluster.initial-check")

    # Don't trust this state from the last hook. Daemons may have
    # crashed and servers rebooted since then.
    if reactive.is_state("postgresql.cluster.created"):
        try:
            reactive.toggle_state("postgresql.cluster.is_running", postgresql.is_running())
        except subprocess.CalledProcessError as x:
            if not reactive.is_state("workloadstatus.blocked"):
                status_set(
                    "blocked",
                    "Local PostgreSQL cluster is corrupt: {}".format(x.stderr),
                )

    # Reconfigure PostgreSQL. While we don't strictly speaking need
    # to do this every hook, we do need to do this almost every hook,
    # since even things like the number of peers or number of clients
    # can affect minimum viable configuration settings.
    reactive.remove_state("postgresql.cluster.configured")

    log_states()  # Debug noise.
Example #7
0
def init_config_states():
    import yaml
    from charmhelpers.core import hookenv
    from charms.reactive import set_state
    from charms.reactive import toggle_state
    config = hookenv.config()
    config_defaults = {}
    config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
    if os.path.exists(config_yaml):
        with open(config_yaml) as fp:
            config_defs = yaml.load(fp).get('options', {})
            config_defaults = {key: value.get('default')
                               for key, value in config_defs.items()}
    for opt in config.keys():
        if config.changed(opt):
            set_state('config.changed')
            set_state('config.changed.{}'.format(opt))
        toggle_state('config.set.{}'.format(opt), config[opt])
        toggle_state('config.default.{}'.format(opt),
                     config[opt] == config_defaults[opt])
    hookenv.atexit(clear_config_states)
def check_journalnode_quorum(datanode):
    nodes = datanode.nodes()
    quorum_size = hookenv.config('journalnode_quorum_size')
    toggle_state('journalnode.quorum', len(nodes) >= quorum_size)
Example #9
0
def wal_e_restore():
    reactive.remove_state("action.wal-e-restore")
    params = hookenv.action_get()
    backup = params["backup-name"].strip().replace("-", "_")
    storage_uri = params["storage-uri"].strip()

    ship_uri = hookenv.config().get("wal_e_storage_uri")
    if storage_uri == ship_uri:
        hookenv.action_fail(
            "The storage-uri parameter is identical to "
            "the wal_e_storage_uri config setting. Your "
            "restoration source cannot be the same as the "
            "folder you are archiving too to avoid corrupting "
            "the backups."
        )
        return

    if not params["confirm"]:
        m = "Recovery from {}.".format(storage_uri)
        if ship_uri:
            m += "\nContents of {} will be destroyed.".format(ship_uri)
        m += "\nExisting local database will be destroyed."
        m += "\nRerun action with 'confirm=true' to proceed."
        hookenv.action_set({"info": m})
        return

    with tempfile.TemporaryDirectory(prefix="wal-e", suffix="envdir") as envdir:
        update_wal_e_env_dir(envdir, storage_uri)

        # Confirm there is a backup to restore
        backups = wal_e_list_backups(envdir)
        if not backups:
            hookenv.action_fail("No backups found at {}".format(storage_uri))
            return
        if backup != "LATEST" and backup not in (b["name"] for b in backups):
            hookenv.action_fail("Backup {} not found".format(backup))
            return

        # Shutdown PostgreSQL. Note we want this action to run synchronously,
        # so there is no opportunity to ask permission from the leader. If
        # there are other units cloning this database, those clone operations
        # will fail. Which seems preferable to blocking a recovery operation
        # in any case, because if we are doing disaster recovery we generally
        # want to do it right now.
        status_set("maintenance", "Stopping PostgreSQL for backup restoration")
        postgresql.stop()

        # Trash the existing database. Its dangerous to do this first, but
        # we probably need the space.
        data_dir = postgresql.data_dir()  # May be a symlink
        for content in os.listdir(data_dir):
            cpath = os.path.join(data_dir, content)
            if os.path.isdir(cpath) and not os.path.islink(cpath):
                shutil.rmtree(cpath)
            else:
                os.remove(cpath)

        # WAL-E recover
        status_set("maintenance", "Restoring backup {}".format(backup))
        wal_e_run(["backup-fetch", data_dir, backup], envdir=envdir)

        # Create recovery.conf to complete recovery
        is_master = reactive.is_state("postgresql.replication.is_master")
        standby_mode = "off" if is_master else "on"
        if params.get("target-time"):
            target_time = "recovery_target_time='{}'" "".format(params["target-time"])
        else:
            target_time = ""
        target_action = "promote" if is_master else "shutdown"
        immediate = "" if is_master else "recovery_target='immediate'"
        helpers.write(
            postgresql.recovery_conf_path(),
            dedent(
                """\
                             # Managed by Juju. PITR in progress.
                             standby_mode = {}
                             restore_command='{}'
                             recovery_target_timeline = {}
                             recovery_target_action = {}
                             {}
                             {}
                             """
            ).format(
                standby_mode,
                wal_e_restore_command(envdir=envdir),
                params["target-timeline"],
                target_action,
                target_time,
                immediate,
            ),
            mode=0o600,
            user="******",
            group="postgres",
        )

        # Avoid circular import. We could also avoid the import entirely
        # with a sufficiently complex set of handlers in the replication
        # module, but that seems to be a worse solution. Better to break
        # out this action into a separate module.
        from reactive.postgresql import replication

        if is_master:
            if ship_uri:
                # If master, trash the configured wal-e storage. This may
                # contain WAL and backups from the old cluster which will
                # conflict with the new cluster. Hopefully it does not
                # contain anything important, because we have no way to
                # prompt the user for confirmation.
                wal_e_run(["delete", "--confirm", "everything"])

            # Then, wait for recovery and promotion.
            postgresql.start()
            con = postgresql.connect()
            cur = con.cursor()
            while True:
                if postgresql.has_version("10"):
                    cur.execute(
                        """SELECT pg_is_in_recovery(),
                                          pg_last_wal_replay_lsn()"""
                    )
                else:
                    cur.execute(
                        """SELECT pg_is_in_recovery(),
                                          pg_last_xlog_replay_location()"""
                    )
                in_rec, loc = cur.fetchone()
                if not in_rec:
                    break
                status_set("maintenance", "Recovery at {}".format(loc))
                time.sleep(10)
        else:
            # If standby, startup and wait for recovery to complete and
            # shutdown.
            status_set("maintenance", "Recovery")
            # Startup might shutdown immediately and look like a failure.
            postgresql.start(ignore_failure=True)
            # No recovery point status yet for standbys, as we would need
            # to handle connection failures when the DB shuts down. We
            # should do this.
            while postgresql.is_running():
                time.sleep(5)
            replication.update_recovery_conf(follow=replication.get_master())

    # Reactive handlers will deal with the rest of the cleanup.
    # eg. ensuring required users and roles exist
    replication.update_replication_states()
    reactive.remove_state("postgresql.cluster.configured")
    reactive.toggle_state("postgresql.cluster.is_running", postgresql.is_running())
    reactive.remove_state("postgresql.nagios.user_ensured")
    reactive.remove_state("postgresql.replication.replication_user_created")
    reactive.remove_state("postgresql.client.published")
Example #10
0
def update_replication_states():
    """
    Set the following states appropriately:

        postgresql.replication.has_peers

            This unit has peers.

        postgresql.replication.had_peers

            This unit once had peers, but may not any more. The peer
            relation exists.

        postgresql.replication.master.peered

            This unit is peered with the master. It is not the master.

        postgresql.replication.master.authorized

            This unit is peered with and authorized by the master. It is
            not the master.

        postgresql.replication.is_master

            This unit is the master.

        postgresql.replication.has_master

            This unit is the master, or it is peered with and
            authorized by the master.

        postgresql.replication.cloned

            This unit is on the master's timeline. It has been cloned from
            the master, or is the master. Undefined with manual replication.

        postgresql.replication.manual

            Manual replication mode has been selected and the charm
            must not do any replication setup or maintenance.

        postgresql.replication.is_primary

            The unit is writable. It is either the master or manual
            replication mode is in effect.

        postgresql.replication.switchover

            In switchover to a new master. A switchover is a controlled
            failover, where the existing master is available.

        postgresql.replication.is_anointed

            In switchover and this unit is anointed to be the new master.
    """
    peers = helpers.get_peer_relation()
    reactive.toggle_state("postgresql.replication.has_peers", peers)
    if peers:
        reactive.set_state("postgresql.replication.had_peers")

    reactive.toggle_state("postgresql.replication.manual",
                          hookenv.config()["manual_replication"])

    master = get_master()  # None if postgresql.replication.manual state.
    reactive.toggle_state("postgresql.replication.is_master",
                          master == hookenv.local_unit())
    reactive.toggle_state("postgresql.replication.master.peered", peers
                          and master in peers)
    reactive.toggle_state(
        "postgresql.replication.master.authorized",
        peers and master in peers and authorized_by(master),
    )
    ready = reactive.is_state(
        "postgresql.replication.is_master") or reactive.is_state(
            "postgresql.replication.master.authorized")
    reactive.toggle_state("postgresql.replication.has_master", ready)

    anointed = get_anointed()
    reactive.toggle_state("postgresql.replication.switchover",
                          anointed is not None and anointed != master)
    reactive.toggle_state(
        "postgresql.replication.is_anointed",
        anointed is not None and anointed != master
        and anointed == hookenv.local_unit(),
    )

    reactive.toggle_state("postgresql.replication.is_primary",
                          postgresql.is_primary())

    if reactive.is_state("postgresql.replication.is_primary"):
        if reactive.is_state("postgresql.replication.is_master"):
            # If the unit is a primary and the master, it is on the master
            # timeline by definition and gets the 'cloned' state.
            reactive.set_state("postgresql.replication.cloned")
        elif reactive.is_state("postgresql.replication.is_anointed"):
            # The anointed unit retains its 'cloned' state.
            pass
        else:
            # If the unit is a primary and not the master, it is on a
            # divered timeline and needs to lose the 'cloned' state.
            reactive.remove_state("postgresql.replication.cloned")

    cloned = reactive.is_state("postgresql.replication.cloned")
    reactive.toggle_state(
        "postgresql.replication.failover",
        master != hookenv.local_unit() and peers and cloned
        and (master not in peers),
    )