freezer/freezer/engine/nova/nova.py

362 lines
15 KiB
Python

# (c) Copyright 2016 Hewlett-Packard Development Enterprise, 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 concurrent import futures
import os
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils as json
from freezer.common import client_manager
from freezer.engine import engine
from freezer.engine.tar import tar
from freezer.utils import utils
import tempfile
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class NovaEngine(engine.BackupEngine):
def __init__(self, storage, **kwargs):
super(NovaEngine, self).__init__(storage=storage)
self.client = client_manager.get_client_manager(CONF)
self.nova = self.client.create_nova()
self.glance = self.client.create_glance()
self.cinder = self.client.create_cinder()
self.neutron = self.client.create_neutron()
self.encrypt_pass_file = kwargs.get('encrypt_key')
self.exclude = kwargs.get('exclude')
self.server_info = None
self.openssl_path = None
self.compression_algo = 'gzip'
self.is_windows = None
self.dry_run = kwargs.get('dry_run', False)
self.max_segment_size = kwargs.get('max_segment_size')
self.storage = storage
self.dereference_symlink = kwargs.get('symlinks')
@property
def name(self):
return "nova"
def stream_image(self, pipe):
"""Reading bytes from a pipe and converting it to a stream-like"""
try:
while True:
chunk = pipe.recv_bytes()
yield chunk
except EOFError:
pass
def get_nova_tenant(self, project_id):
# Load info about tenant instances
if self.storage._type == 'swift':
swift_connection = self.client.create_swift()
headers, data = swift_connection.get_object(
self.storage.storage_path,
"project_" + project_id)
elif self.storage._type == 's3':
bucket_name, object_name = self.get_storage_info(project_id)
data = self.storage.get_object(
bucket_name=bucket_name,
key=object_name
)['Body'].read()
elif self.storage._type in ['local', 'ssh', 'ftp', 'ftps']:
backup_basepath = os.path.join(self.storage.storage_path,
'project_' + project_id)
with self.storage.open(backup_basepath, 'rb') as backup_file:
data = backup_file.readline()
elif self.storage._type in ['ftp', 'ftps']:
backup_basepath = os.path.join(self.storage.storage_path,
'project_' + project_id)
file = tempfile.NamedTemporaryFile('wb', delete=True)
self.storage.get_file(backup_basepath, file.name)
with open(file.name) as f:
data = f.readline()
LOG.info("get_nova_tenant download {0}".format(data))
return json.loads(data)
def restore_nova_tenant(self, project_id, hostname_backup_name,
overwrite, recent_to_date):
instance_ids = self.get_nova_tenant(project_id)
for instance_id in instance_ids:
LOG.info("Restore nova instance ID: {0} from container {1}".
format(instance_id, self.storage.storage_path))
backup_name = os.path.join(hostname_backup_name,
instance_id)
self.restore(
hostname_backup_name=backup_name,
restore_resource=instance_id,
overwrite=overwrite,
recent_to_date=recent_to_date)
def restore_level(self, restore_resource, read_pipe, backup, except_queue):
try:
metadata = backup.metadata()
if (not self.encrypt_pass_file and
metadata.get("encryption", False)):
raise Exception("Cannot restore encrypted backup without key")
engine_metadata = backup.engine_metadata()
server_info = metadata.get('server', {})
length = int(engine_metadata.get('length'))
available_networks = server_info.get('addresses')
nova_networks = self.neutron.list_networks()['networks']
net_names = [network for network, _ in
available_networks.iteritems()]
match_networks = [{"net-id": network.get('id')} for network in
nova_networks
if network.get('name') in net_names]
stream = self.stream_image(read_pipe)
data = utils.ReSizeStream(stream, length, 1)
image = self.client.create_image(
"Restore: {0}".format(
server_info.get('name', server_info.get('id', None))
),
'bare',
'raw',
data=data
)
if self.encrypt_pass_file:
try:
tmpdir = tempfile.mkdtemp()
except Exception:
LOG.error("Unable to create a tmp directory")
raise
tar_engine = tar.TarEngine(self.compression_algo,
self.dereference_symlink,
self.exclude, self.storage,
self.max_segment_size,
self.encrypt_pass_file,
self.dry_run)
tar_engine.restore_level(tmpdir, read_pipe, backup,
except_queue)
utils.wait_for(
NovaEngine.image_active,
1,
CONF.timeout,
message="Waiting for image to finish uploading {0} and become"
" active".format(image.id),
kwargs={"glance_client": self.glance, "image_id": image.id}
)
server = self.nova.servers.create(
name=server_info.get('name'),
flavor=server_info['flavor']['id'],
image=image.id,
nics=match_networks
)
return server
except Exception as e:
LOG.exception(e)
except_queue.put(e)
raise
def backup_nova_tenant(self, project_id, hostname_backup_name,
no_incremental, max_level, always_level,
restart_always_level):
instance_ids = [server.id for server in
self.nova.servers.list(detailed=False)]
data = json.dumps(instance_ids)
LOG.info("Saving information about instances {0}".format(data))
if self.storage._type == 'swift':
swift_connection = self.client.create_swift()
swift_connection.put_object(self.storage.storage_path,
"project_{0}".format(project_id),
data)
elif self.storage._type == 's3':
bucket_name, object_name = self.get_storage_info(project_id)
self.storage.put_object(
bucket_name=bucket_name,
key=object_name,
data=data
)
elif self.storage._type in ['local', 'ssh']:
backup_basepath = os.path.join(self.storage.storage_path,
"project_" + project_id)
with self.storage.open(backup_basepath, 'wb') as backup_file:
backup_file.write(data)
elif self.storage._type in ['ftp', 'ftps']:
backup_basepath = os.path.join(self.storage.storage_path,
'project_' + project_id)
file = tempfile.NamedTemporaryFile('wb', delete=True)
with open(file.name, 'wb') as f:
f.write(data)
LOG.info("backup_nova_tenant data={0}".format(data))
self.storage.put_file(file.name, backup_basepath)
executor = futures.ThreadPoolExecutor(
max_workers=len(instance_ids))
futures_list = []
for instance_id in instance_ids:
LOG.info("Backup nova instance ID: {0} to container {1}".
format(instance_id, self.storage.storage_path))
backup_name = os.path.join(hostname_backup_name,
instance_id)
futures_list.append(executor.submit(
self.backup,
backup_resource=instance_id,
hostname_backup_name=backup_name,
no_incremental=no_incremental,
max_level=max_level,
always_level=always_level,
restart_always_level=restart_always_level))
futures.wait(futures_list, CONF.timeout)
def get_storage_info(self, project_id):
if self.storage.get_object_prefix() != '':
object_name = "{0}/project_{1}".format(
self.storage.get_object_prefix(),
project_id
)
else:
object_name = "project_{0}".format(project_id)
return self.storage.get_bucket_name(), object_name
def backup_data(self, backup_resource, manifest_path):
server = self.nova.servers.get(backup_resource)
if not server:
raise Exception("Server not found {0}".format(backup_resource))
def instance_finish_task():
server = self.nova.servers.get(backup_resource)
return not server.__dict__['OS-EXT-STS:task_state']
utils.wait_for(
instance_finish_task, 1, CONF.timeout,
message="Waiting for instance {0} to finish {1} to start the "
"snapshot process".format(
backup_resource,
server.__dict__['OS-EXT-STS:task_state']
)
)
image_id = self.nova.servers.create_image(
server,
"snapshot_of_{0}".format(backup_resource)
)
image = self.glance.images.get(image_id)
if not image:
raise Exception(
"Image {0} is not created or can't be found.".format(image_id)
)
# wait a bit for the snapshot to be taken and completely uploaded
# to glance.
utils.wait_for(
NovaEngine.image_active,
1,
100,
message="Waiting for instnace {0} snapshot to become "
"active".format(backup_resource),
kwargs={"glance_client": self.glance, "image_id": image_id}
)
image = self.glance.images.get(image_id)
image_temporary_snapshot_id = None
copied_volume = None
image_info = getattr(server, "image", None)
if image_info is not None and isinstance(image_info, dict):
LOG.info('Image type instance backup')
boot_device_type = "image"
stream = self.client.download_image(image)
else:
LOG.info('Volume or snapshot type instance backup')
boot_device_type = "volume"
image_block_mapping_info = image.get("block_device_mapping")
image_block_mapping = json.loads(image_block_mapping_info) \
if image_block_mapping_info else None
image_temporary_snapshot_id = \
image_block_mapping[0]['snapshot_id'] \
if image_block_mapping else None
copied_volume = self.client.do_copy_volume(
self.cinder.volume_snapshots.get(
image_temporary_snapshot_id))
LOG.debug("Deleting temporary glance image "
"generated by snapshot")
self.glance.images.delete(image.id)
LOG.debug("Creation temporary glance image")
image = self.client.make_glance_image(
copied_volume.id, copied_volume)
LOG.debug("Download temporary glance image {0}".format(image.id))
stream = self.client.download_image(image)
LOG.info("Uploading image to storage path")
headers = {"server_name": server.name,
"flavour_id": str(server.flavor.get('id')),
'length': str(len(stream)),
"boot_device_type": boot_device_type}
self.set_tenant_meta(manifest_path, headers)
for chunk in stream:
yield chunk
if self.encrypt_pass_file:
tar_engine = tar.TarEngine(self.compression_algo,
self.dereference_symlink,
self.exclude, self.storage,
self.max_segment_size,
self.encrypt_pass_file, self.dry_run)
for data_chunk in tar_engine.backup_data('.', manifest_path):
yield data_chunk
if image_temporary_snapshot_id is not None:
LOG.info("Deleting temporary snapshot {0}"
.format(image_temporary_snapshot_id))
self.cinder.volume_snapshots.delete(image_temporary_snapshot_id)
if copied_volume is not None:
LOG.info("Deleting temporary copied volume {0}"
.format(copied_volume.id))
self.cinder.volumes.delete(copied_volume)
LOG.info("Deleting temporary image {0}".format(image.id))
self.glance.images.delete(image.id)
@staticmethod
def image_active(glance_client, image_id):
"""Check if the image is in the active state or not"""
image = glance_client.images.get(image_id)
return image.status == 'active'
def metadata(self, backup_resource):
"""Construct metadata"""
server_info = self.nova.servers.get(backup_resource).to_dict()
return {
"engine_name": self.name,
"server": server_info,
"encryption": bool(self.encrypt_pass_file)
}
def set_tenant_meta(self, path, metadata):
"""push data to the manifest file"""
with open(path, 'wb') as fb:
fb.writelines(json.dumps(metadata))
def get_tenant_meta(self, path):
with open(path, 'rb') as fb:
json.loads(fb.read())