Guest User

Untitled

a guest
Apr 30th, 2019
137
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 36.30 KB | None | 0 0
  1. import argparse
  2. import base64
  3. from collections import defaultdict
  4. from distutils.version import LooseVersion
  5. from datetime import datetime
  6. import json
  7. import logging
  8. from operator import attrgetter
  9. import os
  10. import sys
  11. import time
  12.  
  13. from cinderclient import client
  14. from cinderclient import v2
  15. from cinderclient import __version__ as cinder_version
  16.  
  17. from keystoneauth1 import loading
  18. from keystoneauth1 import session
  19.  
  20.  
  21. __version__ = '0.1'
  22.  
  23.  
  24. # Available script actions
  25. BACKUP = 'backup'
  26. RESTORE = 'restore'
  27. LIST = 'list'
  28. EXPORT = 'export'
  29. IMPORT = 'import'
  30.  
  31. _LI = _LW = _LE = _LC = _LX = None
  32. DEFAULT_LOG_LEVEL = logging.INFO
  33.  
  34.  
  35. def get_arg_parser():
  36.     """Create parser with script options."""
  37.  
  38.     class MyParser(argparse.ArgumentParser):
  39.         def error(self, message):
  40.             self.print_help()
  41.             sys.stderr.write('\nerror: %s\n' % message)
  42.             sys.exit(2)
  43.  
  44.     general_description = (
  45.         "Cinder auto backup management tool\n\n"
  46.         "This is a helper for OpenStack's Cinder backup functionality to help "
  47.         "create and restore automatic backups, with rotation, as well as "
  48.         "export and import backup metadata.\n\n"
  49.         "Metadata for backup volumes is stored in the DB and if this is lost, "
  50.         "Cinder won't be able to restore volumes from backups. So it is "
  51.         "recommended to always export your backup metadata and keep it "
  52.         "safe.\n\n"
  53.         "Currently Cinder can only backup available volumes, so for in-use "
  54.         "volumes this helper will create a temporary snapshot of the volume, "
  55.         "create a temporary volume from that snapshot and create the backup "
  56.         "from that snapshot. This means that if we look in Cinder backup the "
  57.         "volume id will not match the originator. This helper will show "
  58.         "original volume id on list. Once Cinder supports backup from snapshot"
  59.         " the volume creation step will be removed.\n\n"
  60.         "This tool does not support incremental backups since its behavior is "
  61.         "not consistent between all drivers at this moment.\n\n"
  62.         "Cinder backup by default doesn't restore volume name and description,"
  63.         " but this helper does.")
  64.  
  65.     general_epilog = (
  66.         "Use {action} -h to see specific action help\n\n"
  67.         "*Basic usage:*\n"
  68.         "Create backups for all our volumes using credentials from environment"
  69.         " and keep only the previous backup (delete older ones):\n"
  70.         "\tcinderback.py backup --keep-only 2\n"
  71.         "Restore latests backups for all our volumes using credentials from "
  72.         "environment:\n"
  73.         "\tcinderback.py restore\n"
  74.         "List existing automatic backups:\n"
  75.         "\tcinderback.py list\n"
  76.         "\n*Advanced usage:*\n"
  77.         "As administrator create backups of all tenants volumes, export "
  78.         "metadata and hide backups from tenants:\n"
  79.         "\tcinderback.py --all-tenants backup --forget-tenants "
  80.         "--export-metadata ./backup.metadata\n"
  81.         "As administrator import metadata and restore all backups created by "
  82.         "us (if we created volumes for other tenants they will also be "
  83.         "restored) to their original ids (volumes with those ids must "
  84.         "exist):\n"
  85.         "\tcinderback.py restore --restore-id --import-metadata "
  86.         "./backup.metadata\n"
  87.         "As administrator import newest backups for every volume (created by "
  88.         "us or by tenants):\n"
  89.         "\tcinderback.py --all-tenants restore\n"
  90.         "Restore only 1 specific automatic backup using the backup id (used "
  91.         "for non last backups):\n"
  92.         "\tcinderback.py restore --backup_id $backup_uuid\n"
  93.         "Restore only latest automatic backup for specific volume:\n"
  94.         "\tcinderback.py restore --volume-id $volume_id\n"
  95.         "List existing backups from all tenants:\n"
  96.         "\tcinderback.py --all-tenants list\n")
  97.  
  98.     parser = MyParser(description=general_description,
  99.                       epilog=general_epilog, version=__version__,
  100.                       add_help=True,
  101.                       formatter_class=argparse.RawTextHelpFormatter)
  102.  
  103.     # Common arguments to all actions
  104.     parser.add_argument('-a', '--all-tenants', dest='all_tenants',
  105.                         action='store_true', default=False,
  106.                         help='include volumes/backups from all tenants, needs '
  107.                         'cinderclient v1.1.1 (default only from supplied '
  108.                         'tenant)')
  109.     parser.add_argument('--os-username', metavar='<auth-user-name>',
  110.                         dest='username',
  111.                         default=os.environ.get('OS_USERNAME', ''),
  112.                         help='OpenStack user name. Default=env[OS_USERNAME]')
  113.     parser.add_argument('--os-password', metavar='<auth-password>',
  114.                         dest='password',
  115.                         default=os.environ.get('OS_PASSWORD', ''),
  116.                         help='Password for OpenStack user. '
  117.                         'Default=env[OS_PASSWORD]')
  118.     parser.add_argument('--os-tenant-name', metavar='<auth-tenant-name>',
  119.                         dest='tenant_name',
  120.                         default=os.environ.get('OS_TENANT_NAME', ''),
  121.                         help='Tenant name. Default=env[OS_TENANT_NAME]')
  122.     parser.add_argument('--os-auth-url', metavar='<auth-url>', dest='auth_url',
  123.                         default=os.environ.get('OS_AUTH_URL', ''),
  124.                         help='URL for the authentication service. '
  125.                              'Default=env[OS_AUTH_URL]')
  126.     parser.add_argument('-q', '--quiet', dest='quiet',
  127.                         default=False, action='store_true',
  128.                         help='No output except warnings or errors')
  129.  
  130.     # Subparser for available actions
  131.     subparsers = parser.add_subparsers(title='actions', dest='action',
  132.                                        help='action to perform')
  133.  
  134.     # Metadata export action
  135.     parser_export = subparsers.add_parser(EXPORT,
  136.                                           help='export backups metadata')
  137.     parser_export.add_argument('filename', metavar='<FILENAME>',
  138.                                help='file to export to')
  139.  
  140.     # Metadata import action
  141.     parser_import = subparsers.add_parser(IMPORT,
  142.                                           help='import backups metadata')
  143.     parser_import.add_argument('filename',  metavar='<FILENAME>',
  144.                                help='file to import from')
  145.  
  146.     # Backups list action
  147.     subparsers.add_parser(LIST, help='list available automatic backups')
  148.  
  149.     # Keep tenants argument is common to backup and restore
  150.     forget_tenants = dict(dest='keep_tenants', action='store_false',
  151.                           default=True, help="don't make backups available to "
  152.                           "original tenant (default available)")
  153.  
  154.     # Timeout argument is common to backup and restore
  155.     timeout = dict(dest='max_secs_gbi', type=int, default=300,
  156.                    help='maximum expected time in seconds to transfer each '
  157.                    'GB, for timeout purposes. Backup/Restored Volume  will be '
  158.                    'deleted if it timeouts (default 5 minutes)')
  159.  
  160.     # Backup action
  161.     parser_backup = subparsers.add_parser(BACKUP, help='do backups')
  162.     parser_backup.add_argument('--export-metadata', dest='filename',
  163.                                default=None,  metavar='<FILENAME>',
  164.                                help='export all auto backup metadata to file')
  165.     parser_backup.add_argument('--forget-tenants', **forget_tenants)
  166.     parser_backup.add_argument('--timeout-gb', **timeout)
  167.     parser_backup.add_argument('--keep-only', dest='keep_only', default=0,
  168.                                metavar='<#>',
  169.                                type=int, help='backup rotation, how many '
  170.                                               'automatic backups to keep, '
  171.                                               'oldest ones will be deleted '
  172.                                               '(default keep all)')
  173.  
  174.     # Restore action
  175.     parser_restore = subparsers.add_parser(RESTORE, help='restore backups',
  176.                                            epilog='Restore last backup')
  177.  
  178.     parser_restore.add_argument('--timeout-gb', **timeout)
  179.     parser_restore.add_argument('--forget-tenants', **forget_tenants)
  180.     parser_restore.add_argument('--restore-id',
  181.                                 dest='restore_id',
  182.                                 default=False, action='store_true',
  183.                                 help='restore backup to the original '
  184.                                      'volume id, it must exist in cinder '
  185.                                      '(default create new id)')
  186.     parser_restore.add_argument('--forget-data',
  187.                                 dest='restore_data',
  188.                                 default=True, action='store_false',
  189.                                 help="don't restore volume's name and "
  190.                                      "description (default restore)")
  191.     parser_restore.add_argument('--import-metadata', dest='filename',
  192.                                 default=None, metavar='<FILENAME>',
  193.                                 help='import auto backup metadata from file')
  194.  
  195.     parser_restore_id = parser_restore.add_mutually_exclusive_group()
  196.     parser_restore_id.add_argument('--volume-id',
  197.                                    dest='volume_id',
  198.                                    default=None,
  199.                                    help='specific volume to restore')
  200.     parser_restore_id.add_argument('--backup-id',
  201.                                    dest='backup_id',
  202.                                    default=None,
  203.                                    help='specific backup to restore')
  204.     return parser
  205.  
  206.  
  207. def create_logger(quiet=False):
  208.     global _LI, _LW, _LE, _LC, _LX
  209.  
  210.     logger = logging.getLogger(__name__)
  211.  
  212.     logger.setLevel(logging.WARNING if quiet else DEFAULT_LOG_LEVEL)
  213.  
  214.     # create console handler and set level
  215.     ch = logging.StreamHandler()
  216.     ch.setLevel(DEFAULT_LOG_LEVEL)
  217.  
  218.     # create formatter for ch
  219.     formatter = logging.Formatter('%(levelname)s: %(message)s')
  220.     ch.setFormatter(formatter)
  221.  
  222.     # add ch to logger
  223.     logger.addHandler(ch)
  224.  
  225.     _LI = logger.info
  226.     _LW = logger.warning
  227.     _LE = logger.error
  228.     _LC = logger.critical
  229.     _LX = logger.exception
  230.  
  231.     return logger
  232.  
  233.  
  234. class BackupInfo(object):
  235.     """Representation of volume information to store in backup description."""
  236.  
  237.     @staticmethod
  238.     def _from_b64_json(data):
  239.         """Convert from base 64 json data to Python objects."""
  240.         return json.loads(base64.b64decode(data))
  241.  
  242.     def __init__(self, data):
  243.         """Accept string, backup or volume classes."""
  244.         # For strings we assume it's the __repr__ value
  245.         if isinstance(data, str):
  246.             self.__dict__ = self._from_b64_json(data)
  247.  
  248.         # If it's a backup we extract information from the description
  249.         elif isinstance(data, v2.volume_backups.VolumeBackup):
  250.             self.__dict__ = self._from_b64_json(data.description)
  251.  
  252.         # If it's a volume we store relevant volume information.
  253.         # At this point in time it's only id, tenant, name and description.
  254.         elif isinstance(data, v2.volumes.Volume):
  255.             self.id = data.id
  256.             self.owner_tenant_id = getattr(data,
  257.                                            'os-vol-tenant-attr:tenant_id')
  258.             self.name = data.name
  259.             self.description = data.description
  260.  
  261.         # We don't know how to treat additional types
  262.         else:
  263.             raise ValueError('data argument is of unknown class %s',
  264.                              type(data))
  265.  
  266.     def __repr__(self):
  267.         """Base 64 encodejson representation of instance."""
  268.         return base64.b64encode(json.dumps(self.__dict__))
  269.  
  270.  
  271. class BackupServiceException(Exception):
  272.     def __init__(self, what, *args, **kwargs):
  273.         super(BackupServiceException, self).__init__(*args, **kwargs)
  274.         self.what = what
  275.  
  276.     def __str__(self):
  277.         return u'%s: %s' % (self.__class__.__name__, self.what)
  278.  
  279.  
  280. class UnexpectedStatus(BackupServiceException):
  281.     def __init__(self, what, intermediate='', final='', *args, **kwargs):
  282.         super(UnexpectedStatus, self).__init__(what, *args, **kwargs)
  283.         self.intermediate = intermediate
  284.         self.final = final
  285.  
  286.     def __str__(self):
  287.         if self.intermediate or self.final:
  288.             steps = (' [intermediate: %s, final: %s]' %
  289.                      (self.intermediate, self.final))
  290.         else:
  291.             steps = ''
  292.         return (u'%s: Status is %s%s' %
  293.                 (self.__class__.__name__, self.what.status, steps))
  294.  
  295.  
  296. class TimeoutError(BackupServiceException):
  297.     pass
  298.  
  299.  
  300. class BackupIsDown(BackupServiceException):
  301.     pass
  302.  
  303.  
  304. class BackupService(object):
  305.     """Backup creation and restoration class."""
  306.  
  307.     # Poll interval in seconds when creating or destroying resources.
  308.     default_poll_deplay = 10
  309.     WANT_V = '1.1.1'
  310.     HAS_SEARCH_OPTS = LooseVersion(cinder_version) >= LooseVersion(WANT_V)
  311.  
  312.     def __init__(self, project_id,
  313.                  poll_delay=None, name_prefix='auto_backup_',
  314.                  max_secs_gbi=None):
  315.         super(BackupService, self).__init__()
  316.         self.loader = loading.get_plugin_loader('password')
  317.         self.password = 'k3st0ne2017ZEtka'
  318.         self.user_domain_name = 'default'
  319.  
  320.         self.username = 'admin'
  321.         self.project_id = project_id
  322.  
  323.         self.auth_url = 'http://10.10.33.140:35357/v3'
  324.         self.auth = self.loader.load_from_options(auth_url=self.auth_url,username=self.username, password=self.password,
  325.                 project_id= self.project_id, user_domain_name=self.user_domain_name)
  326.         self.sess = session.Session(auth=self.auth)
  327.         self.poll_delay = poll_delay or self.default_poll_deplay
  328.         self.name_prefix = name_prefix
  329.  
  330.         # Some functionality requires API version 2
  331.         self.client = client.Client('2', session=self.sess)
  332.  
  333.         self.status_msg = ''
  334.         self.max_secs_gbi = max_secs_gbi or 300
  335.  
  336.         if not self.HAS_SEARCH_OPTS:
  337.             _LW('--all-tenants disabled, need cinderclient v%s', self.WANT_V)
  338.  
  339.     @property
  340.     def backup_status(self):
  341.         """On error this may have additional information."""
  342.         return self.status_msg
  343.  
  344.     @property
  345.     def is_up(self):
  346.         """Check whether backup service is up and running or not.
  347.        If we are not allowed to check it we assume it's always up."""
  348.         # Get services list
  349.         try:
  350.             services = self.client.services.list()
  351.  
  352.         # If policy doesn't allow us to check we'll have to assume it's there
  353.         except client.exceptions.Forbidden:
  354.             return True
  355.  
  356.         # Search for cinder backup service
  357.         for service in services:
  358.             if service.binary == 'cinder-backup':
  359.                 # Must be up
  360.                 if service.state != 'up':
  361.                     self.status_msg = service.state
  362.                     return False
  363.                 # And enabled
  364.                 if service.status != 'enabled':
  365.                     self.status_msg = service.status
  366.                     if service.disabled_reason:
  367.                         self.status_msg += ' (%s)' % service.disabled_reason
  368.                     return False
  369.                 return True
  370.  
  371.         # If we can't even find it in services list it's not loaded
  372.         self.status_msg = "Not loaded"
  373.         return False
  374.  
  375.     def _list_arguments(self, all_tenants):
  376.         if self.HAS_SEARCH_OPTS:
  377.             return {'search_opts': {'all_tenants': all_tenants}}
  378.  
  379.         return {}
  380.  
  381.     def backup_all(self, all_tenants=True, keep_tenant=True, keep_only=0):
  382.         """Creates backup for all visible volumes.
  383.        :all_tenants: Backup volumes for all tenants, not only ourselves.
  384.        :param keep_tenant: If we want owners to see automatic backups of their
  385.                            volumes. Only relevant when using all_tenants=True
  386.        :param keep_only: Amount of backups to keep including the new one.
  387.                          Older ones will be deleted.
  388.        :return: ([successful_backup_object], [failed_volume_object])
  389.        """
  390.         backups = []
  391.         failed = []
  392.  
  393.         # Get visible volumes
  394.         volumes = self.client.volumes.list(**self._list_arguments(all_tenants))
  395.  
  396.         # Get existing backups
  397.         existing_backups = self.existing_backups(all_tenants=all_tenants)
  398.  
  399.         _LI('Starting Volumes Backup')
  400.         for vol in volumes:
  401.             _LI('Processing %dGB from volume %s (id: %s)', vol.size, vol.name,
  402.                 vol.id)
  403.             backup_name = self.name_prefix + vol.id
  404.  
  405.             # See owner tenant and check if it's us
  406.             owner_tenant_id = getattr(vol, 'os-vol-tenant-attr:tenant_id')
  407.  
  408.             # If we want to keep the tenant, get the right client
  409.             tenant_client = self.get_client(owner_tenant_id, keep_tenant)
  410.  
  411.             # Do the backup
  412.             try:
  413.                 backup = self.backup_volume(vol, name=backup_name,
  414.                                             client=tenant_client)
  415.             except BackupIsDown:
  416.                 raise
  417.             except TimeoutError:
  418.                 _LE('Timeout on backup')
  419.                 failed.append(vol)
  420.                 backup = None
  421.             except UnexpectedStatus:
  422.                 failed.append(vol)
  423.             except client.exceptions.OverLimit as exc:
  424.                 _LE('Error while doing backup %s', exc)
  425.                 failed.append(vol)
  426.                 break
  427.             except Exception:
  428.                 _LX('Exception while doing backup')
  429.                 failed.append(vol)
  430.                 backup = None
  431.  
  432.             # On success
  433.             else:
  434.                 backups.append(backup)
  435.                 existing_backups[vol.id].append(backup)
  436.                 # If we limit the number of backups and we have too many
  437.                 # backups for this volume
  438.                 if (keep_only and len(existing_backups.get(vol.id, tuple())) >
  439.                         keep_only):
  440.                     remove = len(existing_backups[vol.id]) - keep_only
  441.                     # We may have to remove multiple backups and we remove the
  442.                     # oldest ones, which are the first on the list.
  443.                     for __ in xrange(remove):
  444.                         back = existing_backups[vol.id].pop(0)
  445.                         _LI('Removing old backup %s from %s', back.id,
  446.                             back.created_at_dt)
  447.                         self._delete_resource(back, need_up=True)
  448.             _LI('Backup completed')
  449.         _LI('Finished with backups')
  450.         return (backups, failed)
  451.  
  452.     def _wait_for(self, resource, allowed_states, expected_states=None,
  453.                   need_up=False):
  454.         """Waits for a resource to come to a specific state.
  455.        :param resource: Resource we want to wait for
  456.        :param allowed_states: iterator with allowed intermediary states
  457.        :param expected_states: states we expect to have at the end, if None
  458.                                is supplied then anything is good.
  459.        :param need_up: If wee need backup service to be up and running
  460.        :return: The most updated resource
  461.        """
  462.         deadline = time.time() + (self.max_secs_gbi * resource.size)
  463.         while resource.status in allowed_states:
  464.             time.sleep(self.poll_delay)
  465.             if need_up and not self.is_up:
  466.                 raise BackupIsDown(what=resource)
  467.             if deadline <= time.time():
  468.                 raise TimeoutError(what=resource)
  469.             resource = resource.manager.get(resource.id)
  470.  
  471.         if expected_states and resource.status not in expected_states:
  472.             raise UnexpectedStatus(what=resource, intermediate=allowed_states,
  473.                                    final=expected_states)
  474.  
  475.         return resource
  476.  
  477.     def _delete_resource(self, resource, wait=True, need_up=False,
  478.                          raise_on_exc=True):
  479.         # Snapshots and volumes, may be used with backups if need_up=True
  480.         if not resource:
  481.             return
  482.  
  483.         try:
  484.             resource.delete()
  485.             if wait:
  486.                 self._wait_for(resource, ('deleting',), need_up=need_up)
  487.  
  488.         # If it doesn't exist we consider it "deleted"
  489.         except client.exceptions.NotFound:
  490.             pass
  491.         except Exception as exc:
  492.             if raise_on_exc:
  493.                 raise
  494.             _LI('Error cleaning up %(id)s: %(exc)s.', {'id': resource.id,
  495.                                                        'exc': exc})
  496.  
  497.     def _create_and_wait(self, msg, module, arguments, resources=tuple()):
  498.         """Creates a resource and waits for completion, with optional cleanup
  499.        on error.
  500.        :param msg: Message to display on start
  501.        :param module: Module to create resource from
  502.        :param arguments: Arguments for resource creation
  503.        :param resources: Allocated resources that must be cleaned up on error
  504.        :return: Created resource
  505.        """
  506.         def _cleanup(new_resource):
  507.             self._delete_resource(new_resource, raise_on_exc=False)
  508.             for res in resources:
  509.                 self._delete_resource(res, raise_on_exc=False)
  510.  
  511.         _LI(msg)
  512.         result = None
  513.         try:
  514.             result = module.create(**arguments)
  515.             result = self._wait_for(result, ('creating',), 'available', True)
  516.         except:
  517.             _cleanup(result)
  518.             raise
  519.  
  520.         return result
  521.  
  522.     def backup_volume(self, volume, name=None, client=None):
  523.         """Backup a volume using a volume object or it's id.
  524.        :param volume: Volume object or volume id as a string.
  525.        :param name: Name for the backup
  526.        :param client: If we want ot use a specific client instead of this
  527.                       instance's client. Useful when creating backups for
  528.                       other tenants.
  529.        :return: Backup object
  530.        """
  531.         if isinstance(volume, str):
  532.             # TODO: This could fail
  533.             volume = self.client.volumes.get(volume)
  534.  
  535.         # Use given client or instance's client
  536.         client = client or self.client
  537.         name = name or self.name_prefix + volume.id
  538.  
  539.         # Use encoded original volume info as description
  540.         description = BackupInfo(volume)
  541.  
  542.         if volume.status == 'in-use':
  543.             _LI('Volume online so this is a multi-step process')
  544.  
  545.             # Force snapshot since volume it's in-use
  546.             snapshot = self._create_and_wait(
  547.                 'Creating snapshot', client.volume_snapshots,
  548.                 arguments=dict(
  549.                     volume_id=volume.id, force=True, name='tmp ' + name,
  550.                     description='Temporary snapshot for backup',
  551.                     metadata=volume.metadata))
  552.  
  553.             # Create temporary volume from snapshot
  554.             tmp_vol = self._create_and_wait(
  555.                 'Creating temp volume from snapshot', client.volumes,
  556.                 arguments=dict(
  557.                     size=snapshot.size, snapshot_id=snapshot.id,
  558.                     name='tmp '+name,
  559.                     description='Temporary volume for backup',
  560.                     metadata=volume.metadata), resources=(snapshot,))
  561.  
  562.             # Backup temporary volume
  563.             backup = self._create_and_wait(
  564.                 'Doing the actual backup', client.backups,
  565.                 arguments=dict(
  566.                     volume_id=tmp_vol.id, name=name, container=None,
  567.                     description=str(description)),
  568.                 resources=(snapshot, tmp_vol))
  569.  
  570.             # Cleanup temporary resources
  571.             _LI('Deleting temporary volume and snapshot')
  572.             tmp_vol.delete()
  573.             snapshot.delete()
  574.  
  575.         elif volume.status == 'available':
  576.             backup = self._create_and_wait(
  577.                 'Creating direct backup', client.backups,
  578.                 arguments=dict(
  579.                     volume_id=volume.id, name=name, container=None,
  580.                     description=str(description)))
  581.  
  582.         else:
  583.             _LE("We don't backup volume because status is %s", volume.status)
  584.             raise UnexpectedStatus(what=volume)
  585.  
  586.         return backup
  587.  
  588.     def _is_auto_backup(self, backup):
  589.         """Check if a backup was created by us."""
  590.         # It must have the right prefix
  591.         if not backup.name or not backup.name.startswith(self.name_prefix):
  592.             return False
  593.  
  594.         # And description must contain json formatted data base64 encoded
  595.         try:
  596.             BackupInfo(backup)
  597.         except ValueError:
  598.             return False
  599.         return True
  600.  
  601.     def existing_backups(self, all_tenants=True):
  602.         """Retrieve existing backups and return a defaultdict with backups
  603.        grouped by original volume id."""
  604.         # Get list of backups from Cinder Backup service
  605.         backups = self.client.backups.list(**self._list_arguments(all_tenants))
  606.  
  607.         # Leave only automatic backups based on the backup name
  608.         backups = filter(self._is_auto_backup, backups)
  609.  
  610.         # Dictionary of volumes with the list of backups for each one
  611.         volumes = defaultdict(list)
  612.         for backup in backups:
  613.             backup.created_at_dt = datetime.strptime(backup.created_at,
  614.                                                      '%Y-%m-%dT%H:%M:%S.%f')
  615.             volumes[backup.name[len(self.name_prefix):]].append(backup)
  616.  
  617.         # Order the backups for each volume oldest first
  618.         for volume in volumes.itervalues():
  619.             volume.sort(key=attrgetter('created_at_dt'))
  620.  
  621.         return volumes
  622.  
  623.     def _restore_and_wait(self, client, backup_id, new_volume_id):
  624.         # Restore the backup
  625.         restore = client.restores.restore(backup_id=backup_id,
  626.                                           volume_id=new_volume_id)
  627.  
  628.         volume = client.volumes.get(restore.volume_id)
  629.         result = self._wait_for(volume, ('restoring-backup',), 'available',
  630.                                 True)
  631.         return result
  632.  
  633.     def restore_volume(self, backup, keep_tenant, restore_id, restore_data):
  634.         """Restore a specific backup
  635.        :param backup: Backup object to restore
  636.        :param keep_tenant: If we want to restore original tenant
  637.        :param restore_id: If we want to restore to the original volume id
  638.        :param restore_data: Restore original volume name and description
  639.        :return: None
  640.        """
  641.         # Decode original volume information from backup object's description
  642.         backup_info = BackupInfo(backup)
  643.  
  644.         # If we want to restore the id the volume must exist
  645.         if restore_id:
  646.             try:
  647.                 volume = self.client.volumes.get(backup_info.id)
  648.                 if volume.status != 'available':
  649.                     _LW('Skipping, cannot restore to a non-available volume')
  650.                     return
  651.             except client.exceptions.NotFound:
  652.                 _LW("Skipping, destination id doesn't exist")
  653.                 return
  654.             except client.exceptions.ClientException as e:
  655.                 _LW('Error when checking volume (%s)', e)
  656.                 return
  657.             new_id = backup_info.id
  658.         # If we don't give a new id one will be auto generated
  659.         else:
  660.             new_id = None
  661.  
  662.         # If we have to restore the tenant we need a different client
  663.         tenant_client = self.get_client(backup_info.owner_tenant_id,
  664.                                         keep_tenant)
  665.  
  666.         # Restore the backup
  667.         restore = self._restore_and_wait(tenant_client, backup.id, new_id)
  668.  
  669.         # Recover volume name and description
  670.         if restore_data:
  671.             restore.update(name=backup_info.name,
  672.                            description=backup_info.description)
  673.         else:
  674.             restore.update(description='auto_restore_' + backup_info.id + '_' +
  675.                            backup_info.name)
  676.  
  677.     def restore_all(self, all_tenants=True, keep_tenant=True, restore_id=False,
  678.                     restore_data=True, volume_id=None, backup_id=None):
  679.         """Restore volumes.
  680.        :param all_tenants: Restore volumes created by any tenant
  681.        :param keep_tenant: Restore the volumes' tenants
  682.        :param restore_id: We want to restore volumes to their original ids
  683.        :param restore_data: We want to restore volume names and descriptions
  684.        :param volume_id: Restore a specific volume id (cannot be used together
  685.                          with backup_id)
  686.        :param backup_id: Restore a specific backup_id (cannot be used together
  687.                          with volume_id)
  688.        """
  689.         _LI('Starting Volumes Restore')
  690.         backups = self.existing_backups(all_tenants=all_tenants)
  691.  
  692.         # If we want to get a specific volume's backup
  693.         if volume_id:
  694.             backup = backups.get(volume_id)
  695.             if not backup:
  696.                 _LE('no backups for volume %s', volume_id)
  697.                 exit(1)
  698.  
  699.             # Fake that this is the only volume with backups
  700.             backups = {volume_id: backups[volume_id]}
  701.  
  702.         # If we want a specific backup
  703.         elif backup_id:
  704.             # Look for it in the volumes
  705.             for vol, backs in backups.iteritems():
  706.                 back = filter(lambda b: b.id == backup_id, backs)
  707.                 # If we find it fake that this is the only backup
  708.                 if back:
  709.                     backups = {vol: [back]}
  710.                     break
  711.             else:
  712.                 _LE("backup doesn't exist")
  713.                 exit(1)
  714.  
  715.         for volume_id in backups:
  716.             # Get the latest backup
  717.             backup = backups[volume_id][-1]
  718.             _LI('Processing %dGB from volume id %s in backup %s', backup.size,
  719.                 volume_id, backup.id)
  720.  
  721.             try:
  722.                 self.restore_volume(backup, keep_tenant=keep_tenant,
  723.                                     restore_id=restore_id,
  724.                                     restore_data=restore_data)
  725.             except BackupIsDown:
  726.                 raise
  727.             except Exception as exc:
  728.                 _LE('Exception while restoring backup: %s', exc)
  729.  
  730.             _LI('Restore completed')
  731.         _LI('Finished with restores')
  732.  
  733.     def export_metadata(self, filename, all_tenants=False):
  734.         """Export backup metadata to a file."""
  735.         _LI('Exporting metadata to %s', filename)
  736.         existing_backs = self.existing_backups(all_tenants=all_tenants)
  737.         # Flatten the lists
  738.         backups = [back for backs in existing_backs.itervalues()
  739.                    for back in backs
  740.                    if back.status not in ('deleting', 'error')]
  741.  
  742.         metadatas = []
  743.         for back in backups:
  744.             try:
  745.                 metadata = self.client.backups.export_record(back.id)
  746.             except Exception as e:
  747.                 _LE('Error getting metadata for backup %(id)s (%(exception)s)',
  748.                     {'id': back.id, 'exception': e})
  749.             else:
  750.                 backup_info = BackupInfo(back)
  751.                 metadatas.append({'metadata': metadata,
  752.                                   'tenant': backup_info.owner_tenant_id})
  753.  
  754.         try:
  755.             with open(filename, 'w') as f:
  756.                 json.dump(metadatas, f)
  757.         except Exception as e:
  758.             _LE('Error saving metadata to %(filename)s (%(exception)s)',
  759.                 {'filename': filename, 'exception': e})
  760.  
  761.     def get_client(self, tenant_id, keep_tenant=True):
  762.         """Return a client for requested tenant"""
  763.         # If we are the original tenant of the volume
  764.         if (not keep_tenant or
  765.                 self.client.client.auth_ref['token']['tenant']['id']
  766.                 == tenant_id):
  767.             return self.client
  768.  
  769.         _LI("Using tenant id %s", tenant_id)
  770.         return client.Client(version=2,
  771.                              username=self.username,
  772.                              api_key=self.api_key,
  773.                              tenant_id=tenant_id,
  774.                              auth_url=self.auth_url)
  775.  
  776.     def import_metadata(self, filename):
  777.         """Import backup metadata to DB from file."""
  778.         _LI('Importing metadata from %s', filename)
  779.         try:
  780.             with open(filename, 'r') as f:
  781.                 records = json.load(f)
  782.         except Exception as e:
  783.             _LE('Error loading from file %(filename)s (%(exception)s)',
  784.                 {'filename': filename, 'exception': e})
  785.             return False
  786.  
  787.         for data in records:
  788.             # Select client to use
  789.             client = self.get_client(data['tenant'])
  790.  
  791.             try:
  792.                 client.backups.import_record(**data['metadata'])
  793.             except Exception as e:
  794.                 _LE('Error importing record %s', data['metadata'])
  795.                 return False
  796.  
  797.         return True
  798.  
  799.     def list_backups(self, all_tenants=False):
  800.         def _separator(separator):
  801.             return (separator * (19+1) +
  802.                     '+' + separator * (1+36+1) +
  803.                     '+' + separator * (1+36+1) +
  804.                     '+' + separator * (1+9+1) +
  805.                     '+' + separator * (1+4+1))
  806.  
  807.         backups = self.existing_backups(all_tenants)
  808.         format = '{:^19s} | {:^36} | {:^36} | {:^9} | {:5}'
  809.         print format.format('Created at', 'Volume ID', 'Backup ID', 'Status',
  810.                             'Size')
  811.         print(_separator('='))
  812.         mid_separator = _separator('-')
  813.         for volume_id in backups:
  814.             for backup in backups[volume_id]:
  815.                 print format.format(str(backup.created_at_dt), volume_id,
  816.                                     backup.id, backup.status, backup.size)
  817.             print mid_separator
  818.  
  819.  
  820. def main(args):
  821.     backup = BackupService(username=args.username,
  822.                            api_key=args.password,
  823.                            project_id=args.tenant_name,
  824.                            auth_url=args.auth_url,
  825.                            max_secs_gbi=getattr(args, 'max_secs_gbi', None))
  826.  
  827.     if not backup.is_up:
  828.         _LC('Cinder Backup is ' + backup.backup_status)
  829.         exit(1)
  830.  
  831.     if args.action == LIST:
  832.         backup.list_backups(all_tenants=args.all_tenants)
  833.  
  834.     elif args.action == EXPORT:
  835.         backup.export_metadata(filename=args.filename,
  836.                                all_tenants=args.all_tenants)
  837.  
  838.     elif args.action == IMPORT:
  839.         backup.import_metadata(filename=args.filename)
  840.  
  841.     elif args.action == BACKUP:
  842.         failed = True
  843.         try:
  844.             __, failed = backup.backup_all(all_tenants=args.all_tenants,
  845.                                            keep_tenant=args.keep_tenants,
  846.                                            keep_only=args.keep_only)
  847.         except BackupIsDown:
  848.             _LC('Cinder Backup is ' + backup.backup_status)
  849.  
  850.         if args.filename:
  851.             backup.export_metadata(filename=args.filename,
  852.                                    all_tenants=args.all_tenants)
  853.  
  854.         if failed:
  855.             exit(1)
  856.  
  857.     else:  # if args.action == RESTORE:
  858.         # TODO look if metadata from other tenants is restored correctly
  859.         # (they can see it)
  860.         if args.filename:
  861.             if not backup.import_metadata(filename=args.filename):
  862.                 return
  863.             # Give it a little time to update the DB
  864.             time.sleep(1)
  865.  
  866.         backup.restore_all(all_tenants=args.all_tenants,
  867.                            keep_tenant=args.keep_tenants,
  868.                            restore_id=args.restore_id,
  869.                            restore_data=args.restore_data,
  870.                            volume_id=args.volume_id,
  871.                            backup_id=args.backup_id)
  872.  
  873. if __name__ == '__main__':
  874.     parser = get_arg_parser()
  875.     args = parser.parse_args()
  876.     __ = create_logger(quiet=args.quiet)
  877.  
  878.     required = {'username': '--os-username or env[OS_USERNAME]',
  879.                 'password': '--os-password or env[OS_PASSWORD]',
  880.                 'tenant_name': '--os-tenant-name or env[OS_TENANT_NAME]',
  881.                 'auth_url': '--os-auth-url or env[OS_AUTH_URL]'}
  882.     missing = {k: v for k, v in required.iteritems()
  883.                if not getattr(args, k, None)}
  884.     if missing:
  885.         _LE('You must provide %s', ', '.join(required.itervalues()))
  886.     else:
  887.         main(args)
Add Comment
Please, Sign In to add comment