1# Licensed to the Apache Software Foundation (ASF) under one or more 2# contributor license agreements. See the NOTICE file distributed with 3# this work for additional information regarding copyright ownership. 4# The ASF licenses this file to You under the Apache License, Version 2.0 5# (the "License"); you may not use this file except in compliance with 6# the License. You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16from libcloud.utils.py3 import ET 17from libcloud.backup.base import BackupDriver, BackupTarget, BackupTargetJob 18from libcloud.backup.types import BackupTargetType 19from libcloud.backup.types import Provider 20from libcloud.common.dimensiondata import dd_object_to_id 21from libcloud.common.dimensiondata import DimensionDataConnection 22from libcloud.common.dimensiondata import DimensionDataBackupClient 23from libcloud.common.dimensiondata import DimensionDataBackupClientAlert 24from libcloud.common.dimensiondata import DimensionDataBackupClientType 25from libcloud.common.dimensiondata import DimensionDataBackupDetails 26from libcloud.common.dimensiondata import DimensionDataBackupSchedulePolicy 27from libcloud.common.dimensiondata import DimensionDataBackupStoragePolicy 28from libcloud.common.dimensiondata import API_ENDPOINTS, DEFAULT_REGION 29from libcloud.common.dimensiondata import TYPES_URN 30from libcloud.common.dimensiondata import GENERAL_NS, BACKUP_NS 31from libcloud.utils.xml import fixxpath, findtext, findall 32 33# pylint: disable=no-member 34 35DEFAULT_BACKUP_PLAN = 'Advanced' 36 37 38class DimensionDataBackupDriver(BackupDriver): 39 """ 40 DimensionData backup driver. 41 """ 42 43 selected_region = None 44 connectionCls = DimensionDataConnection 45 name = 'Dimension Data Backup' 46 website = 'https://cloud.dimensiondata.com/' 47 type = Provider.DIMENSIONDATA 48 api_version = 1.0 49 50 network_domain_id = None 51 52 def __init__(self, key, secret=None, secure=True, host=None, port=None, 53 api_version=None, region=DEFAULT_REGION, **kwargs): 54 55 if region not in API_ENDPOINTS and host is None: 56 raise ValueError( 57 'Invalid region: %s, no host specified' % (region)) 58 if region is not None: 59 self.selected_region = API_ENDPOINTS[region] 60 61 super(DimensionDataBackupDriver, self).__init__( 62 key=key, secret=secret, 63 secure=secure, host=host, 64 port=port, 65 api_version=api_version, 66 region=region, 67 **kwargs) 68 69 def _ex_connection_class_kwargs(self): 70 """ 71 Add the region to the kwargs before the connection is instantiated 72 """ 73 74 kwargs = super(DimensionDataBackupDriver, 75 self)._ex_connection_class_kwargs() 76 kwargs['region'] = self.selected_region 77 return kwargs 78 79 def get_supported_target_types(self): 80 """ 81 Get a list of backup target types this driver supports 82 83 :return: ``list`` of :class:``BackupTargetType`` 84 """ 85 return [BackupTargetType.VIRTUAL] 86 87 def list_targets(self): 88 """ 89 List all backuptargets 90 91 :rtype: ``list`` of :class:`BackupTarget` 92 """ 93 targets = self._to_targets( 94 self.connection.request_with_orgId_api_2('server/server').object) 95 return targets 96 97 def create_target(self, name, address, 98 type=BackupTargetType.VIRTUAL, extra=None): 99 """ 100 Creates a new backup target 101 102 :param name: Name of the target (not used) 103 :type name: ``str`` 104 105 :param address: The ID of the node in Dimension Data Cloud 106 :type address: ``str`` 107 108 :param type: Backup target type, only Virtual supported 109 :type type: :class:`BackupTargetType` 110 111 :param extra: (optional) Extra attributes (driver specific). 112 :type extra: ``dict`` 113 114 :rtype: Instance of :class:`BackupTarget` 115 """ 116 if extra is not None: 117 service_plan = extra.get('servicePlan', DEFAULT_BACKUP_PLAN) 118 else: 119 service_plan = DEFAULT_BACKUP_PLAN 120 extra = {'servicePlan': service_plan} 121 122 create_node = ET.Element('NewBackup', 123 {'xmlns': BACKUP_NS}) 124 create_node.set('servicePlan', service_plan) 125 126 response = self.connection.request_with_orgId_api_1( 127 'server/%s/backup' % (address), 128 method='POST', 129 data=ET.tostring(create_node)).object 130 131 asset_id = None 132 for info in findall(response, 133 'additionalInformation', 134 GENERAL_NS): 135 if info.get('name') == 'assetId': 136 asset_id = findtext(info, 'value', GENERAL_NS) 137 138 return BackupTarget( 139 id=asset_id, 140 name=name, 141 address=address, 142 type=type, 143 extra=extra, 144 driver=self 145 ) 146 147 def create_target_from_node(self, node, type=BackupTargetType.VIRTUAL, 148 extra=None): 149 """ 150 Creates a new backup target from an existing node 151 152 :param node: The Node to backup 153 :type node: ``Node`` 154 155 :param type: Backup target type (Physical, Virtual, ...). 156 :type type: :class:`BackupTargetType` 157 158 :param extra: (optional) Extra attributes (driver specific). 159 :type extra: ``dict`` 160 161 :rtype: Instance of :class:`BackupTarget` 162 """ 163 return self.create_target(name=node.name, 164 address=node.id, 165 type=BackupTargetType.VIRTUAL, 166 extra=extra) 167 168 def create_target_from_container(self, container, 169 type=BackupTargetType.OBJECT, 170 extra=None): 171 """ 172 Creates a new backup target from an existing storage container 173 174 :param node: The Container to backup 175 :type node: ``Container`` 176 177 :param type: Backup target type (Physical, Virtual, ...). 178 :type type: :class:`BackupTargetType` 179 180 :param extra: (optional) Extra attributes (driver specific). 181 :type extra: ``dict`` 182 183 :rtype: Instance of :class:`BackupTarget` 184 """ 185 return NotImplementedError( 186 'create_target_from_container not supported for this driver') 187 188 def update_target(self, target, name=None, address=None, extra=None): 189 """ 190 Update the properties of a backup target, only changing the serviceplan 191 is supported. 192 193 :param target: Backup target to update 194 :type target: Instance of :class:`BackupTarget` or ``str`` 195 196 :param name: Name of the target 197 :type name: ``str`` 198 199 :param address: Hostname, FQDN, IP, file path etc. 200 :type address: ``str`` 201 202 :param extra: (optional) Extra attributes (driver specific). 203 :type extra: ``dict`` 204 205 :rtype: Instance of :class:`BackupTarget` 206 """ 207 if extra is not None: 208 service_plan = extra.get('servicePlan', DEFAULT_BACKUP_PLAN) 209 else: 210 service_plan = DEFAULT_BACKUP_PLAN 211 request = ET.Element('ModifyBackup', 212 {'xmlns': BACKUP_NS}) 213 request.set('servicePlan', service_plan) 214 server_id = self._target_to_target_address(target) 215 self.connection.request_with_orgId_api_1( 216 'server/%s/backup/modify' % (server_id), 217 method='POST', 218 data=ET.tostring(request)).object 219 if isinstance(target, BackupTarget): 220 target.extra = extra 221 else: 222 target = self.ex_get_target_by_id(server_id) 223 return target 224 225 def delete_target(self, target): 226 """ 227 Delete a backup target 228 229 :param target: Backup target to delete 230 :type target: Instance of :class:`BackupTarget` or ``str`` 231 232 :rtype: ``bool`` 233 """ 234 server_id = self._target_to_target_address(target) 235 response = self.connection.request_with_orgId_api_1( 236 'server/%s/backup?disable' % (server_id), 237 method='GET').object 238 response_code = findtext(response, 'result', GENERAL_NS) 239 return response_code in ['IN_PROGRESS', 'SUCCESS'] 240 241 def list_recovery_points(self, target, start_date=None, end_date=None): 242 """ 243 List the recovery points available for a target 244 245 :param target: Backup target to delete 246 :type target: Instance of :class:`BackupTarget` 247 248 :param start_date: The start date to show jobs between (optional) 249 :type start_date: :class:`datetime.datetime` 250 251 :param end_date: The end date to show jobs between (optional) 252 :type end_date: :class:`datetime.datetime`` 253 254 :rtype: ``list`` of :class:`BackupTargetRecoveryPoint` 255 """ 256 raise NotImplementedError( 257 'list_recovery_points not implemented for this driver') 258 259 def recover_target(self, target, recovery_point, path=None): 260 """ 261 Recover a backup target to a recovery point 262 263 :param target: Backup target to delete 264 :type target: Instance of :class:`BackupTarget` 265 266 :param recovery_point: Backup target with the backup data 267 :type recovery_point: Instance of :class:`BackupTarget` 268 269 :param path: The part of the recovery point to recover (optional) 270 :type path: ``str`` 271 272 :rtype: Instance of :class:`BackupTargetJob` 273 """ 274 raise NotImplementedError( 275 'recover_target not implemented for this driver') 276 277 def recover_target_out_of_place(self, target, recovery_point, 278 recovery_target, path=None): 279 """ 280 Recover a backup target to a recovery point out-of-place 281 282 :param target: Backup target with the backup data 283 :type target: Instance of :class:`BackupTarget` 284 285 :param recovery_point: Backup target with the backup data 286 :type recovery_point: Instance of :class:`BackupTarget` 287 288 :param recovery_target: Backup target with to recover the data to 289 :type recovery_target: Instance of :class:`BackupTarget` 290 291 :param path: The part of the recovery point to recover (optional) 292 :type path: ``str`` 293 294 :rtype: Instance of :class:`BackupTargetJob` 295 """ 296 raise NotImplementedError( 297 'recover_target_out_of_place not implemented for this driver') 298 299 def get_target_job(self, target, id): 300 """ 301 Get a specific backup job by ID 302 303 :param target: Backup target with the backup data 304 :type target: Instance of :class:`BackupTarget` 305 306 :param id: Backup target with the backup data 307 :type id: Instance of :class:`BackupTarget` 308 309 :rtype: :class:`BackupTargetJob` 310 """ 311 jobs = self.list_target_jobs(target) 312 return list(filter(lambda x: x.id == id, jobs))[0] 313 314 def list_target_jobs(self, target): 315 """ 316 List the backup jobs on a target 317 318 :param target: Backup target with the backup data 319 :type target: Instance of :class:`BackupTarget` 320 321 :rtype: ``list`` of :class:`BackupTargetJob` 322 """ 323 raise NotImplementedError( 324 'list_target_jobs not implemented for this driver') 325 326 def create_target_job(self, target, extra=None): 327 """ 328 Create a new backup job on a target 329 330 :param target: Backup target with the backup data 331 :type target: Instance of :class:`BackupTarget` 332 333 :param extra: (optional) Extra attributes (driver specific). 334 :type extra: ``dict`` 335 336 :rtype: Instance of :class:`BackupTargetJob` 337 """ 338 raise NotImplementedError( 339 'create_target_job not implemented for this driver') 340 341 def resume_target_job(self, target, job): 342 """ 343 Resume a suspended backup job on a target 344 345 :param target: Backup target with the backup data 346 :type target: Instance of :class:`BackupTarget` 347 348 :param job: Backup target job to resume 349 :type job: Instance of :class:`BackupTargetJob` 350 351 :rtype: ``bool`` 352 """ 353 raise NotImplementedError( 354 'resume_target_job not implemented for this driver') 355 356 def suspend_target_job(self, target, job): 357 """ 358 Suspend a running backup job on a target 359 360 :param target: Backup target with the backup data 361 :type target: Instance of :class:`BackupTarget` 362 363 :param job: Backup target job to suspend 364 :type job: Instance of :class:`BackupTargetJob` 365 366 :rtype: ``bool`` 367 """ 368 raise NotImplementedError( 369 'suspend_target_job not implemented for this driver') 370 371 def cancel_target_job(self, job, ex_client=None, ex_target=None): 372 """ 373 Cancel a backup job on a target 374 375 :param job: Backup target job to cancel. If it is ``None`` 376 ex_client and ex_target must be set 377 :type job: Instance of :class:`BackupTargetJob` or ``None`` 378 379 :param ex_client: Client of the job to cancel. 380 Not necessary if job is specified. 381 DimensionData only has 1 job per client 382 :type ex_client: Instance of :class:`DimensionDataBackupClient` 383 or ``str`` 384 385 :param ex_target: Target to cancel a job from. 386 Not necessary if job is specified. 387 :type ex_target: Instance of :class:`BackupTarget` or ``str`` 388 389 :rtype: ``bool`` 390 """ 391 if job is None: 392 if ex_client is None or ex_target is None: 393 raise ValueError("Either job or ex_client and " 394 "ex_target have to be set") 395 server_id = self._target_to_target_address(ex_target) 396 client_id = self._client_to_client_id(ex_client) 397 else: 398 server_id = job.target.address 399 client_id = job.extra['clientId'] 400 401 response = self.connection.request_with_orgId_api_1( 402 'server/%s/backup/client/%s?cancelJob' % (server_id, 403 client_id), 404 method='GET').object 405 response_code = findtext(response, 'result', GENERAL_NS) 406 return response_code in ['IN_PROGRESS', 'SUCCESS'] 407 408 def ex_get_target_by_id(self, id): 409 """ 410 Get a target by server id 411 412 :param id: The id of the target you want to get 413 :type id: ``str`` 414 415 :rtype: :class:`BackupTarget` 416 """ 417 node = self.connection.request_with_orgId_api_2( 418 'server/server/%s' % id).object 419 return self._to_target(node) 420 421 def ex_add_client_to_target(self, target, client_type, storage_policy, 422 schedule_policy, trigger, email): 423 """ 424 Add a client to a target 425 426 :param target: Backup target with the backup data 427 :type target: Instance of :class:`BackupTarget` or ``str`` 428 429 :param client: Client to add to the target 430 :type client: Instance of :class:`DimensionDataBackupClientType` 431 or ``str`` 432 433 :param storage_policy: The storage policy for the client 434 :type storage_policy: Instance of 435 :class:`DimensionDataBackupStoragePolicy` 436 or ``str`` 437 438 :param schedule_policy: The schedule policy for the client 439 :type schedule_policy: Instance of 440 :class:`DimensionDataBackupSchedulePolicy` 441 or ``str`` 442 443 :param trigger: The notify trigger for the client 444 :type trigger: ``str`` 445 446 :param email: The notify email for the client 447 :type email: ``str`` 448 449 :rtype: ``bool`` 450 """ 451 server_id = self._target_to_target_address(target) 452 453 backup_elm = ET.Element('NewBackupClient', 454 {'xmlns': BACKUP_NS}) 455 if isinstance(client_type, DimensionDataBackupClientType): 456 ET.SubElement(backup_elm, "type").text = client_type.type 457 else: 458 ET.SubElement(backup_elm, "type").text = client_type 459 460 if isinstance(storage_policy, DimensionDataBackupStoragePolicy): 461 ET.SubElement(backup_elm, 462 "storagePolicyName").text = storage_policy.name 463 else: 464 ET.SubElement(backup_elm, 465 "storagePolicyName").text = storage_policy 466 467 if isinstance(schedule_policy, DimensionDataBackupSchedulePolicy): 468 ET.SubElement(backup_elm, 469 "schedulePolicyName").text = schedule_policy.name 470 else: 471 ET.SubElement(backup_elm, 472 "schedulePolicyName").text = schedule_policy 473 474 alerting_elm = ET.SubElement(backup_elm, "alerting") 475 alerting_elm.set('trigger', trigger) 476 ET.SubElement(alerting_elm, "emailAddress").text = email 477 478 response = self.connection.request_with_orgId_api_1( 479 'server/%s/backup/client' % (server_id), 480 method='POST', 481 data=ET.tostring(backup_elm)).object 482 response_code = findtext(response, 'result', GENERAL_NS) 483 return response_code in ['IN_PROGRESS', 'SUCCESS'] 484 485 def ex_remove_client_from_target(self, target, backup_client): 486 """ 487 Removes a client from a backup target 488 489 :param target: The backup target to remove the client from 490 :type target: :class:`BackupTarget` or ``str`` 491 492 :param backup_client: The backup client to remove 493 :type backup_client: :class:`DimensionDataBackupClient` or ``str`` 494 495 :rtype: ``bool`` 496 """ 497 server_id = self._target_to_target_address(target) 498 client_id = self._client_to_client_id(backup_client) 499 response = self.connection.request_with_orgId_api_1( 500 'server/%s/backup/client/%s?disable' % (server_id, client_id), 501 method='GET').object 502 response_code = findtext(response, 'result', GENERAL_NS) 503 return response_code in ['IN_PROGRESS', 'SUCCESS'] 504 505 def ex_get_backup_details_for_target(self, target): 506 """ 507 Returns a backup details object for a target 508 509 :param target: The backup target to get details for 510 :type target: :class:`BackupTarget` or ``str`` 511 512 :rtype: :class:`DimensionDataBackupDetails` 513 """ 514 if not isinstance(target, BackupTarget): 515 target = self.ex_get_target_by_id(target) 516 if target is None: 517 return 518 response = self.connection.request_with_orgId_api_1( 519 'server/%s/backup' % (target.address), 520 method='GET').object 521 return self._to_backup_details(response, target) 522 523 def ex_list_available_client_types(self, target): 524 """ 525 Returns a list of available backup client types 526 527 :param target: The backup target to list available types for 528 :type target: :class:`BackupTarget` or ``str`` 529 530 :rtype: ``list`` of :class:`DimensionDataBackupClientType` 531 """ 532 server_id = self._target_to_target_address(target) 533 response = self.connection.request_with_orgId_api_1( 534 'server/%s/backup/client/type' % (server_id), 535 method='GET').object 536 return self._to_client_types(response) 537 538 def ex_list_available_storage_policies(self, target): 539 """ 540 Returns a list of available backup storage policies 541 542 :param target: The backup target to list available policies for 543 :type target: :class:`BackupTarget` or ``str`` 544 545 :rtype: ``list`` of :class:`DimensionDataBackupStoragePolicy` 546 """ 547 server_id = self._target_to_target_address(target) 548 response = self.connection.request_with_orgId_api_1( 549 'server/%s/backup/client/storagePolicy' % (server_id), 550 method='GET').object 551 return self._to_storage_policies(response) 552 553 def ex_list_available_schedule_policies(self, target): 554 """ 555 Returns a list of available backup schedule policies 556 557 :param target: The backup target to list available policies for 558 :type target: :class:`BackupTarget` or ``str`` 559 560 :rtype: ``list`` of :class:`DimensionDataBackupSchedulePolicy` 561 """ 562 server_id = self._target_to_target_address(target) 563 response = self.connection.request_with_orgId_api_1( 564 'server/%s/backup/client/schedulePolicy' % (server_id), 565 method='GET').object 566 return self._to_schedule_policies(response) 567 568 def _to_storage_policies(self, object): 569 elements = object.findall(fixxpath('storagePolicy', BACKUP_NS)) 570 571 return [self._to_storage_policy(el) for el in elements] 572 573 def _to_storage_policy(self, element): 574 return DimensionDataBackupStoragePolicy( 575 retention_period=int(element.get('retentionPeriodInDays')), 576 name=element.get('name'), 577 secondary_location=element.get('secondaryLocation') 578 ) 579 580 def _to_schedule_policies(self, object): 581 elements = object.findall(fixxpath('schedulePolicy', BACKUP_NS)) 582 583 return [self._to_schedule_policy(el) for el in elements] 584 585 def _to_schedule_policy(self, element): 586 return DimensionDataBackupSchedulePolicy( 587 name=element.get('name'), 588 description=element.get('description') 589 ) 590 591 def _to_client_types(self, object): 592 elements = object.findall(fixxpath('backupClientType', BACKUP_NS)) 593 594 return [self._to_client_type(el) for el in elements] 595 596 def _to_client_type(self, element): 597 description = element.get('description') 598 if description is None: 599 description = findtext(element, 'description', BACKUP_NS) 600 return DimensionDataBackupClientType( 601 type=element.get('type'), 602 description=description, 603 is_file_system=bool(element.get('isFileSystem') == 'true') 604 ) 605 606 def _to_backup_details(self, object, target): 607 return DimensionDataBackupDetails( 608 asset_id=object.get('assetId'), 609 service_plan=object.get('servicePlan'), 610 status=object.get('state'), 611 clients=self._to_clients(object, target) 612 ) 613 614 def _to_clients(self, object, target): 615 elements = object.findall(fixxpath('backupClient', BACKUP_NS)) 616 617 return [self._to_client(el, target) for el in elements] 618 619 def _to_client(self, element, target): 620 client_id = element.get('id') 621 return DimensionDataBackupClient( 622 id=client_id, 623 type=self._to_client_type(element), 624 status=element.get('status'), 625 schedule_policy=findtext(element, 'schedulePolicyName', BACKUP_NS), 626 storage_policy=findtext(element, 'storagePolicyName', BACKUP_NS), 627 download_url=findtext(element, 'downloadUrl', BACKUP_NS), 628 running_job=self._to_backup_job(element, target, client_id), 629 alert=self._to_alert(element) 630 ) 631 632 def _to_alert(self, element): 633 alert = element.find(fixxpath('alerting', BACKUP_NS)) 634 if alert is not None: 635 notify_list = [ 636 email_addr.text for email_addr 637 in alert.findall(fixxpath('emailAddress', BACKUP_NS)) 638 ] 639 return DimensionDataBackupClientAlert( 640 trigger=element.get('trigger'), 641 notify_list=notify_list 642 ) 643 return None 644 645 def _to_backup_job(self, element, target, client_id): 646 running_job = element.find(fixxpath('runningJob', BACKUP_NS)) 647 if running_job is not None: 648 return BackupTargetJob( 649 id=running_job.get('id'), 650 status=running_job.get('status'), 651 progress=int(running_job.get('percentageComplete')), 652 driver=self.connection.driver, 653 target=target, 654 extra={'clientId': client_id} 655 ) 656 return None 657 658 def _to_targets(self, object): 659 node_elements = object.findall(fixxpath('server', TYPES_URN)) 660 661 return [self._to_target(el) for el in node_elements] 662 663 def _to_target(self, element): 664 backup = findall(element, 'backup', TYPES_URN) 665 if len(backup) == 0: 666 return 667 extra = { 668 'description': findtext(element, 'description', TYPES_URN), 669 'sourceImageId': findtext(element, 'sourceImageId', TYPES_URN), 670 'datacenterId': element.get('datacenterId'), 671 'deployedTime': findtext(element, 'createTime', TYPES_URN), 672 'servicePlan': backup[0].get('servicePlan') 673 } 674 675 n = BackupTarget(id=backup[0].get('assetId'), 676 name=findtext(element, 'name', TYPES_URN), 677 address=element.get('id'), 678 driver=self.connection.driver, 679 type=BackupTargetType.VIRTUAL, 680 extra=extra) 681 return n 682 683 @staticmethod 684 def _client_to_client_id(backup_client): 685 return dd_object_to_id(backup_client, DimensionDataBackupClient) 686 687 @staticmethod 688 def _target_to_target_address(target): 689 return dd_object_to_id(target, BackupTarget, id_value='address') 690