| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You 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 libcloud.utils.py3 import ET |
| from libcloud.utils.xml import findall, findtext, fixxpath |
| from libcloud.backup.base import BackupDriver, BackupTarget, BackupTargetJob |
| from libcloud.backup.types import Provider, BackupTargetType |
| from libcloud.common.dimensiondata import ( |
| BACKUP_NS, |
| TYPES_URN, |
| GENERAL_NS, |
| API_ENDPOINTS, |
| DEFAULT_REGION, |
| DimensionDataConnection, |
| DimensionDataBackupClient, |
| DimensionDataBackupDetails, |
| DimensionDataBackupClientType, |
| DimensionDataBackupClientAlert, |
| DimensionDataBackupStoragePolicy, |
| DimensionDataBackupSchedulePolicy, |
| dd_object_to_id, |
| ) |
| |
| # pylint: disable=no-member |
| |
| DEFAULT_BACKUP_PLAN = "Advanced" |
| |
| |
| class DimensionDataBackupDriver(BackupDriver): |
| """ |
| DimensionData backup driver. |
| """ |
| |
| selected_region = None |
| connectionCls = DimensionDataConnection |
| name = "Dimension Data Backup" |
| website = "https://cloud.dimensiondata.com/" |
| type = Provider.DIMENSIONDATA |
| api_version = 1.0 |
| |
| network_domain_id = None |
| |
| def __init__( |
| self, |
| key, |
| secret=None, |
| secure=True, |
| host=None, |
| port=None, |
| api_version=None, |
| region=DEFAULT_REGION, |
| **kwargs, |
| ): |
| |
| if region not in API_ENDPOINTS and host is None: |
| raise ValueError("Invalid region: %s, no host specified" % (region)) |
| if region is not None: |
| self.selected_region = API_ENDPOINTS[region] |
| |
| super().__init__( |
| key=key, |
| secret=secret, |
| secure=secure, |
| host=host, |
| port=port, |
| api_version=api_version, |
| region=region, |
| **kwargs, |
| ) |
| |
| def _ex_connection_class_kwargs(self): |
| """ |
| Add the region to the kwargs before the connection is instantiated |
| """ |
| |
| kwargs = super()._ex_connection_class_kwargs() |
| kwargs["region"] = self.selected_region |
| return kwargs |
| |
| def get_supported_target_types(self): |
| """ |
| Get a list of backup target types this driver supports |
| |
| :return: ``list`` of :class:``BackupTargetType`` |
| """ |
| return [BackupTargetType.VIRTUAL] |
| |
| def list_targets(self): |
| """ |
| List all backuptargets |
| |
| :rtype: ``list`` of :class:`BackupTarget` |
| """ |
| targets = self._to_targets(self.connection.request_with_orgId_api_2("server/server").object) |
| return targets |
| |
| def create_target(self, name, address, type=BackupTargetType.VIRTUAL, extra=None): |
| """ |
| Creates a new backup target |
| |
| :param name: Name of the target (not used) |
| :type name: ``str`` |
| |
| :param address: The ID of the node in Dimension Data Cloud |
| :type address: ``str`` |
| |
| :param type: Backup target type, only Virtual supported |
| :type type: :class:`BackupTargetType` |
| |
| :param extra: (optional) Extra attributes (driver specific). |
| :type extra: ``dict`` |
| |
| :rtype: Instance of :class:`BackupTarget` |
| """ |
| if extra is not None: |
| service_plan = extra.get("servicePlan", DEFAULT_BACKUP_PLAN) |
| else: |
| service_plan = DEFAULT_BACKUP_PLAN |
| extra = {"servicePlan": service_plan} |
| |
| create_node = ET.Element("NewBackup", {"xmlns": BACKUP_NS}) |
| create_node.set("servicePlan", service_plan) |
| |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup" % (address), method="POST", data=ET.tostring(create_node) |
| ).object |
| |
| asset_id = None |
| for info in findall(response, "additionalInformation", GENERAL_NS): |
| if info.get("name") == "assetId": |
| asset_id = findtext(info, "value", GENERAL_NS) |
| |
| return BackupTarget( |
| id=asset_id, name=name, address=address, type=type, extra=extra, driver=self |
| ) |
| |
| def create_target_from_node(self, node, type=BackupTargetType.VIRTUAL, extra=None): |
| """ |
| Creates a new backup target from an existing node |
| |
| :param node: The Node to backup |
| :type node: ``Node`` |
| |
| :param type: Backup target type (Physical, Virtual, ...). |
| :type type: :class:`BackupTargetType` |
| |
| :param extra: (optional) Extra attributes (driver specific). |
| :type extra: ``dict`` |
| |
| :rtype: Instance of :class:`BackupTarget` |
| """ |
| return self.create_target( |
| name=node.name, address=node.id, type=BackupTargetType.VIRTUAL, extra=extra |
| ) |
| |
| def create_target_from_container(self, container, type=BackupTargetType.OBJECT, extra=None): |
| """ |
| Creates a new backup target from an existing storage container |
| |
| :param node: The Container to backup |
| :type node: ``Container`` |
| |
| :param type: Backup target type (Physical, Virtual, ...). |
| :type type: :class:`BackupTargetType` |
| |
| :param extra: (optional) Extra attributes (driver specific). |
| :type extra: ``dict`` |
| |
| :rtype: Instance of :class:`BackupTarget` |
| """ |
| return NotImplementedError("create_target_from_container not supported for this driver") |
| |
| def update_target(self, target, name=None, address=None, extra=None): |
| """ |
| Update the properties of a backup target, only changing the serviceplan |
| is supported. |
| |
| :param target: Backup target to update |
| :type target: Instance of :class:`BackupTarget` or ``str`` |
| |
| :param name: Name of the target |
| :type name: ``str`` |
| |
| :param address: Hostname, FQDN, IP, file path etc. |
| :type address: ``str`` |
| |
| :param extra: (optional) Extra attributes (driver specific). |
| :type extra: ``dict`` |
| |
| :rtype: Instance of :class:`BackupTarget` |
| """ |
| if extra is not None: |
| service_plan = extra.get("servicePlan", DEFAULT_BACKUP_PLAN) |
| else: |
| service_plan = DEFAULT_BACKUP_PLAN |
| request = ET.Element("ModifyBackup", {"xmlns": BACKUP_NS}) |
| request.set("servicePlan", service_plan) |
| server_id = self._target_to_target_address(target) |
| self.connection.request_with_orgId_api_1( |
| "server/%s/backup/modify" % (server_id), |
| method="POST", |
| data=ET.tostring(request), |
| ).object |
| if isinstance(target, BackupTarget): |
| target.extra = extra |
| else: |
| target = self.ex_get_target_by_id(server_id) |
| return target |
| |
| def delete_target(self, target): |
| """ |
| Delete a backup target |
| |
| :param target: Backup target to delete |
| :type target: Instance of :class:`BackupTarget` or ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| server_id = self._target_to_target_address(target) |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup?disable" % (server_id), method="GET" |
| ).object |
| response_code = findtext(response, "result", GENERAL_NS) |
| return response_code in ["IN_PROGRESS", "SUCCESS"] |
| |
| def list_recovery_points(self, target, start_date=None, end_date=None): |
| """ |
| List the recovery points available for a target |
| |
| :param target: Backup target to delete |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param start_date: The start date to show jobs between (optional) |
| :type start_date: :class:`datetime.datetime` |
| |
| :param end_date: The end date to show jobs between (optional) |
| :type end_date: :class:`datetime.datetime`` |
| |
| :rtype: ``list`` of :class:`BackupTargetRecoveryPoint` |
| """ |
| raise NotImplementedError("list_recovery_points not implemented for this driver") |
| |
| def recover_target(self, target, recovery_point, path=None): |
| """ |
| Recover a backup target to a recovery point |
| |
| :param target: Backup target to delete |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param recovery_point: Backup target with the backup data |
| :type recovery_point: Instance of :class:`BackupTarget` |
| |
| :param path: The part of the recovery point to recover (optional) |
| :type path: ``str`` |
| |
| :rtype: Instance of :class:`BackupTargetJob` |
| """ |
| raise NotImplementedError("recover_target not implemented for this driver") |
| |
| def recover_target_out_of_place(self, target, recovery_point, recovery_target, path=None): |
| """ |
| Recover a backup target to a recovery point out-of-place |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param recovery_point: Backup target with the backup data |
| :type recovery_point: Instance of :class:`BackupTarget` |
| |
| :param recovery_target: Backup target with to recover the data to |
| :type recovery_target: Instance of :class:`BackupTarget` |
| |
| :param path: The part of the recovery point to recover (optional) |
| :type path: ``str`` |
| |
| :rtype: Instance of :class:`BackupTargetJob` |
| """ |
| raise NotImplementedError("recover_target_out_of_place not implemented for this driver") |
| |
| def get_target_job(self, target, id): |
| """ |
| Get a specific backup job by ID |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param id: Backup target with the backup data |
| :type id: Instance of :class:`BackupTarget` |
| |
| :rtype: :class:`BackupTargetJob` |
| """ |
| jobs = self.list_target_jobs(target) |
| return list(filter(lambda x: x.id == id, jobs))[0] |
| |
| def list_target_jobs(self, target): |
| """ |
| List the backup jobs on a target |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` |
| |
| :rtype: ``list`` of :class:`BackupTargetJob` |
| """ |
| raise NotImplementedError("list_target_jobs not implemented for this driver") |
| |
| def create_target_job(self, target, extra=None): |
| """ |
| Create a new backup job on a target |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param extra: (optional) Extra attributes (driver specific). |
| :type extra: ``dict`` |
| |
| :rtype: Instance of :class:`BackupTargetJob` |
| """ |
| raise NotImplementedError("create_target_job not implemented for this driver") |
| |
| def resume_target_job(self, target, job): |
| """ |
| Resume a suspended backup job on a target |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param job: Backup target job to resume |
| :type job: Instance of :class:`BackupTargetJob` |
| |
| :rtype: ``bool`` |
| """ |
| raise NotImplementedError("resume_target_job not implemented for this driver") |
| |
| def suspend_target_job(self, target, job): |
| """ |
| Suspend a running backup job on a target |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` |
| |
| :param job: Backup target job to suspend |
| :type job: Instance of :class:`BackupTargetJob` |
| |
| :rtype: ``bool`` |
| """ |
| raise NotImplementedError("suspend_target_job not implemented for this driver") |
| |
| def cancel_target_job(self, job, ex_client=None, ex_target=None): |
| """ |
| Cancel a backup job on a target |
| |
| :param job: Backup target job to cancel. If it is ``None`` |
| ex_client and ex_target must be set |
| :type job: Instance of :class:`BackupTargetJob` or ``None`` |
| |
| :param ex_client: Client of the job to cancel. |
| Not necessary if job is specified. |
| DimensionData only has 1 job per client |
| :type ex_client: Instance of :class:`DimensionDataBackupClient` |
| or ``str`` |
| |
| :param ex_target: Target to cancel a job from. |
| Not necessary if job is specified. |
| :type ex_target: Instance of :class:`BackupTarget` or ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| if job is None: |
| if ex_client is None or ex_target is None: |
| raise ValueError("Either job or ex_client and " "ex_target have to be set") |
| server_id = self._target_to_target_address(ex_target) |
| client_id = self._client_to_client_id(ex_client) |
| else: |
| server_id = job.target.address |
| client_id = job.extra["clientId"] |
| |
| response = self.connection.request_with_orgId_api_1( |
| "server/{}/backup/client/{}?cancelJob".format(server_id, client_id), |
| method="GET", |
| ).object |
| response_code = findtext(response, "result", GENERAL_NS) |
| return response_code in ["IN_PROGRESS", "SUCCESS"] |
| |
| def ex_get_target_by_id(self, id): |
| """ |
| Get a target by server id |
| |
| :param id: The id of the target you want to get |
| :type id: ``str`` |
| |
| :rtype: :class:`BackupTarget` |
| """ |
| node = self.connection.request_with_orgId_api_2("server/server/%s" % id).object |
| return self._to_target(node) |
| |
| def ex_add_client_to_target( |
| self, target, client_type, storage_policy, schedule_policy, trigger, email |
| ): |
| """ |
| Add a client to a target |
| |
| :param target: Backup target with the backup data |
| :type target: Instance of :class:`BackupTarget` or ``str`` |
| |
| :param client: Client to add to the target |
| :type client: Instance of :class:`DimensionDataBackupClientType` |
| or ``str`` |
| |
| :param storage_policy: The storage policy for the client |
| :type storage_policy: Instance of |
| :class:`DimensionDataBackupStoragePolicy` |
| or ``str`` |
| |
| :param schedule_policy: The schedule policy for the client |
| :type schedule_policy: Instance of |
| :class:`DimensionDataBackupSchedulePolicy` |
| or ``str`` |
| |
| :param trigger: The notify trigger for the client |
| :type trigger: ``str`` |
| |
| :param email: The notify email for the client |
| :type email: ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| server_id = self._target_to_target_address(target) |
| |
| backup_elm = ET.Element("NewBackupClient", {"xmlns": BACKUP_NS}) |
| if isinstance(client_type, DimensionDataBackupClientType): |
| ET.SubElement(backup_elm, "type").text = client_type.type |
| else: |
| ET.SubElement(backup_elm, "type").text = client_type |
| |
| if isinstance(storage_policy, DimensionDataBackupStoragePolicy): |
| ET.SubElement(backup_elm, "storagePolicyName").text = storage_policy.name |
| else: |
| ET.SubElement(backup_elm, "storagePolicyName").text = storage_policy |
| |
| if isinstance(schedule_policy, DimensionDataBackupSchedulePolicy): |
| ET.SubElement(backup_elm, "schedulePolicyName").text = schedule_policy.name |
| else: |
| ET.SubElement(backup_elm, "schedulePolicyName").text = schedule_policy |
| |
| alerting_elm = ET.SubElement(backup_elm, "alerting") |
| alerting_elm.set("trigger", trigger) |
| ET.SubElement(alerting_elm, "emailAddress").text = email |
| |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup/client" % (server_id), |
| method="POST", |
| data=ET.tostring(backup_elm), |
| ).object |
| response_code = findtext(response, "result", GENERAL_NS) |
| return response_code in ["IN_PROGRESS", "SUCCESS"] |
| |
| def ex_remove_client_from_target(self, target, backup_client): |
| """ |
| Removes a client from a backup target |
| |
| :param target: The backup target to remove the client from |
| :type target: :class:`BackupTarget` or ``str`` |
| |
| :param backup_client: The backup client to remove |
| :type backup_client: :class:`DimensionDataBackupClient` or ``str`` |
| |
| :rtype: ``bool`` |
| """ |
| server_id = self._target_to_target_address(target) |
| client_id = self._client_to_client_id(backup_client) |
| response = self.connection.request_with_orgId_api_1( |
| "server/{}/backup/client/{}?disable".format(server_id, client_id), method="GET" |
| ).object |
| response_code = findtext(response, "result", GENERAL_NS) |
| return response_code in ["IN_PROGRESS", "SUCCESS"] |
| |
| def ex_get_backup_details_for_target(self, target): |
| """ |
| Returns a backup details object for a target |
| |
| :param target: The backup target to get details for |
| :type target: :class:`BackupTarget` or ``str`` |
| |
| :rtype: :class:`DimensionDataBackupDetails` |
| """ |
| if not isinstance(target, BackupTarget): |
| target = self.ex_get_target_by_id(target) |
| if target is None: |
| return |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup" % (target.address), method="GET" |
| ).object |
| return self._to_backup_details(response, target) |
| |
| def ex_list_available_client_types(self, target): |
| """ |
| Returns a list of available backup client types |
| |
| :param target: The backup target to list available types for |
| :type target: :class:`BackupTarget` or ``str`` |
| |
| :rtype: ``list`` of :class:`DimensionDataBackupClientType` |
| """ |
| server_id = self._target_to_target_address(target) |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup/client/type" % (server_id), method="GET" |
| ).object |
| return self._to_client_types(response) |
| |
| def ex_list_available_storage_policies(self, target): |
| """ |
| Returns a list of available backup storage policies |
| |
| :param target: The backup target to list available policies for |
| :type target: :class:`BackupTarget` or ``str`` |
| |
| :rtype: ``list`` of :class:`DimensionDataBackupStoragePolicy` |
| """ |
| server_id = self._target_to_target_address(target) |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup/client/storagePolicy" % (server_id), method="GET" |
| ).object |
| return self._to_storage_policies(response) |
| |
| def ex_list_available_schedule_policies(self, target): |
| """ |
| Returns a list of available backup schedule policies |
| |
| :param target: The backup target to list available policies for |
| :type target: :class:`BackupTarget` or ``str`` |
| |
| :rtype: ``list`` of :class:`DimensionDataBackupSchedulePolicy` |
| """ |
| server_id = self._target_to_target_address(target) |
| response = self.connection.request_with_orgId_api_1( |
| "server/%s/backup/client/schedulePolicy" % (server_id), method="GET" |
| ).object |
| return self._to_schedule_policies(response) |
| |
| def _to_storage_policies(self, object): |
| elements = object.findall(fixxpath("storagePolicy", BACKUP_NS)) |
| |
| return [self._to_storage_policy(el) for el in elements] |
| |
| def _to_storage_policy(self, element): |
| return DimensionDataBackupStoragePolicy( |
| retention_period=int(element.get("retentionPeriodInDays")), |
| name=element.get("name"), |
| secondary_location=element.get("secondaryLocation"), |
| ) |
| |
| def _to_schedule_policies(self, object): |
| elements = object.findall(fixxpath("schedulePolicy", BACKUP_NS)) |
| |
| return [self._to_schedule_policy(el) for el in elements] |
| |
| def _to_schedule_policy(self, element): |
| return DimensionDataBackupSchedulePolicy( |
| name=element.get("name"), description=element.get("description") |
| ) |
| |
| def _to_client_types(self, object): |
| elements = object.findall(fixxpath("backupClientType", BACKUP_NS)) |
| |
| return [self._to_client_type(el) for el in elements] |
| |
| def _to_client_type(self, element): |
| description = element.get("description") |
| if description is None: |
| description = findtext(element, "description", BACKUP_NS) |
| return DimensionDataBackupClientType( |
| type=element.get("type"), |
| description=description, |
| is_file_system=bool(element.get("isFileSystem") == "true"), |
| ) |
| |
| def _to_backup_details(self, object, target): |
| return DimensionDataBackupDetails( |
| asset_id=object.get("assetId"), |
| service_plan=object.get("servicePlan"), |
| status=object.get("state"), |
| clients=self._to_clients(object, target), |
| ) |
| |
| def _to_clients(self, object, target): |
| elements = object.findall(fixxpath("backupClient", BACKUP_NS)) |
| |
| return [self._to_client(el, target) for el in elements] |
| |
| def _to_client(self, element, target): |
| client_id = element.get("id") |
| return DimensionDataBackupClient( |
| id=client_id, |
| type=self._to_client_type(element), |
| status=element.get("status"), |
| schedule_policy=findtext(element, "schedulePolicyName", BACKUP_NS), |
| storage_policy=findtext(element, "storagePolicyName", BACKUP_NS), |
| download_url=findtext(element, "downloadUrl", BACKUP_NS), |
| running_job=self._to_backup_job(element, target, client_id), |
| alert=self._to_alert(element), |
| ) |
| |
| def _to_alert(self, element): |
| alert = element.find(fixxpath("alerting", BACKUP_NS)) |
| if alert is not None: |
| notify_list = [ |
| email_addr.text for email_addr in alert.findall(fixxpath("emailAddress", BACKUP_NS)) |
| ] |
| return DimensionDataBackupClientAlert( |
| trigger=element.get("trigger"), notify_list=notify_list |
| ) |
| return None |
| |
| def _to_backup_job(self, element, target, client_id): |
| running_job = element.find(fixxpath("runningJob", BACKUP_NS)) |
| if running_job is not None: |
| return BackupTargetJob( |
| id=running_job.get("id"), |
| status=running_job.get("status"), |
| progress=int(running_job.get("percentageComplete")), |
| driver=self.connection.driver, |
| target=target, |
| extra={"clientId": client_id}, |
| ) |
| return None |
| |
| def _to_targets(self, object): |
| node_elements = object.findall(fixxpath("server", TYPES_URN)) |
| |
| return [self._to_target(el) for el in node_elements] |
| |
| def _to_target(self, element): |
| backup = findall(element, "backup", TYPES_URN) |
| if len(backup) == 0: |
| return |
| extra = { |
| "description": findtext(element, "description", TYPES_URN), |
| "sourceImageId": findtext(element, "sourceImageId", TYPES_URN), |
| "datacenterId": element.get("datacenterId"), |
| "deployedTime": findtext(element, "createTime", TYPES_URN), |
| "servicePlan": backup[0].get("servicePlan"), |
| } |
| |
| n = BackupTarget( |
| id=backup[0].get("assetId"), |
| name=findtext(element, "name", TYPES_URN), |
| address=element.get("id"), |
| driver=self.connection.driver, |
| type=BackupTargetType.VIRTUAL, |
| extra=extra, |
| ) |
| return n |
| |
| @staticmethod |
| def _client_to_client_id(backup_client): |
| return dd_object_to_id(backup_client, DimensionDataBackupClient) |
| |
| @staticmethod |
| def _target_to_target_address(target): |
| return dd_object_to_id(target, BackupTarget, id_value="address") |