freezer/freezer/engine/nova/nova.py

277 lines
11 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 freezer.common import client_manager
from freezer.engine import engine
from freezer.utils import utils
import json
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.server_info = None
@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 restore_nova_tenant(self, project_id, hostname_backup_name,
overwrite, recent_to_date):
# Load info about tenant instances in swift
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 in ['local', 'ssh']:
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()
instance_ids = json.loads(data)
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()
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.nova.networks.findall()
net_names = [network for network, _ in
available_networks.iteritems()]
match_networks = [{"net-id": network.id} for network in
nova_networks
if network.to_dict().get('label') 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
)
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 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)
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 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 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,
}
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())