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)
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.
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)
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')
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.
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)
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")
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), )