diff --git a/doc/README.rst b/doc/README.rst index 8fe528d9..b59adfa9 100644 --- a/doc/README.rst +++ b/doc/README.rst @@ -930,6 +930,7 @@ Available options:: usage: freezer-agent [-h] [--action ACTION] [--always-level ALWAYS_LEVEL] [--backup-name BACKUP_NAME] [--cinder-vol-id CINDER_VOL_ID] + [--cinder-vol-name CINDER_VOL_NAME] [--cindernative-vol-id CINDERNATIVE_VOL_ID] [--command COMMAND] [--compression COMPRESSION] [--config CONFIG] [--config-dir DIR] [--config-file PATH] @@ -955,6 +956,7 @@ Available options:: [--nodry-run] [--noinsecure] [--nooverwrite] [--noquiet] [--nouse-syslog] [--nouse-syslog-rfc-format] [--nova-inst-id NOVA_INST_ID] [--noverbose] + [--nova-inst-name NOVA_INST_NAME] [--nowatch-log-file] [--os-identity-api-version OS_IDENTITY_API_VERSION] [--overwrite] [--path-to-backup PATH_TO_BACKUP] @@ -993,6 +995,8 @@ optional arguments: backup on Swift --cinder-vol-id CINDER_VOL_ID Id of cinder volume for backup + --cinder-vol-name CINDER_VOL_NAME + Name of cinder volume for backup --cindernative-vol-id CINDERNATIVE_VOL_ID Id of cinder volume for native backup --command COMMAND Command executed by exec action @@ -1126,6 +1130,8 @@ optional arguments: The inverse of --use-syslog-rfc-format --nova-inst-id NOVA_INST_ID Id of nova instance for backup + --nova-inst-name NOVA_INST_NAME + Name of nova instance for backup --noverbose The inverse of --verbose --nowatch-log-file The inverse of --watch-log-file --os-identity-api-version OS_IDENTITY_API_VERSION, --os_auth_ver OS_IDENTITY_API_VERSION diff --git a/doc/source/cli/freezer-agent.rst b/doc/source/cli/freezer-agent.rst index 95c431a0..ad9ecfdc 100644 --- a/doc/source/cli/freezer-agent.rst +++ b/doc/source/cli/freezer-agent.rst @@ -566,6 +566,14 @@ OPTIONS Id of cinder volume for backup +.. oslo.config:option:: cinder_vol_name + + :Type: string + :Default: + + Name of cinder volume for backup + + .. oslo.config:option:: cinderbrick_vol_id :Type: string @@ -598,6 +606,14 @@ OPTIONS Id of nova instance for backup +.. oslo.config:option:: nova_inst_name + + :Type: string + :Default: + + Name of nova instance for backup + + .. oslo.config:option:: project_id :Type: string diff --git a/freezer/common/config.py b/freezer/common/config.py index f045cc5a..56babce9 100644 --- a/freezer/common/config.py +++ b/freezer/common/config.py @@ -69,7 +69,9 @@ DEFAULT_PARAMS = { 'encrypt_pass_file': None, 'volume': None, 'proxy': None, 'cinder_vol_id': '', 'cindernative_vol_id': '', 'cinderbrick_vol_id': '', + 'cinder_vol_name': '', 'nova_inst_id': '', '__version__': FREEZER_VERSION, + 'nova_inst_name': '', 'remove_older_than': None, 'restore_from_date': None, 'upload_limit': -1, 'always_level': False, 'version': None, 'dry_run': False, 'lvm_snapsize': DEFAULT_LVM_SNAPSIZE, @@ -375,6 +377,11 @@ _COMMON = [ default=DEFAULT_PARAMS['cinder_vol_id'], help="Id of cinder volume for backup" ), + cfg.StrOpt('cinder-vol-name', + dest='cinder_vol_name', + default=DEFAULT_PARAMS['cinder_vol_name'], + help="Name of cinder volume for backup" + ), cfg.StrOpt('cinderbrick-vol-id', dest='cinderbrick_vol_id', default=DEFAULT_PARAMS['cinderbrick_vol_id'], @@ -395,6 +402,12 @@ _COMMON = [ default=DEFAULT_PARAMS['nova_inst_id'], help="Id of nova instance for backup" ), + cfg.StrOpt('nova-inst-name', + dest='nova_inst_name', + default=DEFAULT_PARAMS['nova_inst_name'], + help="Name of nova instance for backup" + ), + cfg.StrOpt('project-id', dest='project_id', default=DEFAULT_PARAMS['project_id'], @@ -653,12 +666,13 @@ def get_backup_args(): backup_args.path_to_backup[:3] backup_media = 'fs' - if backup_args.cinder_vol_id: + if backup_args.cinder_vol_id or backup_args.cinder_vol_name: backup_media = 'cinder' elif backup_args.cindernative_vol_id or backup_args.cindernative_backup_id: backup_media = 'cindernative' elif backup_args.engine_name == 'nova' and (backup_args.project_id or - backup_args.nova_inst_id): + backup_args.nova_inst_id or + backup_args.nova_inst_name): backup_media = 'nova' elif backup_args.cinderbrick_vol_id: backup_media = 'cinderbrick' diff --git a/freezer/job.py b/freezer/job.py index d343989e..f4ad9601 100644 --- a/freezer/job.py +++ b/freezer/job.py @@ -15,6 +15,7 @@ import abc +from concurrent import futures import datetime import os import sys @@ -25,6 +26,7 @@ from oslo_log import log from oslo_utils import importutils import six +from freezer.common import client_manager from freezer.openstack import admin from freezer.openstack import backup from freezer.openstack import restore @@ -48,8 +50,21 @@ class Job(object): self.conf = conf_dict self.storage = storage self.engine = conf_dict.engine + self.client = client_manager.get_client_manager(CONF) + self.nova = self.client.get_nova() + self.cinder = self.client.get_cinder() self._general_validation() self._validate() + if self.conf.nova_inst_name: + self.nova_instance_ids = [server.id for server in + self.nova.servers.list(detailed=False) + if server.name == + self.conf.nova_inst_name] + if self.conf.cinder_vol_name: + self.cinder_vol_ids = [volume.id for volume in + self.cinder.volumes.list() + if volume.name == + self.conf.cinder_inst_name] @abc.abstractmethod def _validate(self): @@ -124,13 +139,15 @@ class BackupJob(Job): if not self.conf.no_incremental: raise ValueError("Incremental nova backup is not supported") - if not self.conf.nova_inst_id and not self.conf.project_id: - raise ValueError("nova-inst-id or project-id" + if not self.conf.nova_inst_id and not self.conf.project_id \ + and not self.conf.nova_inst_name: + raise ValueError("nova-inst-id or project-id or nova-inst-name" " argument must be provided") if self.conf.mode == 'cinder': - if not self.conf.cinder_vol_id: - raise ValueError("cinder-vol-id argument must be provided") + if not self.conf.cinder_vol_id and not self.conf.cinder_vol_name: + raise ValueError("cinder-vol-id or cinder-vol-name argument " + "must be provided") if self.conf.mode == "cindernative": if not self.conf.cindernative_vol_id: @@ -266,9 +283,11 @@ class BackupJob(Job): max_level=self.conf.max_level, always_level=self.conf.always_level, restart_always_level=self.conf.restart_always_level) - else: + + elif self.conf.nova_inst_id: LOG.info('Executing nova backup. Instance ID: {0}'.format( self.conf.nova_inst_id)) + hostname_backup_name = os.path.join( self.conf.hostname_backup_name, self.conf.nova_inst_id) @@ -280,6 +299,25 @@ class BackupJob(Job): always_level=self.conf.always_level, restart_always_level=self.conf.restart_always_level) + else: + executor = futures.ThreadPoolExecutor( + max_workers=len(self.nova_instance_ids)) + futures_list = [] + for instance_id in self.nova_instance_ids: + hostname_backup_name = os.path.join( + self.conf.hostname_backup_name, instance_id) + futures_list.append(executor.submit( + self.engine.backup( + backup_resource=instance_id, + hostname_backup_name=hostname_backup_name, + no_incremental=self.conf.no_incremental, + max_level=self.conf.max_level, + always_level=self.conf.always_level, + restart_always_level=self.conf.restart_always_level + ))) + + futures.wait(futures_list, CONF.timeout) + elif backup_media == 'cindernative': LOG.info('Executing cinder native backup. Volume ID: {0}, ' 'incremental: {1}'.format(self.conf.cindernative_vol_id, @@ -288,9 +326,22 @@ class BackupJob(Job): name=self.conf.backup_name, incremental=self.conf.incremental) elif backup_media == 'cinder': - LOG.info('Executing cinder snapshot. Volume ID: {0}'.format( - self.conf.cinder_vol_id)) - backup_os.backup_cinder_by_glance(self.conf.cinder_vol_id) + if self.conf.cinder_vol_id: + LOG.info('Executing cinder snapshot. Volume ID: {0}'.format( + self.conf.cinder_vol_id)) + backup_os.backup_cinder_by_glance(self.conf.cinder_vol_id) + else: + executor = futures.ThreadPoolExecutor( + max_workers=len(self.cinder_vol_ids)) + futures_list = [] + for instance_id in self.cinder_vol_ids: + LOG.info('Executing cinder snapshot. Volume ID:' + ' {0}'.format(instance_id)) + futures_list.append(executor.submit( + backup_os.backup_cinder_by_glance(instance_id))) + + futures.wait(futures_list, CONF.timeout) + elif backup_media == 'cinderbrick': LOG.info('Executing cinder volume backup using os-brick. ' 'Volume ID: {0}'.format(self.conf.cinderbrick_vol_id)) @@ -311,7 +362,9 @@ class RestoreJob(Job): def _validate(self): if not any([self.conf.restore_abs_path, self.conf.nova_inst_id, + self.conf.nova_inst_name, self.conf.cinder_vol_id, + self.conf.cinder_vol_name, self.conf.cindernative_vol_id, self.conf.cinderbrick_vol_id, self.conf.project_id]): @@ -366,7 +419,8 @@ class RestoreJob(Job): hostname_backup_name=self.conf.hostname_backup_name, overwrite=conf.overwrite, recent_to_date=restore_timestamp) - else: + + elif conf.nova_inst_id: LOG.info("Restoring nova backup. Instance ID: {0}, " "timestamp: {1} network-id {2}".format( conf.nova_inst_id, @@ -382,11 +436,36 @@ class RestoreJob(Job): recent_to_date=restore_timestamp, backup_media=conf.mode) + else: + for instance_id in self.nova_instance_ids: + LOG.info("Restoring nova backup. Instance ID: {0}, " + "timestamp: {1} network-id {2}".format( + instance_id, restore_timestamp, + conf.nova_restore_network)) + hostname_backup_name = os.path.join( + self.conf.hostname_backup_name, instance_id) + self.engine.restore( + hostname_backup_name=hostname_backup_name, + restore_resource=instance_id, + overwrite=conf.overwrite, + recent_to_date=restore_timestamp, + backup_media=conf.mode) + elif conf.backup_media == 'cinder': - LOG.info("Restoring cinder backup from glance. Volume ID: {0}, " - "timestamp: {1}".format(conf.cinder_vol_id, - restore_timestamp)) - res.restore_cinder_by_glance(conf.cinder_vol_id, restore_timestamp) + if conf.cinder_vol_id: + LOG.info("Restoring cinder backup from glance. " + "Volume ID: {0}, timestamp: {1}".format( + conf.cinder_vol_id, + restore_timestamp)) + res.restore_cinder_by_glance(conf.cinder_vol_id, + restore_timestamp) + else: + for instance_id in self.cinder_vol_ids: + LOG.info("Restoring cinder backup from glance. " + "Volume ID: {0}, timestamp: {1}".format( + instance_id, restore_timestamp)) + res.restore_cinder_by_glance(instance_id, + restore_timestamp) elif conf.backup_media == 'cindernative': LOG.info("Restoring cinder native backup. Volume ID {0}, Backup ID" " {1}, timestamp: {2}".format(conf.cindernative_vol_id, @@ -440,13 +519,24 @@ class AdminJob(Job): timestamp = int(time.mktime(timestamp.timetuple())) if self.conf.backup_media == 'cinder': - old_backups = self.get_cinder_old_backups( - timestamp, - self.conf.cinder_vol_id - ) - self.remove_backup_dirs(old_backups, self.conf.cinder_vol_id) - return {} + if self.conf.cinder_vol_id: + old_backups = self.get_cinder_old_backups( + timestamp, + self.conf.cinder_vol_id + ) + self.remove_backup_dirs(old_backups, + self.conf.cinder_vol_id) + return {} + else: + for instance_id in self.cinder_vol_ids: + old_backups = self.get_cinder_old_backups( + timestamp, + instance_id + ) + self.remove_backup_dirs(old_backups, + instance_id) + return {} hostname_backup_name_set = set() if self.conf.backup_media == 'nova': @@ -457,11 +547,19 @@ class AdminJob(Job): hostname_backup_name = os.path.join( self.conf.hostname_backup_name, instance_id) hostname_backup_name_set.add(hostname_backup_name) - else: + + elif self.conf.nova_inst_id: hostname_backup_name = os.path.join( self.conf.hostname_backup_name, self.conf.nova_inst_id) hostname_backup_name_set.add(hostname_backup_name) + + else: + for instance_id in self.nova_instance_ids: + hostname_backup_name = os.path.join( + self.conf.hostname_backup_name, + instance_id) + hostname_backup_name_set.add(hostname_backup_name) else: hostname_backup_name_set.add(self.conf.hostname_backup_name) diff --git a/freezer/tests/commons.py b/freezer/tests/commons.py index a2083517..658a8147 100644 --- a/freezer/tests/commons.py +++ b/freezer/tests/commons.py @@ -336,9 +336,11 @@ class BackupOpt1(object): self.download_limit = -1 self.sql_server_instance = 'Sql Server' self.cinder_vol_id = '' + self.cinder_vol_name = '' self.cindernative_vol_id = '' self.cindernative_backup_id = '' self.nova_inst_id = '' + self.nova_inst_name = '' self.lvm_snapperm = 'ro' self.compression = 'gzip' diff --git a/freezer/tests/unit/engines/nova/test_nova.py b/freezer/tests/unit/engines/nova/test_nova.py index 193cef2d..512e2463 100644 --- a/freezer/tests/unit/engines/nova/test_nova.py +++ b/freezer/tests/unit/engines/nova/test_nova.py @@ -45,7 +45,13 @@ class TestNovaEngine(commons.FreezerBaseTestCase): super(TestNovaEngine, self).setUp() self.backup_opt = commons.BackupOpt1() self.project_id = "test-project-id" + self.instance_names = ["instance-1", "instance-2", "instance-3"] + if self.instance_names: + self.instance_ids = [server for server in + self.instance_names + if server == self.backup_opt.nova_inst_name] self.instance_ids = ["instance-id-1", "instance-id-2", "instance-id-3"] + self.instance_ids_str = json.dumps(self.instance_ids) servers_list = [FakeServer(instance_id) for instance_id in self.instance_ids]