Restore mysqldump action

The restore-mysqldump action will read a msyqldump SQL file and restore
database data.

Change-Id: I27fa3f5c88df7dede1cdf15d9e128b4a35391bb1
This commit is contained in:
David Ames 2020-02-18 15:37:21 -08:00
parent 15b4d0bcfd
commit 405c388318
6 changed files with 129 additions and 13 deletions

View File

@ -13,6 +13,14 @@ mysqldump:
description: |
Comma delimited database names to dump. If left unset, all databases
will be dumped.
restore-mysqldump:
description: |
Restore a MySQL dump of database(s).
WARNING This is a desctructive action. It may overwrite existing database(s) data.
params:
dump-file:
type: string
description: Path to the mysqldump file.
cluster-status:
description: |
JSON dump of the cluster schema and status. This action can be used to

View File

@ -59,11 +59,10 @@ def mysqldump(args):
:rtype: None
:action param basedir: Base directory to dump the db(s)
:action param databases: Comma separated string of databases
:action return:
:action return: mysqldump-file
"""
basedir = (ch_core.hookenv.action_get("basedir"))
databases = (ch_core.hookenv.action_get("databases"))
basedir = ch_core.hookenv.action_get("basedir")
databases = ch_core.hookenv.action_get("databases")
try:
with charm.provide_charm_instance() as instance:
filename = instance.mysqldump(basedir, databases=databases)
@ -79,6 +78,43 @@ def mysqldump(args):
ch_core.hookenv.action_fail("mysqldump failed")
def restore_mysqldump(args):
"""Restore a mysqldump backup.
Execute mysqldump of the database(s). The mysqldump action will take
in the databases action parameter. If the databases parameter is unset all
databases will be dumped, otherwise only the named databases will be
dumped. The action will use the basedir action parameter to dump the
database into the base directory.
A successful mysqldump backup will set the action results key,
mysqldump-file, with the full path to the dump file.
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.restore_mysqldump
:returns: This function is called for its side effect
:rtype: None
:action param dump-file: Path to mysqldump file to restore.
:action return:
"""
dump_file = ch_core.hookenv.action_get("dump-file")
try:
with charm.provide_charm_instance() as instance:
instance.restore_mysqldump(dump_file)
ch_core.hookenv.action_set({
"outcome": "Success"}
)
except subprocess.CalledProcessError as e:
ch_core.hookenv.action_set({
"output": e.output,
"return-code": e.returncode,
"traceback": traceback.format_exc()})
ch_core.hookenv.action_fail(
"Restore mysqldump of {} failed"
.format(dump_file))
def cluster_status(args):
"""Display cluster status
@ -154,7 +190,7 @@ def rejoin_instance(args):
:action param address: String address of the instance to be joined
:action return: Dictionary with command output
"""
address = (ch_core.hookenv.action_get("address"))
address = ch_core.hookenv.action_get("address")
with charm.provide_charm_instance() as instance:
output = instance.rejoin_instance(address)
ch_core.hookenv.action_set({
@ -178,8 +214,8 @@ def set_cluster_option(args):
:action param value: String option value
:action return: Dictionary with command output
"""
key = (ch_core.hookenv.action_get("key"))
value = (ch_core.hookenv.action_get("value"))
key = ch_core.hookenv.action_get("key")
value = ch_core.hookenv.action_get("value")
with charm.provide_charm_instance() as instance:
output = instance.set_cluster_option(key, value)
ch_core.hookenv.action_set({
@ -191,6 +227,7 @@ def set_cluster_option(args):
# A dictionary of all the defined actions to callables (which take
# parsed arguments).
ACTIONS = {"mysqldump": mysqldump, "cluster-status": cluster_status,
"restore-mysqldump": restore_mysqldump,
"set-cluster-option": set_cluster_option,
"reboot-cluster-from-complete-outage":
reboot_cluster_from_complete_outage,

View File

@ -0,0 +1 @@
actions.py

View File

@ -1171,6 +1171,19 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
return subprocess.check_output(
cmd, stderr=subprocess.STDOUT)
def write_root_my_cnf(self):
"""Write root my.cnf
:side effect: calls render()
:returns: None
:rtype: None
"""
my_cnf_template = "root-my.cnf"
root_my_cnf = "/root/.my.cnf"
context = {"mysql_passwd": self.mysql_password}
ch_core.templating.render(
my_cnf_template, root_my_cnf, context, perms=0o600)
def mysqldump(self, backup_dir, databases=None):
"""Execute a MySQL dump
@ -1183,6 +1196,11 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
:returns: Path to the mysqldump file
:rtype: str
"""
# In order to enable passwordless use of mysqldump
# write out my.cnf for user root
self.write_root_my_cnf()
# Enable use of my.cnf by setting HOME env variable
os.environ["HOME"] = "/root"
_user = "root"
_delimiter = ","
if not os.path.exists(backup_dir):
@ -1190,7 +1208,6 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
backup_dir, owner="mysql", group="mysql", perms=0o750)
bucmd = ["/usr/bin/mysqldump", "-u", _user,
"-p{}".format(self.mysql_password),
"--triggers", "--routines", "--events",
"--ignore-table=mysql.event"]
if databases is not None:
@ -1212,6 +1229,33 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
subprocess.check_call(gzcmd)
return "{}.gz".format(_filename)
def restore_mysqldump(self, dump_file):
"""Restore a MySQL dump file
:param dump_file: Path to mysqldump file to restored.
:type dump_file: str
:side effect: Calls subprocess.check_call
:raises subprocess.CalledProcessError: If the mysqldump fails
:returns: This function is called for its side effect
:rtype: None
"""
# In order to enable passwordless use of mysql
# write out my.cnf for user root
self.write_root_my_cnf()
# Enable use of my.cnf by setting HOME env variable
os.environ["HOME"] = "/root"
# Gunzip if necessary
if ".gz" in dump_file:
gunzip = ["gunzip", dump_file]
subprocess.check_call(gunzip)
dump_file = dump_file[:-3]
_user = "root"
restore_cmd = ["mysql", "-u", _user]
restore = subprocess.Popen(restore_cmd, stdin=subprocess.PIPE)
with open(dump_file, "rb") as _sql:
restore.communicate(input=_sql.read())
restore.wait()
def cluster_peer_addresses(self):
"""Cluster peer addresses

View File

@ -0,0 +1,4 @@
[client]
# The following password will be sent to all standard MySQL clients
password="{{ mysql_passwd }}"

View File

@ -1050,21 +1050,20 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
_time = "_now_"
_now.strftime.return_value = _time
_path = "/tmp/backup"
_pass = "pass"
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
midbc._get_password = mock.MagicMock()
midbc._get_password.return_value = _pass
midbc.write_root_my_cnf = mock.MagicMock()
# All DBrs
# All DBs
_filename = "{}/mysqldump-all-databases-{}".format(_path, _time)
_calls = [
mock.call(
["/usr/bin/mysqldump", "-u", "root", "-ppass", "--triggers",
["/usr/bin/mysqldump", "-u", "root", "--triggers",
"--routines", "--events", "--ignore-table=mysql.event",
"--result-file", _filename, "--all-databases"]),
mock.call(["/usr/bin/gzip", _filename])]
self.assertEqual(midbc.mysqldump(_path), "{}.gz".format(_filename))
midbc.write_root_my_cnf.assert_called_once()
self.subprocess.check_call.assert_has_calls(_calls)
# One DB
@ -1095,6 +1094,29 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
self.assertEqual(midbc.mysqldump(_path, databases=_dbs),
"{}.gz".format(_filename))
def test_restore_mysqldump(self):
self.patch("builtins.open",
new_callable=mock.mock_open(),
name="_open")
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
midbc.write_root_my_cnf = mock.MagicMock()
_dump_file = "/home/ubuntu/dump.sql.gz"
_restore = mock.MagicMock(name="RESTORE")
_sql = mock.MagicMock()
self._open.return_value = _sql
self.subprocess.Popen.return_value = _restore
midbc.restore_mysqldump(_dump_file)
midbc.write_root_my_cnf.assert_called_once()
self.subprocess.check_call.assert_called_once_with(
["gunzip", _dump_file])
self.subprocess.Popen.assert_called_once_with(
["mysql", "-u", "root"], stdin=self.subprocess.PIPE)
_restore.communicate.assert_called_once_with(
input=_sql.__enter__().read())
def test_set_cluster_option(self):
_name = "theCluster"
_string = "status output"