438 lines
15 KiB
Python
438 lines
15 KiB
Python
#Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
|
|
#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.
|
|
|
|
from mock import Mock, MagicMock, patch, ANY
|
|
from webob.exc import HTTPNotFound
|
|
|
|
import hashlib
|
|
import os
|
|
import testtools
|
|
|
|
from trove.common import utils
|
|
from trove.common.context import TroveContext
|
|
from trove.conductor import api as conductor_api
|
|
from trove.guestagent.common import operating_system
|
|
from trove.guestagent.strategies.backup import mysql_impl
|
|
from trove.guestagent.strategies.backup import couchbase_impl
|
|
from trove.guestagent.strategies.restore.base import RestoreRunner
|
|
from trove.backup.state import BackupState
|
|
from trove.guestagent.backup import backupagent
|
|
from trove.guestagent.strategies.backup.base import BackupRunner
|
|
from trove.guestagent.strategies.backup.base import UnknownBackupType
|
|
from trove.guestagent.strategies.storage.base import Storage
|
|
|
|
conductor_api.API.get_client = Mock()
|
|
conductor_api.API.update_backup = Mock()
|
|
|
|
|
|
def create_fake_data():
|
|
from random import choice
|
|
from string import ascii_letters
|
|
|
|
return ''.join([choice(ascii_letters) for _ in xrange(1024)])
|
|
|
|
|
|
class MockBackup(BackupRunner):
|
|
"""Create a large temporary file to 'backup' with subprocess."""
|
|
|
|
backup_type = 'mock_backup'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.data = create_fake_data()
|
|
self.cmd = 'echo %s' % self.data
|
|
super(MockBackup, self).__init__(*args, **kwargs)
|
|
|
|
def cmd(self):
|
|
return self.cmd
|
|
|
|
|
|
class MockCheckProcessBackup(MockBackup):
|
|
"""Backup runner that fails confirming the process."""
|
|
|
|
def check_process(self):
|
|
return False
|
|
|
|
|
|
class MockLossyBackup(MockBackup):
|
|
"""Fake Incomplete writes to swift."""
|
|
|
|
def read(self, *args):
|
|
results = super(MockLossyBackup, self).read(*args)
|
|
if results:
|
|
# strip a few chars from the stream
|
|
return results[20:]
|
|
|
|
|
|
class MockSwift(object):
|
|
"""Store files in String."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.store = ''
|
|
self.containers = []
|
|
self.container = "database_backups"
|
|
self.url = 'http://mockswift/v1'
|
|
self.etag = hashlib.md5()
|
|
|
|
def put_container(self, container):
|
|
if container not in self.containers:
|
|
self.containers.append(container)
|
|
return None
|
|
|
|
def put_object(self, container, obj, contents, **kwargs):
|
|
if container not in self.containers:
|
|
raise HTTPNotFound
|
|
while True:
|
|
if not hasattr(contents, 'read'):
|
|
break
|
|
content = contents.read(2 ** 16)
|
|
if not content:
|
|
break
|
|
self.store += content
|
|
self.etag.update(self.store)
|
|
return self.etag.hexdigest()
|
|
|
|
def save(self, filename, stream):
|
|
location = '%s/%s/%s' % (self.url, self.container, filename)
|
|
return True, 'w00t', 'fake-checksum', location
|
|
|
|
def load(self, context, storage_url, container, filename, backup_checksum):
|
|
pass
|
|
|
|
def load_metadata(self, location, checksum):
|
|
return {}
|
|
|
|
def save_metadata(self, location, metadata):
|
|
pass
|
|
|
|
|
|
class MockStorage(Storage):
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
return self
|
|
|
|
def load(self, location, backup_checksum):
|
|
pass
|
|
|
|
def save(self, filename, stream):
|
|
pass
|
|
|
|
def load_metadata(self, location, checksum):
|
|
return {}
|
|
|
|
def save_metadata(self, location, metadata={}):
|
|
pass
|
|
|
|
def is_enabled(self):
|
|
return True
|
|
|
|
|
|
class MockRestoreRunner(RestoreRunner):
|
|
def __init__(self, storage, **kwargs):
|
|
pass
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
pass
|
|
|
|
def restore(self):
|
|
pass
|
|
|
|
def is_zipped(self):
|
|
return False
|
|
|
|
|
|
class MockStats:
|
|
f_blocks = 1024 ** 2
|
|
f_bsize = 4096
|
|
f_bfree = 512 * 1024
|
|
|
|
|
|
class BackupAgentTest(testtools.TestCase):
|
|
def setUp(self):
|
|
super(BackupAgentTest, self).setUp()
|
|
mysql_impl.get_auth_password = MagicMock(return_value='123')
|
|
backupagent.get_storage_strategy = MagicMock(return_value=MockSwift)
|
|
os.statvfs = MagicMock(return_value=MockStats)
|
|
self.orig_utils_execute_with_timeout = utils.execute_with_timeout
|
|
self.orig_os_get_ip_address = operating_system.get_ip_address
|
|
|
|
def tearDown(self):
|
|
super(BackupAgentTest, self).tearDown()
|
|
utils.execute_with_timeout = self.orig_utils_execute_with_timeout
|
|
operating_system.get_ip_address = self.orig_os_get_ip_address
|
|
|
|
def test_backup_impl_MySQLDump(self):
|
|
"""This test is for
|
|
guestagent/strategies/backup/impl
|
|
"""
|
|
mysql_dump = mysql_impl.MySQLDump(
|
|
'abc', extra_opts='')
|
|
self.assertIsNotNone(mysql_dump.cmd)
|
|
str_mysql_dump_cmd = ('mysqldump'
|
|
' --all-databases'
|
|
' %(extra_opts)s'
|
|
' --opt'
|
|
' --password=123'
|
|
' -u os_admin'
|
|
' 2>/tmp/mysqldump.log'
|
|
' | gzip |'
|
|
' openssl enc -aes-256-cbc -salt '
|
|
'-pass pass:default_aes_cbc_key')
|
|
self.assertEqual(mysql_dump.cmd, str_mysql_dump_cmd)
|
|
self.assertIsNotNone(mysql_dump.manifest)
|
|
self.assertEqual(mysql_dump.manifest, 'abc.gz.enc')
|
|
|
|
def test_backup_impl_InnoBackupEx(self):
|
|
"""This test is for
|
|
guestagent/strategies/backup/impl
|
|
"""
|
|
inno_backup_ex = mysql_impl.InnoBackupEx('innobackupex', extra_opts='')
|
|
self.assertIsNotNone(inno_backup_ex.cmd)
|
|
str_innobackup_cmd = ('sudo innobackupex'
|
|
' --stream=xbstream'
|
|
' %(extra_opts)s'
|
|
' /var/lib/mysql 2>/tmp/innobackupex.log'
|
|
' | gzip |'
|
|
' openssl enc -aes-256-cbc -salt '
|
|
'-pass pass:default_aes_cbc_key')
|
|
self.assertEqual(inno_backup_ex.cmd, str_innobackup_cmd)
|
|
self.assertIsNotNone(inno_backup_ex.manifest)
|
|
str_innobackup_manifest = 'innobackupex.xbstream.gz.enc'
|
|
self.assertEqual(inno_backup_ex.manifest, str_innobackup_manifest)
|
|
|
|
def test_backup_impl_CbBackup(self):
|
|
operating_system.get_ip_address = Mock(return_value="1.1.1.1")
|
|
utils.execute_with_timeout = Mock(return_value=None)
|
|
cbbackup = couchbase_impl.CbBackup('cbbackup', extra_opts='')
|
|
self.assertIsNotNone(cbbackup)
|
|
str_cbbackup_cmd = ("tar cpPf - /tmp/backups | "
|
|
"gzip | openssl enc -aes-256-cbc -salt -pass "
|
|
"pass:default_aes_cbc_key")
|
|
self.assertEqual(str_cbbackup_cmd, cbbackup.cmd)
|
|
self.assertIsNotNone(cbbackup.manifest)
|
|
self.assertIn('gz.enc', cbbackup.manifest)
|
|
|
|
def test_backup_base(self):
|
|
"""This test is for
|
|
guestagent/strategies/backup/base
|
|
"""
|
|
BackupRunner.cmd = "%s"
|
|
backup_runner = BackupRunner('sample', cmd='echo command')
|
|
if backup_runner.is_zipped:
|
|
self.assertEqual(backup_runner.zip_manifest, '.gz')
|
|
self.assertIsNotNone(backup_runner.zip_manifest)
|
|
self.assertIsNotNone(backup_runner.zip_cmd)
|
|
self.assertEqual(backup_runner.zip_cmd, ' | gzip')
|
|
else:
|
|
self.assertIsNone(backup_runner.zip_manifest)
|
|
self.assertIsNone(backup_runner.zip_cmd)
|
|
self.assertEqual(backup_runner.backup_type, 'BackupRunner')
|
|
|
|
def test_execute_backup(self):
|
|
"""This test should ensure backup agent
|
|
ensures that backup and storage is not running
|
|
resolves backup instance
|
|
starts backup
|
|
starts storage
|
|
reports status
|
|
"""
|
|
agent = backupagent.BackupAgent()
|
|
backup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'InnoBackupEx',
|
|
'checksum': 'fake-checksum',
|
|
'datastore': 'mysql',
|
|
'datastore_version': '5.5'
|
|
}
|
|
agent.execute_backup(context=None, backup_info=backup_info,
|
|
runner=MockBackup)
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
state=BackupState.NEW))
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
size=ANY,
|
|
state=BackupState.BUILDING))
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
checksum=ANY,
|
|
location=ANY,
|
|
note=ANY,
|
|
backup_type=backup_info['type'],
|
|
state=BackupState.COMPLETED))
|
|
|
|
def test_execute_bad_process_backup(self):
|
|
agent = backupagent.BackupAgent()
|
|
backup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'InnoBackupEx',
|
|
'checksum': 'fake-checksum',
|
|
'datastore': 'mysql',
|
|
'datastore_version': '5.5'
|
|
}
|
|
|
|
self.assertRaises(backupagent.BackupError, agent.execute_backup,
|
|
context=None, backup_info=backup_info,
|
|
runner=MockCheckProcessBackup)
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
state=BackupState.NEW))
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
size=ANY,
|
|
state=BackupState.BUILDING))
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
checksum=ANY,
|
|
location=ANY,
|
|
note=ANY,
|
|
backup_type=backup_info['type'],
|
|
state=BackupState.FAILED))
|
|
|
|
def test_execute_lossy_backup(self):
|
|
"""This test verifies that incomplete writes to swift will fail."""
|
|
with patch.object(MockSwift, 'save',
|
|
return_value=(False, 'Error', 'y', 'z')):
|
|
|
|
agent = backupagent.BackupAgent()
|
|
|
|
backup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'InnoBackupEx',
|
|
'checksum': 'fake-checksum',
|
|
}
|
|
|
|
self.assertRaises(backupagent.BackupError, agent.execute_backup,
|
|
context=None, backup_info=backup_info,
|
|
runner=MockLossyBackup)
|
|
|
|
self.assertTrue(
|
|
conductor_api.API.update_backup.called_once_with(
|
|
ANY,
|
|
backup_id=backup_info['id'],
|
|
state=BackupState.FAILED))
|
|
|
|
def test_execute_restore(self):
|
|
"""This test should ensure backup agent
|
|
resolves backup instance
|
|
determines backup/restore type
|
|
transfers/downloads data and invokes the restore module
|
|
reports status
|
|
"""
|
|
with patch.object(backupagent, 'get_storage_strategy',
|
|
return_value=MockStorage):
|
|
|
|
with patch.object(backupagent, 'get_restore_strategy',
|
|
return_value=MockRestoreRunner):
|
|
|
|
agent = backupagent.BackupAgent()
|
|
|
|
bkup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'InnoBackupEx',
|
|
'checksum': 'fake-checksum',
|
|
}
|
|
agent.execute_restore(TroveContext(),
|
|
bkup_info,
|
|
'/var/lib/mysql')
|
|
|
|
def test_restore_unknown(self):
|
|
with patch.object(backupagent, 'get_restore_strategy',
|
|
side_effect=ImportError):
|
|
|
|
agent = backupagent.BackupAgent()
|
|
|
|
bkup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'foo',
|
|
'checksum': 'fake-checksum',
|
|
}
|
|
|
|
self.assertRaises(UnknownBackupType, agent.execute_restore,
|
|
context=None, backup_info=bkup_info,
|
|
restore_location='/var/lib/mysql')
|
|
|
|
def test_backup_incremental_metadata(self):
|
|
with patch.object(backupagent, 'get_storage_strategy',
|
|
return_value=MockSwift):
|
|
MockStorage.save_metadata = Mock()
|
|
with patch.object(MockSwift, 'load_metadata',
|
|
return_value={'lsn': '54321'}):
|
|
meta = {
|
|
'lsn': '12345',
|
|
'parent_location': 'fake',
|
|
'parent_checksum': 'md5',
|
|
}
|
|
|
|
mysql_impl.InnoBackupExIncremental.metadata = MagicMock(
|
|
return_value=meta)
|
|
mysql_impl.InnoBackupExIncremental.run = MagicMock(
|
|
return_value=True)
|
|
mysql_impl.InnoBackupExIncremental.__exit__ = MagicMock(
|
|
return_value=True)
|
|
|
|
agent = backupagent.BackupAgent()
|
|
|
|
bkup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'InnoBackupEx',
|
|
'checksum': 'fake-checksum',
|
|
'parent': {'location': 'fake', 'checksum': 'md5'}
|
|
}
|
|
|
|
agent.execute_backup(TroveContext(),
|
|
bkup_info,
|
|
'/var/lib/mysql')
|
|
|
|
self.assertTrue(MockStorage.save_metadata.called_once_with(
|
|
ANY,
|
|
meta))
|
|
|
|
def test_backup_incremental_bad_metadata(self):
|
|
with patch.object(backupagent, 'get_storage_strategy',
|
|
return_value=MockSwift):
|
|
|
|
agent = backupagent.BackupAgent()
|
|
|
|
bkup_info = {'id': '123',
|
|
'location': 'fake-location',
|
|
'type': 'InnoBackupEx',
|
|
'checksum': 'fake-checksum',
|
|
'parent': {'location': 'fake', 'checksum': 'md5'}
|
|
}
|
|
|
|
self.assertRaises(
|
|
AttributeError,
|
|
agent.execute_backup, TroveContext(), bkup_info, 'location')
|