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:
parent
15b4d0bcfd
commit
405c388318
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
actions.py
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[client]
|
||||
# The following password will be sent to all standard MySQL clients
|
||||
password="{{ mysql_passwd }}"
|
||||
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue