[Postgresql] Create replica
Change-Id: Ia00032074dc44a6fbfc1e2d5ab16d1734a1a732c
This commit is contained in:
parent
8761f327fe
commit
5482c54645
@ -10,6 +10,7 @@ RUN export DEBIAN_FRONTEND="noninteractive" \
|
|||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install $APTOPTS gnupg2 lsb-release apt-utils apt-transport-https ca-certificates software-properties-common curl \
|
&& apt-get install $APTOPTS gnupg2 lsb-release apt-utils apt-transport-https ca-certificates software-properties-common curl \
|
||||||
|
&& apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall netbase \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ RUN ./install.sh $DATASTORE ${PERCONA_XTRABACKUP_VERSION}
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install $APTOPTS build-essential python3-setuptools python3-all python3-all-dev python3-pip libffi-dev libssl-dev libxml2-dev libxslt1-dev libyaml-dev \
|
&& apt-get install $APTOPTS build-essential python3-setuptools python3-all python3-all-dev python3-pip libffi-dev libssl-dev libxml2-dev libxslt1-dev libyaml-dev \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& pip3 --no-cache-dir install -U -r requirements.txt \
|
&& pip3 --no-cache-dir install -U -r requirements.txt \
|
||||||
&& curl -sSL https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 -o /usr/local/bin/dumb-init \
|
&& curl -sSL https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 -o /usr/local/bin/dumb-init \
|
||||||
&& chmod +x /usr/local/bin/dumb-init
|
&& chmod +x /usr/local/bin/dumb-init
|
||||||
|
@ -42,7 +42,7 @@ class InnoBackupEx(mysql_base.MySQLBaseRunner):
|
|||||||
cmd = ('innobackupex'
|
cmd = ('innobackupex'
|
||||||
' --stream=xbstream'
|
' --stream=xbstream'
|
||||||
' --parallel=2 ' +
|
' --parallel=2 ' +
|
||||||
self.user_and_pass + ' %s' % self.default_data_dir +
|
self.user_and_pass + ' %s' % self.datadir +
|
||||||
' 2>' + self.backup_log
|
' 2>' + self.backup_log
|
||||||
)
|
)
|
||||||
return cmd + self.zip_cmd + self.encrypt_cmd
|
return cmd + self.zip_cmd + self.encrypt_cmd
|
||||||
@ -111,7 +111,7 @@ class InnoBackupExIncremental(InnoBackupEx):
|
|||||||
' --stream=xbstream'
|
' --stream=xbstream'
|
||||||
' --incremental'
|
' --incremental'
|
||||||
' --incremental-lsn=%(lsn)s ' +
|
' --incremental-lsn=%(lsn)s ' +
|
||||||
self.user_and_pass + ' %s' % self.default_data_dir +
|
self.user_and_pass + ' %s' % self.datadir +
|
||||||
' 2>' + self.backup_log)
|
' 2>' + self.backup_log)
|
||||||
return cmd + self.zip_cmd + self.encrypt_cmd
|
return cmd + self.zip_cmd + self.encrypt_cmd
|
||||||
|
|
||||||
|
@ -53,7 +53,8 @@ class MySQLBaseRunner(base.BaseRunner):
|
|||||||
|
|
||||||
last_line = output.splitlines()[-1].strip()
|
last_line = output.splitlines()[-1].strip()
|
||||||
if not re.search('completed OK!', last_line):
|
if not re.search('completed OK!', last_line):
|
||||||
LOG.error("Backup did not complete successfully.")
|
LOG.error(f"Backup did not complete successfully, last line:\n"
|
||||||
|
f"{last_line}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -9,23 +9,25 @@ case "$1" in
|
|||||||
curl -sSL https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb -o percona-release.deb
|
curl -sSL https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb -o percona-release.deb
|
||||||
dpkg -i percona-release.deb
|
dpkg -i percona-release.deb
|
||||||
percona-release enable-only tools release
|
percona-release enable-only tools release
|
||||||
|
apt-get update
|
||||||
apt-get install $APTOPTS percona-xtrabackup-$2
|
apt-get install $APTOPTS percona-xtrabackup-$2
|
||||||
apt-get clean
|
rm -f percona-release.deb
|
||||||
;;
|
;;
|
||||||
"mariadb")
|
"mariadb")
|
||||||
apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc'
|
apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc'
|
||||||
add-apt-repository "deb [arch=amd64] http://mirror2.hs-esslingen.de/mariadb/repo/10.4/ubuntu $(lsb_release -cs) main"
|
add-apt-repository "deb [arch=amd64] http://mirror2.hs-esslingen.de/mariadb/repo/10.4/ubuntu $(lsb_release -cs) main"
|
||||||
apt-get install $APTOPTS mariadb-backup
|
apt-get install $APTOPTS mariadb-backup
|
||||||
apt-get clean
|
|
||||||
;;
|
;;
|
||||||
"postgresql")
|
"postgresql")
|
||||||
apt-key adv --fetch-keys 'https://www.postgresql.org/media/keys/ACCC4CF8.asc'
|
apt-key adv --fetch-keys 'https://www.postgresql.org/media/keys/ACCC4CF8.asc'
|
||||||
add-apt-repository "deb [arch=amd64] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main"
|
add-apt-repository "deb [arch=amd64] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main"
|
||||||
apt-get install $APTOPTS postgresql-client-12
|
apt-get install $APTOPTS postgresql-client-12
|
||||||
apt-get clean
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "datastore $1 not supported"
|
echo "datastore $1 not supported"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -80,19 +80,19 @@ class Commands(object):
|
|||||||
|
|
||||||
def db_load_datastore_config_parameters(self,
|
def db_load_datastore_config_parameters(self,
|
||||||
datastore,
|
datastore,
|
||||||
datastore_version,
|
datastore_version_name,
|
||||||
config_file_location):
|
config_file_location):
|
||||||
print("Loading config parameters for datastore (%s) version (%s)"
|
print("Loading config parameters for datastore (%s) version (%s)"
|
||||||
% (datastore, datastore_version))
|
% (datastore, datastore_version_name))
|
||||||
config_models.load_datastore_configuration_parameters(
|
config_models.load_datastore_configuration_parameters(
|
||||||
datastore, datastore_version, config_file_location)
|
datastore, datastore_version_name, config_file_location)
|
||||||
|
|
||||||
def db_remove_datastore_config_parameters(self, datastore,
|
def db_remove_datastore_config_parameters(self, datastore,
|
||||||
datastore_version):
|
datastore_version_name):
|
||||||
print("Removing config parameters for datastore (%s) version (%s)"
|
print("Removing config parameters for datastore (%s) version (%s)"
|
||||||
% (datastore, datastore_version))
|
% (datastore, datastore_version_name))
|
||||||
config_models.remove_datastore_configuration_parameters(
|
config_models.remove_datastore_configuration_parameters(
|
||||||
datastore, datastore_version)
|
datastore, datastore_version_name)
|
||||||
|
|
||||||
def datastore_version_flavor_add(self, datastore_name,
|
def datastore_version_flavor_add(self, datastore_name,
|
||||||
datastore_version_name, flavor_ids):
|
datastore_version_name, flavor_ids):
|
||||||
@ -230,7 +230,7 @@ def main():
|
|||||||
'datastore',
|
'datastore',
|
||||||
help='Name of the datastore.')
|
help='Name of the datastore.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'datastore_version',
|
'datastore_version_name',
|
||||||
help='Name of the datastore version.')
|
help='Name of the datastore version.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'config_file_location',
|
'config_file_location',
|
||||||
@ -245,7 +245,7 @@ def main():
|
|||||||
'datastore',
|
'datastore',
|
||||||
help='Name of the datastore.')
|
help='Name of the datastore.')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'datastore_version',
|
'datastore_version_name',
|
||||||
help='Name of the datastore version.')
|
help='Name of the datastore version.')
|
||||||
|
|
||||||
parser = subparser.add_parser(
|
parser = subparser.add_parser(
|
||||||
|
@ -250,6 +250,9 @@ common_opts = [
|
|||||||
cfg.StrOpt('datastore_manager', default=None,
|
cfg.StrOpt('datastore_manager', default=None,
|
||||||
help='Manager class in the Guest Agent, set up by the '
|
help='Manager class in the Guest Agent, set up by the '
|
||||||
'Taskmanager on instance provision.'),
|
'Taskmanager on instance provision.'),
|
||||||
|
cfg.StrOpt('datastore_version', default=None,
|
||||||
|
help='The guest datastore version that is set by the '
|
||||||
|
'Taskmanager during instance provision.'),
|
||||||
cfg.StrOpt('block_device_mapping', default='vdb',
|
cfg.StrOpt('block_device_mapping', default='vdb',
|
||||||
help='Block device to map onto the created instance.'),
|
help='Block device to map onto the created instance.'),
|
||||||
cfg.IntOpt('server_delete_time_out', default=60,
|
cfg.IntOpt('server_delete_time_out', default=60,
|
||||||
@ -1076,13 +1079,16 @@ postgresql_opts = [
|
|||||||
help='The TCP port the server listens on.'),
|
help='The TCP port the server listens on.'),
|
||||||
cfg.StrOpt('backup_strategy', default='pg_basebackup',
|
cfg.StrOpt('backup_strategy', default='pg_basebackup',
|
||||||
help='Default strategy to perform backups.'),
|
help='Default strategy to perform backups.'),
|
||||||
cfg.StrOpt('replication_strategy',
|
cfg.StrOpt(
|
||||||
|
'replication_strategy',
|
||||||
default='PostgresqlReplicationStreaming',
|
default='PostgresqlReplicationStreaming',
|
||||||
help='Default strategy for replication.'),
|
help='Default strategy for replication.'
|
||||||
cfg.StrOpt('replication_namespace',
|
),
|
||||||
default='trove.guestagent.strategies.replication.experimental.'
|
cfg.StrOpt(
|
||||||
'postgresql_impl',
|
'replication_namespace',
|
||||||
help='Namespace to load replication strategies from.'),
|
default='trove.guestagent.strategies.replication.postgresql',
|
||||||
|
help='Namespace to load replication strategies from.'
|
||||||
|
),
|
||||||
cfg.StrOpt('mount_point', default='/var/lib/postgresql',
|
cfg.StrOpt('mount_point', default='/var/lib/postgresql',
|
||||||
help="Filesystem path for mounting "
|
help="Filesystem path for mounting "
|
||||||
"volumes if volume support is enabled."),
|
"volumes if volume support is enabled."),
|
||||||
|
@ -542,13 +542,14 @@ class API(object):
|
|||||||
return self._call("get_replica_context",
|
return self._call("get_replica_context",
|
||||||
self.agent_high_timeout, version=version)
|
self.agent_high_timeout, version=version)
|
||||||
|
|
||||||
def attach_replica(self, replica_info, slave_config):
|
def attach_replica(self, replica_info, slave_config, restart=False):
|
||||||
LOG.debug("Attaching replica %s.", replica_info)
|
LOG.debug("Attaching replica %s.", replica_info)
|
||||||
version = self.API_BASE_VERSION
|
version = self.API_BASE_VERSION
|
||||||
|
|
||||||
self._call("attach_replica",
|
self._call("attach_replica",
|
||||||
self.agent_high_timeout, version=version,
|
self.agent_high_timeout, version=version,
|
||||||
replica_info=replica_info, slave_config=slave_config)
|
replica_info=replica_info, slave_config=slave_config,
|
||||||
|
restart=restart)
|
||||||
|
|
||||||
def make_read_only(self, read_only):
|
def make_read_only(self, read_only):
|
||||||
LOG.debug("Executing make_read_only(%s)", read_only)
|
LOG.debug("Executing make_read_only(%s)", read_only)
|
||||||
|
@ -115,6 +115,19 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def replication(self):
|
||||||
|
"""If the datastore supports replication, return an instance of
|
||||||
|
the strategy.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return repl_strategy.get_instance(self.manager)
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.warning("Cannot get replication instance for '%(manager)s': "
|
||||||
|
"%(msg)s", {'manager': self.manager, 'msg': str(ex)})
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def replication_strategy(self):
|
def replication_strategy(self):
|
||||||
"""If the datastore supports replication, return the strategy."""
|
"""If the datastore supports replication, return the strategy."""
|
||||||
@ -825,41 +838,63 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
################
|
################
|
||||||
# Replication related
|
# Replication related
|
||||||
################
|
################
|
||||||
|
def backup_required_for_replication(self, context):
|
||||||
|
return self.replication.backup_required_for_replication()
|
||||||
|
|
||||||
def get_replication_snapshot(self, context, snapshot_info,
|
def get_replication_snapshot(self, context, snapshot_info,
|
||||||
replica_source_config=None):
|
replica_source_config=None):
|
||||||
LOG.debug("Getting replication snapshot.")
|
LOG.info("Getting replication snapshot, snapshot_info: %s",
|
||||||
raise exception.DatastoreOperationNotSupported(
|
snapshot_info)
|
||||||
operation='get_replication_snapshot', datastore=self.manager)
|
|
||||||
|
|
||||||
def attach_replication_slave(self, context, snapshot, slave_config):
|
self.replication.enable_as_master(self.app, replica_source_config)
|
||||||
LOG.debug("Attaching replication slave.")
|
LOG.info('Enabled as replication master')
|
||||||
|
|
||||||
|
snapshot_id, log_position = self.replication.snapshot_for_replication(
|
||||||
|
context, self.app, self.adm, None, snapshot_info)
|
||||||
|
|
||||||
|
volume_stats = self.get_filesystem_stats(context, None)
|
||||||
|
|
||||||
|
replication_snapshot = {
|
||||||
|
'dataset': {
|
||||||
|
'datastore_manager': self.manager,
|
||||||
|
'dataset_size': volume_stats.get('used', 0.0),
|
||||||
|
'volume_size': volume_stats.get('total', 0.0),
|
||||||
|
'snapshot_id': snapshot_id
|
||||||
|
},
|
||||||
|
'replication_strategy': self.replication_strategy,
|
||||||
|
'master': self.replication.get_master_ref(self.app, snapshot_info),
|
||||||
|
'log_position': log_position
|
||||||
|
}
|
||||||
|
|
||||||
|
return replication_snapshot
|
||||||
|
|
||||||
|
def attach_replica(self, context, snapshot, slave_config, restart=False):
|
||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
operation='attach_replication_slave', datastore=self.manager)
|
operation='attach_replication_slave', datastore=self.manager)
|
||||||
|
|
||||||
def detach_replica(self, context, for_failover=False):
|
def detach_replica(self, context, for_failover=False):
|
||||||
LOG.debug("Detaching replica.")
|
"""Running on replica, detach from the primary."""
|
||||||
raise exception.DatastoreOperationNotSupported(
|
LOG.info("Detaching replica.")
|
||||||
operation='detach_replica', datastore=self.manager)
|
replica_info = self.replication.detach_slave(self.app, for_failover)
|
||||||
|
return replica_info
|
||||||
|
|
||||||
def get_replica_context(self, context):
|
def get_replica_context(self, context):
|
||||||
LOG.debug("Getting replica context.")
|
"""Running on primary."""
|
||||||
raise exception.DatastoreOperationNotSupported(
|
LOG.info("Getting replica context.")
|
||||||
operation='get_replica_context', datastore=self.manager)
|
replica_info = self.replication.get_replica_context(self.app, self.adm)
|
||||||
|
return replica_info
|
||||||
|
|
||||||
def make_read_only(self, context, read_only):
|
def make_read_only(self, context, read_only):
|
||||||
LOG.debug("Making datastore read-only.")
|
|
||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
operation='make_read_only', datastore=self.manager)
|
operation='make_read_only', datastore=self.manager)
|
||||||
|
|
||||||
def enable_as_master(self, context, replica_source_config):
|
def enable_as_master(self, context, replica_source_config):
|
||||||
LOG.debug("Enabling as master.")
|
LOG.info("Enable as master")
|
||||||
raise exception.DatastoreOperationNotSupported(
|
self.replication.enable_as_master(self.app, replica_source_config)
|
||||||
operation='enable_as_master', datastore=self.manager)
|
|
||||||
|
|
||||||
def demote_replication_master(self, context):
|
def demote_replication_master(self, context):
|
||||||
LOG.debug("Demoting replication master.")
|
LOG.info("Demoting replication master.")
|
||||||
raise exception.DatastoreOperationNotSupported(
|
self.replication.demote_master(self.app)
|
||||||
operation='demote_replication_master', datastore=self.manager)
|
|
||||||
|
|
||||||
def get_txn_count(self, context):
|
def get_txn_count(self, context):
|
||||||
LOG.debug("Getting transaction count.")
|
LOG.debug("Getting transaction count.")
|
||||||
@ -867,11 +902,9 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
operation='get_txn_count', datastore=self.manager)
|
operation='get_txn_count', datastore=self.manager)
|
||||||
|
|
||||||
def get_latest_txn_id(self, context):
|
def get_latest_txn_id(self, context):
|
||||||
LOG.debug("Getting latest transaction id.")
|
|
||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
operation='get_latest_txn_id', datastore=self.manager)
|
operation='get_latest_txn_id', datastore=self.manager)
|
||||||
|
|
||||||
def wait_for_txn(self, context, txn):
|
def wait_for_txn(self, context, txn):
|
||||||
LOG.debug("Waiting for transaction.")
|
|
||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
operation='wait_for_txn', datastore=self.manager)
|
operation='wait_for_txn', datastore=self.manager)
|
||||||
|
@ -27,7 +27,6 @@ from trove.common.notification import EndNotification
|
|||||||
from trove.guestagent import guest_log
|
from trove.guestagent import guest_log
|
||||||
from trove.guestagent.common import operating_system
|
from trove.guestagent.common import operating_system
|
||||||
from trove.guestagent.datastore import manager
|
from trove.guestagent.datastore import manager
|
||||||
from trove.guestagent.strategies import replication as repl_strategy
|
|
||||||
from trove.guestagent.utils import docker as docker_util
|
from trove.guestagent.utils import docker as docker_util
|
||||||
from trove.guestagent.utils import mysql as mysql_util
|
from trove.guestagent.utils import mysql as mysql_util
|
||||||
from trove.instance import service_status
|
from trove.instance import service_status
|
||||||
@ -50,19 +49,6 @@ class MySqlManager(manager.Manager):
|
|||||||
def configuration_manager(self):
|
def configuration_manager(self):
|
||||||
return self.app.configuration_manager
|
return self.app.configuration_manager
|
||||||
|
|
||||||
@property
|
|
||||||
def replication(self):
|
|
||||||
"""If the datastore supports replication, return an instance of
|
|
||||||
the strategy.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return repl_strategy.get_instance(self.manager)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.warning("Cannot get replication instance for '%(manager)s': "
|
|
||||||
"%(msg)s", {'manager': self.manager, 'msg': str(ex)})
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_service_status(self):
|
def get_service_status(self):
|
||||||
try:
|
try:
|
||||||
with mysql_util.SqlClient(self.app.get_engine()) as client:
|
with mysql_util.SqlClient(self.app.get_engine()) as client:
|
||||||
@ -133,7 +119,8 @@ class MySqlManager(manager.Manager):
|
|||||||
LOG.info(f"Creating backup {backup_info['id']}")
|
LOG.info(f"Creating backup {backup_info['id']}")
|
||||||
with EndNotification(context):
|
with EndNotification(context):
|
||||||
volumes_mapping = {
|
volumes_mapping = {
|
||||||
'/var/lib/mysql': {'bind': '/var/lib/mysql', 'mode': 'rw'}
|
'/var/lib/mysql': {'bind': '/var/lib/mysql', 'mode': 'rw'},
|
||||||
|
'/tmp': {'bind': '/tmp', 'mode': 'rw'}
|
||||||
}
|
}
|
||||||
self.app.create_backup(context, backup_info,
|
self.app.create_backup(context, backup_info,
|
||||||
volumes_mapping=volumes_mapping,
|
volumes_mapping=volumes_mapping,
|
||||||
@ -273,7 +260,7 @@ class MySqlManager(manager.Manager):
|
|||||||
'slave_volume_size': volume_stats.get('total', 0.0)
|
'slave_volume_size': volume_stats.get('total', 0.0)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def attach_replica(self, context, replica_info, slave_config):
|
def attach_replica(self, context, replica_info, slave_config, **kwargs):
|
||||||
LOG.info("Attaching replica, replica_info: %s", replica_info)
|
LOG.info("Attaching replica, replica_info: %s", replica_info)
|
||||||
try:
|
try:
|
||||||
if 'replication_strategy' in replica_info:
|
if 'replication_strategy' in replica_info:
|
||||||
@ -286,45 +273,6 @@ class MySqlManager(manager.Manager):
|
|||||||
self.status.set_status(service_status.ServiceStatuses.FAILED)
|
self.status.set_status(service_status.ServiceStatuses.FAILED)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def detach_replica(self, context, for_failover=False):
|
|
||||||
LOG.info("Detaching replica.")
|
|
||||||
replica_info = self.replication.detach_slave(self.app, for_failover)
|
|
||||||
return replica_info
|
|
||||||
|
|
||||||
def backup_required_for_replication(self, context):
|
|
||||||
return self.replication.backup_required_for_replication()
|
|
||||||
|
|
||||||
def get_replication_snapshot(self, context, snapshot_info,
|
|
||||||
replica_source_config=None):
|
|
||||||
LOG.info("Getting replication snapshot, snapshot_info: %s",
|
|
||||||
snapshot_info)
|
|
||||||
|
|
||||||
self.replication.enable_as_master(self.app, replica_source_config)
|
|
||||||
LOG.info('Enabled as replication master')
|
|
||||||
|
|
||||||
snapshot_id, log_position = self.replication.snapshot_for_replication(
|
|
||||||
context, self.app, self.adm, None, snapshot_info)
|
|
||||||
|
|
||||||
volume_stats = self.get_filesystem_stats(context, None)
|
|
||||||
|
|
||||||
replication_snapshot = {
|
|
||||||
'dataset': {
|
|
||||||
'datastore_manager': self.manager,
|
|
||||||
'dataset_size': volume_stats.get('used', 0.0),
|
|
||||||
'volume_size': volume_stats.get('total', 0.0),
|
|
||||||
'snapshot_id': snapshot_id
|
|
||||||
},
|
|
||||||
'replication_strategy': self.replication_strategy,
|
|
||||||
'master': self.replication.get_master_ref(self.app, snapshot_info),
|
|
||||||
'log_position': log_position
|
|
||||||
}
|
|
||||||
|
|
||||||
return replication_snapshot
|
|
||||||
|
|
||||||
def enable_as_master(self, context, replica_source_config):
|
|
||||||
LOG.info("Enable as master")
|
|
||||||
self.replication.enable_as_master(self.app, replica_source_config)
|
|
||||||
|
|
||||||
def make_read_only(self, context, read_only):
|
def make_read_only(self, context, read_only):
|
||||||
LOG.info("Executing make_read_only(%s)", read_only)
|
LOG.info("Executing make_read_only(%s)", read_only)
|
||||||
self.app.make_read_only(read_only)
|
self.app.make_read_only(read_only)
|
||||||
@ -341,15 +289,6 @@ class MySqlManager(manager.Manager):
|
|||||||
LOG.info("Calling wait_for_txn.")
|
LOG.info("Calling wait_for_txn.")
|
||||||
self.app.wait_for_txn(txn)
|
self.app.wait_for_txn(txn)
|
||||||
|
|
||||||
def get_replica_context(self, context):
|
|
||||||
LOG.info("Getting replica context.")
|
|
||||||
replica_info = self.replication.get_replica_context(self.app, self.adm)
|
|
||||||
return replica_info
|
|
||||||
|
|
||||||
def demote_replication_master(self, context):
|
|
||||||
LOG.info("Demoting replication master.")
|
|
||||||
self.replication.demote_master(self.app)
|
|
||||||
|
|
||||||
def upgrade(self, context, upgrade_info):
|
def upgrade(self, context, upgrade_info):
|
||||||
"""Upgrade the database."""
|
"""Upgrade the database."""
|
||||||
LOG.info('Starting to upgrade database, upgrade_info: %s',
|
LOG.info('Starting to upgrade database, upgrade_info: %s',
|
||||||
|
@ -16,6 +16,8 @@ import os
|
|||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from trove.common import cfg
|
from trove.common import cfg
|
||||||
|
from trove.common import exception
|
||||||
|
from trove.common import utils
|
||||||
from trove.common.notification import EndNotification
|
from trove.common.notification import EndNotification
|
||||||
from trove.guestagent import guest_log
|
from trove.guestagent import guest_log
|
||||||
from trove.guestagent.common import operating_system
|
from trove.guestagent.common import operating_system
|
||||||
@ -56,10 +58,15 @@ class PostgresManager(manager.Manager):
|
|||||||
self.app.set_data_dir(self.app.datadir)
|
self.app.set_data_dir(self.app.datadir)
|
||||||
self.app.update_overrides(overrides)
|
self.app.update_overrides(overrides)
|
||||||
|
|
||||||
|
# Prepare pg_hba.conf
|
||||||
|
self.app.apply_access_rules()
|
||||||
|
self.configuration_manager.apply_system_override(
|
||||||
|
{'hba_file': service.HBA_CONFIG_FILE})
|
||||||
|
|
||||||
# Restore data from backup and reset root password
|
# Restore data from backup and reset root password
|
||||||
if backup_info:
|
if backup_info:
|
||||||
self.perform_restore(context, self.app.datadir, backup_info)
|
self.perform_restore(context, self.app.datadir, backup_info)
|
||||||
|
if not snapshot:
|
||||||
signal_file = f"{self.app.datadir}/recovery.signal"
|
signal_file = f"{self.app.datadir}/recovery.signal"
|
||||||
operating_system.execute_shell_cmd(
|
operating_system.execute_shell_cmd(
|
||||||
f"touch {signal_file}", [], shell=True, as_root=True)
|
f"touch {signal_file}", [], shell=True, as_root=True)
|
||||||
@ -67,14 +74,13 @@ class PostgresManager(manager.Manager):
|
|||||||
CONF.database_service_uid, force=True,
|
CONF.database_service_uid, force=True,
|
||||||
as_root=True)
|
as_root=True)
|
||||||
|
|
||||||
|
if snapshot:
|
||||||
|
# This instance is a replica
|
||||||
|
self.attach_replica(context, snapshot, snapshot['config'])
|
||||||
|
|
||||||
# config_file can only be set on the postgres command line
|
# config_file can only be set on the postgres command line
|
||||||
command = f"postgres -c config_file={service.CONFIG_FILE}"
|
command = f"postgres -c config_file={service.CONFIG_FILE}"
|
||||||
self.app.start_db(ds_version=ds_version, command=command)
|
self.app.start_db(ds_version=ds_version, command=command)
|
||||||
self.app.secure()
|
|
||||||
|
|
||||||
# if snapshot:
|
|
||||||
# # This instance is a replication slave
|
|
||||||
# self.attach_replica(context, snapshot, snapshot['config'])
|
|
||||||
|
|
||||||
def apply_overrides(self, context, overrides):
|
def apply_overrides(self, context, overrides):
|
||||||
pass
|
pass
|
||||||
@ -134,3 +140,46 @@ class PostgresManager(manager.Manager):
|
|||||||
volumes_mapping=volumes_mapping,
|
volumes_mapping=volumes_mapping,
|
||||||
need_dbuser=False,
|
need_dbuser=False,
|
||||||
extra_params=extra_params)
|
extra_params=extra_params)
|
||||||
|
|
||||||
|
def attach_replica(self, context, replica_info, slave_config,
|
||||||
|
restart=False):
|
||||||
|
"""Set up the standby server."""
|
||||||
|
self.replication.enable_as_slave(self.app, replica_info, None)
|
||||||
|
|
||||||
|
# For the previous primary, don't start db service in order to run
|
||||||
|
# pg_rewind command next.
|
||||||
|
if restart:
|
||||||
|
self.app.restart()
|
||||||
|
|
||||||
|
def make_read_only(self, context, read_only):
|
||||||
|
"""There seems to be no way to flag this at the database level in
|
||||||
|
PostgreSQL at the moment -- see discussion here:
|
||||||
|
http://www.postgresql.org/message-id/flat/CA+TgmobWQJ-GCa_tWUc4=80A
|
||||||
|
1RJ2_+Rq3w_MqaVguk_q018dqw@mail.gmail.com#CA+TgmobWQJ-GCa_tWUc4=80A1RJ
|
||||||
|
2_+Rq3w_MqaVguk_q018dqw@mail.gmail.com
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_latest_txn_id(self, context):
|
||||||
|
if self.app.is_replica():
|
||||||
|
lsn = self.app.get_last_wal_replay_lsn()
|
||||||
|
else:
|
||||||
|
lsn = self.app.get_current_wal_lsn()
|
||||||
|
LOG.info("Last wal location found: %s", lsn)
|
||||||
|
return lsn
|
||||||
|
|
||||||
|
def wait_for_txn(self, context, txn):
|
||||||
|
if not self.app.is_replica():
|
||||||
|
raise exception.TroveError("Attempting to wait for a txn on a "
|
||||||
|
"non-replica server")
|
||||||
|
|
||||||
|
def _wait_for_txn():
|
||||||
|
lsn = self.app.get_last_wal_replay_lsn()
|
||||||
|
LOG.info("Last wal location found: %s", lsn)
|
||||||
|
return lsn >= txn
|
||||||
|
|
||||||
|
try:
|
||||||
|
utils.poll_until(_wait_for_txn, time_out=60)
|
||||||
|
except exception.PollTimeOut:
|
||||||
|
raise exception.TroveError(
|
||||||
|
f"Timeout occurred waiting for wal offset to change to {txn}")
|
||||||
|
@ -32,7 +32,6 @@ from trove.instance import service_status
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
ADMIN_USER_NAME = "os_admin"
|
|
||||||
SUPER_USER_NAME = "postgres"
|
SUPER_USER_NAME = "postgres"
|
||||||
CONFIG_FILE = "/etc/postgresql/postgresql.conf"
|
CONFIG_FILE = "/etc/postgresql/postgresql.conf"
|
||||||
CNF_EXT = 'conf'
|
CNF_EXT = 'conf'
|
||||||
@ -95,6 +94,7 @@ class PgSqlApp(service.BaseDbApp):
|
|||||||
# https://github.com/docker-library/docs/blob/master/postgres/README.md#pgdata
|
# https://github.com/docker-library/docs/blob/master/postgres/README.md#pgdata
|
||||||
mount_point = cfg.get_configuration_property('mount_point')
|
mount_point = cfg.get_configuration_property('mount_point')
|
||||||
self.datadir = f"{mount_point}/data/pgdata"
|
self.datadir = f"{mount_point}/data/pgdata"
|
||||||
|
self.adm = PgSqlAdmin(SUPER_USER_NAME)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_data_dir(cls):
|
def get_data_dir(cls):
|
||||||
@ -109,25 +109,6 @@ class PgSqlApp(service.BaseDbApp):
|
|||||||
cmd = f"pg_ctl reload -D {self.datadir}"
|
cmd = f"pg_ctl reload -D {self.datadir}"
|
||||||
docker_util.run_command(self.docker_client, cmd)
|
docker_util.run_command(self.docker_client, cmd)
|
||||||
|
|
||||||
def secure(self):
|
|
||||||
LOG.info("Securing PostgreSQL now.")
|
|
||||||
|
|
||||||
admin_password = utils.generate_random_password()
|
|
||||||
os_admin = models.PostgreSQLUser(ADMIN_USER_NAME, admin_password)
|
|
||||||
|
|
||||||
# Drop os_admin user if exists, this is needed for restore.
|
|
||||||
PgSqlAdmin(SUPER_USER_NAME).delete_user({'_name': ADMIN_USER_NAME})
|
|
||||||
PgSqlAdmin(SUPER_USER_NAME).create_admin_user(os_admin,
|
|
||||||
encrypt_password=True)
|
|
||||||
self.save_password(ADMIN_USER_NAME, admin_password)
|
|
||||||
|
|
||||||
self.apply_access_rules()
|
|
||||||
self.configuration_manager.apply_system_override(
|
|
||||||
{'hba_file': HBA_CONFIG_FILE})
|
|
||||||
self.restart()
|
|
||||||
|
|
||||||
LOG.info("PostgreSQL secure complete.")
|
|
||||||
|
|
||||||
def apply_access_rules(self):
|
def apply_access_rules(self):
|
||||||
"""PostgreSQL Client authentication settings
|
"""PostgreSQL Client authentication settings
|
||||||
|
|
||||||
@ -137,17 +118,15 @@ class PgSqlApp(service.BaseDbApp):
|
|||||||
"""
|
"""
|
||||||
LOG.debug("Applying client authentication access rules.")
|
LOG.debug("Applying client authentication access rules.")
|
||||||
|
|
||||||
local_admins = ','.join([SUPER_USER_NAME, ADMIN_USER_NAME])
|
|
||||||
remote_admins = SUPER_USER_NAME
|
|
||||||
access_rules = OrderedDict(
|
access_rules = OrderedDict(
|
||||||
[('local', [['all', local_admins, None, 'trust'],
|
[('local', [['all', SUPER_USER_NAME, None, 'trust'],
|
||||||
['replication', local_admins, None, 'trust'],
|
['replication', SUPER_USER_NAME, None, 'trust'],
|
||||||
['all', 'all', None, 'md5']]),
|
['all', 'all', None, 'md5']]),
|
||||||
('host', [['all', local_admins, '127.0.0.1/32', 'trust'],
|
('host', [['all', SUPER_USER_NAME, '127.0.0.1/32', 'trust'],
|
||||||
['all', local_admins, '::1/128', 'trust'],
|
['all', SUPER_USER_NAME, '::1/128', 'trust'],
|
||||||
['all', local_admins, 'localhost', 'trust'],
|
['all', SUPER_USER_NAME, 'localhost', 'trust'],
|
||||||
['all', remote_admins, '0.0.0.0/0', 'reject'],
|
['all', SUPER_USER_NAME, '0.0.0.0/0', 'reject'],
|
||||||
['all', remote_admins, '::/0', 'reject'],
|
['all', SUPER_USER_NAME, '::/0', 'reject'],
|
||||||
['all', 'all', '0.0.0.0/0', 'md5'],
|
['all', 'all', '0.0.0.0/0', 'md5'],
|
||||||
['all', 'all', '::/0', 'md5']])
|
['all', 'all', '::/0', 'md5']])
|
||||||
])
|
])
|
||||||
@ -307,6 +286,57 @@ class PgSqlApp(service.BaseDbApp):
|
|||||||
CONF.database_service_uid, force=True,
|
CONF.database_service_uid, force=True,
|
||||||
as_root=True)
|
as_root=True)
|
||||||
|
|
||||||
|
def is_replica(self):
|
||||||
|
"""Wrapper for pg_is_in_recovery() for detecting a server in
|
||||||
|
standby mode
|
||||||
|
"""
|
||||||
|
r = self.adm.query("SELECT pg_is_in_recovery()")
|
||||||
|
return r[0][0]
|
||||||
|
|
||||||
|
def get_current_wal_lsn(self):
|
||||||
|
"""Wrapper for pg_current_wal_lsn()
|
||||||
|
|
||||||
|
Cannot be used against a running replica
|
||||||
|
"""
|
||||||
|
r = self.adm.query("SELECT pg_current_wal_lsn()")
|
||||||
|
return r[0][0]
|
||||||
|
|
||||||
|
def get_last_wal_replay_lsn(self):
|
||||||
|
"""Wrapper for pg_last_wal_replay_lsn()
|
||||||
|
|
||||||
|
For use on replica servers
|
||||||
|
"""
|
||||||
|
r = self.adm.query("SELECT pg_last_wal_replay_lsn()")
|
||||||
|
return r[0][0]
|
||||||
|
|
||||||
|
def pg_rewind(self, conn_info):
|
||||||
|
docker_image = CONF.get(CONF.datastore_manager).docker_image
|
||||||
|
image = f'{docker_image}:{CONF.datastore_version}'
|
||||||
|
user = "%s:%s" % (CONF.database_service_uid, CONF.database_service_uid)
|
||||||
|
volumes = {
|
||||||
|
"/var/run/postgresql": {"bind": "/var/run/postgresql",
|
||||||
|
"mode": "rw"},
|
||||||
|
"/var/lib/postgresql": {"bind": "/var/lib/postgresql",
|
||||||
|
"mode": "rw"},
|
||||||
|
"/var/lib/postgresql/data": {"bind": "/var/lib/postgresql/data",
|
||||||
|
"mode": "rw"},
|
||||||
|
}
|
||||||
|
command = (f"pg_rewind --target-pgdata={self.datadir} "
|
||||||
|
f"--source-server='{conn_info}'")
|
||||||
|
|
||||||
|
docker_util.remove_container(self.docker_client, name='pg_rewind')
|
||||||
|
|
||||||
|
LOG.info('Running pg_rewind in container')
|
||||||
|
output, ret = docker_util.run_container(
|
||||||
|
self.docker_client, image, 'pg_rewind',
|
||||||
|
volumes=volumes, command=command, user=user)
|
||||||
|
result = output[-1]
|
||||||
|
LOG.debug(f"Finished running pg_rewind, last output: {result}")
|
||||||
|
if not ret:
|
||||||
|
msg = f'Failed to run pg_rewind in container, error: {result}'
|
||||||
|
LOG.error(msg)
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
|
||||||
class PgSqlAdmin(object):
|
class PgSqlAdmin(object):
|
||||||
# Default set of options of an administrative account.
|
# Default set of options of an administrative account.
|
||||||
|
@ -440,7 +440,7 @@ class BaseDbApp(object):
|
|||||||
swift_container = (backup_info.get('swift_container') or
|
swift_container = (backup_info.get('swift_container') or
|
||||||
CONF.backup_swift_container)
|
CONF.backup_swift_container)
|
||||||
swift_params = (f'--swift-extra-metadata={swift_metadata} '
|
swift_params = (f'--swift-extra-metadata={swift_metadata} '
|
||||||
f'--swift-container {swift_container}')
|
f'--swift-container={swift_container}')
|
||||||
|
|
||||||
command = (
|
command = (
|
||||||
f'/usr/bin/python3 main.py --backup --backup-id={backup_id} '
|
f'/usr/bin/python3 main.py --backup --backup-id={backup_id} '
|
||||||
@ -449,7 +449,7 @@ class BaseDbApp(object):
|
|||||||
f'{db_userinfo} '
|
f'{db_userinfo} '
|
||||||
f'{swift_params} '
|
f'{swift_params} '
|
||||||
f'{incremental} '
|
f'{incremental} '
|
||||||
f'{extra_params} '
|
f'{extra_params}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update backup status in db
|
# Update backup status in db
|
||||||
@ -489,11 +489,13 @@ class BaseDbApp(object):
|
|||||||
'state': BackupState.COMPLETED,
|
'state': BackupState.COMPLETED,
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
LOG.error(f'Cannot parse backup output: {result}')
|
msg = f'Cannot parse backup output: {result}'
|
||||||
|
LOG.error(msg)
|
||||||
backup_state.update({
|
backup_state.update({
|
||||||
'success': False,
|
'success': False,
|
||||||
'state': BackupState.FAILED,
|
'state': BackupState.FAILED,
|
||||||
})
|
})
|
||||||
|
raise Exception(msg)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
LOG.error("Failed to create backup %s", backup_id)
|
LOG.error("Failed to create backup %s", backup_id)
|
||||||
backup_state.update({
|
backup_state.update({
|
||||||
|
@ -41,7 +41,7 @@ def get_instance(manager):
|
|||||||
replication_strategy, __replication_namespace)
|
replication_strategy, __replication_namespace)
|
||||||
__replication_instance = replication_strategy_cls()
|
__replication_instance = replication_strategy_cls()
|
||||||
__replication_manager = manager
|
__replication_manager = manager
|
||||||
LOG.debug('Got replication instance from: %(namespace)s.%(strategy)s',
|
LOG.debug('Replication instance from: %(namespace)s.%(strategy)s',
|
||||||
{'namespace': __replication_namespace,
|
{'namespace': __replication_namespace,
|
||||||
'strategy': __replication_strategy})
|
'strategy': __replication_strategy})
|
||||||
return __replication_instance
|
return __replication_instance
|
||||||
|
@ -79,7 +79,13 @@ class MysqlReplicationBase(base.Replication):
|
|||||||
def snapshot_for_replication(self, context, service, adm, location,
|
def snapshot_for_replication(self, context, service, adm, location,
|
||||||
snapshot_info):
|
snapshot_info):
|
||||||
LOG.info("Creating backup for replication")
|
LOG.info("Creating backup for replication")
|
||||||
service.create_backup(context, snapshot_info)
|
|
||||||
|
volumes_mapping = {
|
||||||
|
'/var/lib/mysql': {'bind': '/var/lib/mysql', 'mode': 'rw'},
|
||||||
|
'/tmp': {'bind': '/tmp', 'mode': 'rw'}
|
||||||
|
}
|
||||||
|
service.create_backup(context, snapshot_info,
|
||||||
|
volumes_mapping=volumes_mapping)
|
||||||
|
|
||||||
LOG.info('Creating replication user')
|
LOG.info('Creating replication user')
|
||||||
replication_user = self._create_replication_user(service, adm)
|
replication_user = self._create_replication_user(service, adm)
|
||||||
|
220
trove/guestagent/strategies/replication/postgresql.py
Normal file
220
trove/guestagent/strategies/replication/postgresql.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# Copyright 2020 Catalyst Cloud
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import netutils
|
||||||
|
|
||||||
|
from trove.common import cfg
|
||||||
|
from trove.common import exception
|
||||||
|
from trove.common import utils
|
||||||
|
from trove.common.db.postgresql import models
|
||||||
|
from trove.guestagent.common import operating_system
|
||||||
|
from trove.guestagent.common.operating_system import FileMode
|
||||||
|
from trove.guestagent.datastore.postgres import service as pg_service
|
||||||
|
from trove.guestagent.strategies.replication import base
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
REPL_USER = 'replicator'
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresqlReplicationStreaming(base.Replication):
|
||||||
|
def _create_replication_user(self, service, adm_mgr, pwfile):
|
||||||
|
"""Create the replication user and password file.
|
||||||
|
|
||||||
|
Unfortunately, to be able to run pg_rewind, we need SUPERUSER, not just
|
||||||
|
REPLICATION privilege
|
||||||
|
"""
|
||||||
|
pw = utils.generate_random_password()
|
||||||
|
operating_system.write_file(pwfile, pw, as_root=True)
|
||||||
|
operating_system.chown(pwfile, user=CONF.database_service_uid,
|
||||||
|
group=CONF.database_service_uid, as_root=True)
|
||||||
|
operating_system.chmod(pwfile, FileMode.SET_USR_RWX(),
|
||||||
|
as_root=True)
|
||||||
|
LOG.debug(f"File {pwfile} created")
|
||||||
|
|
||||||
|
LOG.debug(f"Creating replication user {REPL_USER}")
|
||||||
|
repl_user = models.PostgreSQLUser(name=REPL_USER, password=pw)
|
||||||
|
adm_mgr.create_user(repl_user, None,
|
||||||
|
*('REPLICATION', 'SUPERUSER', 'LOGIN'))
|
||||||
|
|
||||||
|
return pw
|
||||||
|
|
||||||
|
def _get_or_create_replication_user(self, service):
|
||||||
|
"""There are three scenarios we need to deal with here:
|
||||||
|
|
||||||
|
- This is a fresh master, with no replicator user created.
|
||||||
|
Generate a new u/p
|
||||||
|
- We are attaching a new slave and need to give it the login creds
|
||||||
|
Send the creds we have stored in PGDATA/.replpass
|
||||||
|
- This is a failed-over-to slave, who will have the replicator user
|
||||||
|
but not the credentials file. Recreate the repl user in this case
|
||||||
|
"""
|
||||||
|
LOG.debug("Checking for replication user")
|
||||||
|
|
||||||
|
pwfile = os.path.join(service.datadir, ".replpass")
|
||||||
|
adm_mgr = service.adm
|
||||||
|
|
||||||
|
if adm_mgr.user_exists(REPL_USER):
|
||||||
|
if operating_system.exists(pwfile, as_root=True):
|
||||||
|
LOG.debug("Found existing .replpass")
|
||||||
|
pw = operating_system.read_file(pwfile, as_root=True)
|
||||||
|
else:
|
||||||
|
LOG.debug("Found user but not .replpass, recreate")
|
||||||
|
adm_mgr.delete_user(models.PostgreSQLUser(REPL_USER))
|
||||||
|
pw = self._create_replication_user(service, adm_mgr, pwfile)
|
||||||
|
else:
|
||||||
|
LOG.debug("Found no replicator user, create one")
|
||||||
|
pw = self._create_replication_user(service, adm_mgr, pwfile)
|
||||||
|
|
||||||
|
repl_user_info = {
|
||||||
|
'name': REPL_USER,
|
||||||
|
'password': pw
|
||||||
|
}
|
||||||
|
|
||||||
|
return repl_user_info
|
||||||
|
|
||||||
|
def enable_as_master(self, service, master_config):
|
||||||
|
"""Primary postgredql settings.
|
||||||
|
|
||||||
|
For a server to be a master in postgres, we need to enable
|
||||||
|
the replication user in pg_hba.conf
|
||||||
|
"""
|
||||||
|
self._get_or_create_replication_user(service)
|
||||||
|
|
||||||
|
hba_entry = f"host replication {REPL_USER} 0.0.0.0/0 md5\n"
|
||||||
|
tmp_hba = '/tmp/pg_hba'
|
||||||
|
operating_system.copy(pg_service.HBA_CONFIG_FILE, tmp_hba,
|
||||||
|
force=True, as_root=True)
|
||||||
|
operating_system.chmod(tmp_hba, FileMode.SET_ALL_RWX(),
|
||||||
|
as_root=True)
|
||||||
|
with open(tmp_hba, 'a+') as hba_file:
|
||||||
|
hba_file.write(hba_entry)
|
||||||
|
|
||||||
|
operating_system.copy(tmp_hba, pg_service.HBA_CONFIG_FILE,
|
||||||
|
force=True, as_root=True)
|
||||||
|
operating_system.chown(pg_service.HBA_CONFIG_FILE,
|
||||||
|
user=CONF.database_service_uid,
|
||||||
|
group=CONF.database_service_uid, as_root=True)
|
||||||
|
operating_system.chmod(pg_service.HBA_CONFIG_FILE,
|
||||||
|
FileMode.SET_USR_RWX(),
|
||||||
|
as_root=True)
|
||||||
|
operating_system.remove(tmp_hba, as_root=True)
|
||||||
|
LOG.debug(f"{pg_service.HBA_CONFIG_FILE} changed")
|
||||||
|
|
||||||
|
service.restart()
|
||||||
|
|
||||||
|
def snapshot_for_replication(self, context, service, adm, location,
|
||||||
|
snapshot_info):
|
||||||
|
LOG.info("Creating backup for replication")
|
||||||
|
|
||||||
|
volumes_mapping = {
|
||||||
|
'/var/lib/postgresql/data': {
|
||||||
|
'bind': '/var/lib/postgresql/data', 'mode': 'rw'
|
||||||
|
},
|
||||||
|
"/var/run/postgresql": {"bind": "/var/run/postgresql",
|
||||||
|
"mode": "ro"},
|
||||||
|
}
|
||||||
|
extra_params = f"--pg-wal-archive-dir {pg_service.WAL_ARCHIVE_DIR}"
|
||||||
|
service.create_backup(context, snapshot_info,
|
||||||
|
volumes_mapping=volumes_mapping,
|
||||||
|
need_dbuser=False,
|
||||||
|
extra_params=extra_params)
|
||||||
|
|
||||||
|
LOG.info('Getting or creating replication user')
|
||||||
|
replication_user = self._get_or_create_replication_user(service)
|
||||||
|
|
||||||
|
log_position = {
|
||||||
|
'replication_user': replication_user
|
||||||
|
}
|
||||||
|
return snapshot_info['id'], log_position
|
||||||
|
|
||||||
|
def get_master_ref(self, service, snapshot_info):
|
||||||
|
master_ref = {
|
||||||
|
'host': netutils.get_my_ipv4(),
|
||||||
|
'port': cfg.get_configuration_property('postgresql_port')
|
||||||
|
}
|
||||||
|
return master_ref
|
||||||
|
|
||||||
|
def enable_as_slave(self, service, snapshot, slave_config):
|
||||||
|
"""Set up the replica server."""
|
||||||
|
signal_file = f"{service.datadir}/standby.signal"
|
||||||
|
operating_system.execute_shell_cmd(
|
||||||
|
f"touch {signal_file}", [], shell=True, as_root=True)
|
||||||
|
operating_system.chown(signal_file, CONF.database_service_uid,
|
||||||
|
CONF.database_service_uid, force=True,
|
||||||
|
as_root=True)
|
||||||
|
LOG.debug("Standby signal file created")
|
||||||
|
|
||||||
|
user = snapshot['log_position']['replication_user']
|
||||||
|
conninfo = (f"host={snapshot['master']['host']} "
|
||||||
|
f"port={snapshot['master']['port']} "
|
||||||
|
f"dbname=postgres "
|
||||||
|
f"user={user['name']} password={user['password']}")
|
||||||
|
service.configuration_manager.apply_system_override(
|
||||||
|
{'primary_conninfo': conninfo})
|
||||||
|
LOG.debug("primary_conninfo is set in the config file.")
|
||||||
|
|
||||||
|
def detach_slave(self, service, for_failover):
|
||||||
|
"""Promote replica and wait for its running.
|
||||||
|
|
||||||
|
Running on replica, detach from the primary.
|
||||||
|
"""
|
||||||
|
service.adm.query("select pg_promote()")
|
||||||
|
|
||||||
|
def _wait_for_failover():
|
||||||
|
"""Wait until slave has switched out of recovery mode"""
|
||||||
|
return not service.is_replica()
|
||||||
|
|
||||||
|
try:
|
||||||
|
utils.poll_until(_wait_for_failover, time_out=60)
|
||||||
|
except exception.PollTimeOut:
|
||||||
|
raise exception.TroveError(
|
||||||
|
"Timeout occurred waiting for replica to exit standby mode")
|
||||||
|
|
||||||
|
def get_replica_context(self, service, adm):
|
||||||
|
"""Running on primary."""
|
||||||
|
repl_user_info = self._get_or_create_replication_user(service)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'master': self.get_master_ref(None, None),
|
||||||
|
'log_position': {'replication_user': repl_user_info}
|
||||||
|
}
|
||||||
|
|
||||||
|
def cleanup_source_on_replica_detach(self, admin_service, replica_info):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _pg_rewind(self, service):
|
||||||
|
conn_info = service.configuration_manager.get_value('primary_conninfo')
|
||||||
|
service.pg_rewind(conn_info)
|
||||||
|
|
||||||
|
signal_file = f"{service.datadir}/standby.signal"
|
||||||
|
operating_system.execute_shell_cmd(
|
||||||
|
f"touch {signal_file}", [], shell=True, as_root=True)
|
||||||
|
operating_system.chown(signal_file, CONF.database_service_uid,
|
||||||
|
CONF.database_service_uid, force=True,
|
||||||
|
as_root=True)
|
||||||
|
LOG.debug("Standby signal file created")
|
||||||
|
|
||||||
|
def demote_master(self, service):
|
||||||
|
"""Running on the old primary.
|
||||||
|
|
||||||
|
In order to demote a master we need to shutdown the server and call
|
||||||
|
pg_rewind against the new master to enable a proper timeline
|
||||||
|
switch.
|
||||||
|
"""
|
||||||
|
service.stop_db()
|
||||||
|
self._pg_rewind(service)
|
||||||
|
service.restart()
|
@ -83,7 +83,7 @@ def _decode_output(output):
|
|||||||
|
|
||||||
|
|
||||||
def run_container(client, image, name, network_mode="host", volumes={},
|
def run_container(client, image, name, network_mode="host", volumes={},
|
||||||
command=""):
|
command="", user=""):
|
||||||
"""Run command in a container and return the string output list.
|
"""Run command in a container and return the string output list.
|
||||||
|
|
||||||
:returns output: The log output.
|
:returns output: The log output.
|
||||||
@ -103,6 +103,7 @@ def run_container(client, image, name, network_mode="host", volumes={},
|
|||||||
volumes=volumes,
|
volumes=volumes,
|
||||||
remove=False,
|
remove=False,
|
||||||
command=command,
|
command=command,
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
except docker.errors.ContainerError as err:
|
except docker.errors.ContainerError as err:
|
||||||
output = err.container.logs()
|
output = err.container.logs()
|
||||||
|
@ -929,7 +929,7 @@ class BaseInstance(SimpleInstance):
|
|||||||
self._server_group_loaded = True
|
self._server_group_loaded = True
|
||||||
return self._server_group
|
return self._server_group
|
||||||
|
|
||||||
def get_injected_files(self, datastore_manager):
|
def get_injected_files(self, datastore_manager, datastore_version):
|
||||||
injected_config_location = CONF.get('injected_config_location')
|
injected_config_location = CONF.get('injected_config_location')
|
||||||
guest_info = CONF.get('guest_info')
|
guest_info = CONF.get('guest_info')
|
||||||
|
|
||||||
@ -946,8 +946,10 @@ class BaseInstance(SimpleInstance):
|
|||||||
"[DEFAULT]\n"
|
"[DEFAULT]\n"
|
||||||
"guest_id=%s\n"
|
"guest_id=%s\n"
|
||||||
"datastore_manager=%s\n"
|
"datastore_manager=%s\n"
|
||||||
|
"datastore_version=%s\n"
|
||||||
"tenant_id=%s\n"
|
"tenant_id=%s\n"
|
||||||
% (self.id, datastore_manager, self.tenant_id)
|
% (self.id, datastore_manager, datastore_version,
|
||||||
|
self.tenant_id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
try:
|
try:
|
||||||
if replica.id != master_candidate.id:
|
if replica.id != master_candidate.id:
|
||||||
replica.detach_replica(old_master, for_failover=True)
|
replica.detach_replica(old_master, for_failover=True)
|
||||||
replica.attach_replica(master_candidate)
|
replica.attach_replica(master_candidate, restart=True)
|
||||||
except exception.TroveError as ex:
|
except exception.TroveError as ex:
|
||||||
log_fmt = ("Unable to migrate replica %(slave)s from "
|
log_fmt = ("Unable to migrate replica %(slave)s from "
|
||||||
"old replica source %(old_master)s to "
|
"old replica source %(old_master)s to "
|
||||||
@ -156,7 +156,7 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
|
|
||||||
# dealing with the old master after all the other replicas
|
# dealing with the old master after all the other replicas
|
||||||
# has been migrated.
|
# has been migrated.
|
||||||
old_master.attach_replica(master_candidate)
|
old_master.attach_replica(master_candidate, restart=False)
|
||||||
try:
|
try:
|
||||||
old_master.demote_replication_master()
|
old_master.demote_replication_master()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
@ -565,7 +565,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
|||||||
networks = self._prepare_networks_for_instance(
|
networks = self._prepare_networks_for_instance(
|
||||||
datastore_manager, nics, access=access
|
datastore_manager, nics, access=access
|
||||||
)
|
)
|
||||||
files = self.get_injected_files(datastore_manager)
|
files = self.get_injected_files(datastore_manager, ds_version)
|
||||||
cinder_volume_type = volume_type or CONF.cinder_volume_type
|
cinder_volume_type = volume_type or CONF.cinder_volume_type
|
||||||
volume_info = self._create_server_volume(
|
volume_info = self._create_server_volume(
|
||||||
flavor['id'], image_id,
|
flavor['id'], image_id,
|
||||||
@ -1165,13 +1165,14 @@ class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin):
|
|||||||
if not for_failover:
|
if not for_failover:
|
||||||
self.reset_task_status()
|
self.reset_task_status()
|
||||||
|
|
||||||
def attach_replica(self, master):
|
def attach_replica(self, master, restart=False):
|
||||||
LOG.info("Attaching replica %s to master %s", self.id, master.id)
|
LOG.info("Attaching replica %s to master %s", self.id, master.id)
|
||||||
try:
|
try:
|
||||||
replica_info = master.guest.get_replica_context()
|
replica_info = master.guest.get_replica_context()
|
||||||
flavor = self.nova_client.flavors.get(self.flavor_id)
|
flavor = self.nova_client.flavors.get(self.flavor_id)
|
||||||
slave_config = self._render_replica_config(flavor).config_contents
|
slave_config = self._render_replica_config(flavor).config_contents
|
||||||
self.guest.attach_replica(replica_info, slave_config)
|
self.guest.attach_replica(replica_info, slave_config,
|
||||||
|
restart=restart)
|
||||||
self.update_db(slave_of_id=master.id)
|
self.update_db(slave_of_id=master.id)
|
||||||
self.slave_list = None
|
self.slave_list = None
|
||||||
except (GuestError, GuestTimeout):
|
except (GuestError, GuestTimeout):
|
||||||
@ -2047,7 +2048,9 @@ class RebuildAction(ResizeActionBase):
|
|||||||
self.wait_status = ['ACTIVE']
|
self.wait_status = ['ACTIVE']
|
||||||
|
|
||||||
def _initiate_nova_action(self):
|
def _initiate_nova_action(self):
|
||||||
files = self.instance.get_injected_files(self.instance.datastore.name)
|
files = self.instance.get_injected_files(
|
||||||
|
self.instance.datastore.name,
|
||||||
|
self.instance.datastore_version.name)
|
||||||
|
|
||||||
LOG.debug(f"Rebuilding Nova server {self.instance.server.id}")
|
LOG.debug(f"Rebuilding Nova server {self.instance.server.id}")
|
||||||
# Before Nova version 2.57, userdata is not supported when doing
|
# Before Nova version 2.57, userdata is not supported when doing
|
||||||
|
@ -214,8 +214,9 @@ wal_level = replica # minimal, replica, or logical
|
|||||||
# open_sync
|
# open_sync
|
||||||
#full_page_writes = on # recover from partial page writes
|
#full_page_writes = on # recover from partial page writes
|
||||||
#wal_compression = off # enable compression of full-page writes
|
#wal_compression = off # enable compression of full-page writes
|
||||||
#wal_log_hints = off # also do full page writes of non-critical updates
|
wal_log_hints = on # also do full page writes of non-critical updates
|
||||||
# (change requires restart)
|
# (change requires restart)
|
||||||
|
# (Trove default)
|
||||||
#wal_init_zero = on # zero-fill new WAL files
|
#wal_init_zero = on # zero-fill new WAL files
|
||||||
#wal_recycle = on # recycle WAL files
|
#wal_recycle = on # recycle WAL files
|
||||||
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
|
#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers
|
||||||
|
@ -94,12 +94,13 @@ class TestManager(trove_testtools.TestCase):
|
|||||||
self.mock_slave1.detach_replica.assert_called_with(
|
self.mock_slave1.detach_replica.assert_called_with(
|
||||||
self.mock_old_master, for_failover=True)
|
self.mock_old_master, for_failover=True)
|
||||||
self.mock_old_master.attach_replica.assert_called_with(
|
self.mock_old_master.attach_replica.assert_called_with(
|
||||||
self.mock_slave1)
|
self.mock_slave1, restart=False)
|
||||||
self.mock_slave1.make_read_only.assert_called_with(False)
|
self.mock_slave1.make_read_only.assert_called_with(False)
|
||||||
|
|
||||||
self.mock_slave2.detach_replica.assert_called_with(
|
self.mock_slave2.detach_replica.assert_called_with(
|
||||||
self.mock_old_master, for_failover=True)
|
self.mock_old_master, for_failover=True)
|
||||||
self.mock_slave2.attach_replica.assert_called_with(self.mock_slave1)
|
self.mock_slave2.attach_replica.assert_called_with(self.mock_slave1,
|
||||||
|
restart=True)
|
||||||
|
|
||||||
self.mock_old_master.demote_replication_master.assert_any_call()
|
self.mock_old_master.demote_replication_master.assert_any_call()
|
||||||
|
|
||||||
|
@ -249,7 +249,7 @@ class FreshInstanceTasksTest(BaseFreshInstanceTasksTest):
|
|||||||
cfg.CONF.set_override('injected_config_location', '/etc/trove/conf.d')
|
cfg.CONF.set_override('injected_config_location', '/etc/trove/conf.d')
|
||||||
|
|
||||||
# execute
|
# execute
|
||||||
files = self.freshinstancetasks.get_injected_files("test")
|
files = self.freshinstancetasks.get_injected_files("test", 'test')
|
||||||
# verify
|
# verify
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
'/etc/trove/conf.d/guest_info.conf' in files)
|
'/etc/trove/conf.d/guest_info.conf' in files)
|
||||||
@ -266,7 +266,7 @@ class FreshInstanceTasksTest(BaseFreshInstanceTasksTest):
|
|||||||
cfg.CONF.set_override('injected_config_location', '/etc')
|
cfg.CONF.set_override('injected_config_location', '/etc')
|
||||||
|
|
||||||
# execute
|
# execute
|
||||||
files = self.freshinstancetasks.get_injected_files("test")
|
files = self.freshinstancetasks.get_injected_files("test", 'test')
|
||||||
# verify
|
# verify
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
'/etc/guest_info' in files)
|
'/etc/guest_info' in files)
|
||||||
@ -920,7 +920,7 @@ class BuiltInstanceTasksTest(trove_testtools.TestCase):
|
|||||||
return_value=replica_config):
|
return_value=replica_config):
|
||||||
self.instance_task.attach_replica(master)
|
self.instance_task.attach_replica(master)
|
||||||
self.instance_task._guest.attach_replica.assert_called_with(
|
self.instance_task._guest.attach_replica.assert_called_with(
|
||||||
replica_context, config_content)
|
replica_context, config_content, restart=False)
|
||||||
mock_update_db.assert_called_with(slave_of_id=master.id)
|
mock_update_db.assert_called_with(slave_of_id=master.id)
|
||||||
|
|
||||||
@patch('trove.taskmanager.models.LOG')
|
@patch('trove.taskmanager.models.LOG')
|
||||||
|
Loading…
Reference in New Issue
Block a user