Formed a backup driver example for the Dimension Data Cloud APIs
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/ab2ce8f6 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/ab2ce8f6 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/ab2ce8f6 Branch: refs/heads/trunk Commit: ab2ce8f63db2ef861d9021da2d2b86c5192577c8 Parents: 09a258f Author: anthony-shaw <anthony.p.s...@gmail.com> Authored: Fri Dec 11 17:15:19 2015 +1100 Committer: anthony-shaw <anthony.p.s...@gmail.com> Committed: Fri Dec 11 17:15:19 2015 +1100 ---------------------------------------------------------------------- libcloud/backup/drivers/dimensiondata.py | 487 +++++++++++++++++++ libcloud/backup/providers.py | 2 + libcloud/backup/types.py | 1 + libcloud/test/backup/__init__.py | 37 ++ ...745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml | 49 ++ ...ead52_692f_4314_8725_c8a4f4d13a87_backup.xml | 18 + ...8a4f4d13a87_backup_client_schedulePolicy.xml | 5 + ...c8a4f4d13a87_backup_client_storagePolicy.xml | 6 + ...314_8725_c8a4f4d13a87_backup_client_type.xml | 6 + ...92f_4314_8725_c8a4f4d13a87_backup_modify.xml | 7 + .../dimensiondata/oec_0_9_myaccount.xml | 26 + libcloud/test/backup/test_dimensiondata.py | 160 ++++++ libcloud/test/file_fixtures.py | 7 + 13 files changed, 811 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/backup/drivers/dimensiondata.py ---------------------------------------------------------------------- diff --git a/libcloud/backup/drivers/dimensiondata.py b/libcloud/backup/drivers/dimensiondata.py new file mode 100644 index 0000000..15e8251 --- /dev/null +++ b/libcloud/backup/drivers/dimensiondata.py @@ -0,0 +1,487 @@ +# 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. + +try: + from lxml import etree as ET +except ImportError: + from xml.etree import ElementTree as ET + +from libcloud.backup.base import BackupDriver, BackupTarget +from libcloud.backup.types import BackupTargetType +from libcloud.backup.types import Provider +from libcloud.common.dimensiondata import DimensionDataConnection +from libcloud.common.dimensiondata import API_ENDPOINTS +from libcloud.common.dimensiondata import DEFAULT_REGION +from libcloud.common.dimensiondata import TYPES_URN +from libcloud.common.dimensiondata import GENERAL_NS +from libcloud.utils.xml import fixxpath, findtext, findall + +BACKUP_NS = 'http://oec.api.opsource.net/schemas/backup' + + +class DimensionDataBackupClientType(object): + def __init__(self, type, is_file_system, description): + self.type = type + self.is_file_system = is_file_system + self.description = description + + +class DimensionDataBackupStoragePolicy(object): + def __init__(self, name, retention_period, secondary_location): + self.name = name + self.retention_period = retention_period + self.secondary_location = secondary_location + + +class DimensionDataBackupSchedulePolicy(object): + def __init__(self, name, description): + self.name = name + self.description = description + + +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: + raise ValueError('Invalid region: %s' % (region)) + + self.selected_region = API_ENDPOINTS[region] + + super(DimensionDataBackupDriver, self).__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(DimensionDataBackupDriver, + self)._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` + """ + service_plan = extra.get('service_plan', 'Advanced') + 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` + + :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` + """ + service_plan = extra.get('servicePlan', 'Advanced') + request = ET.Element('ModifyBackup', + {'xmlns': BACKUP_NS}) + request.set('servicePlan', service_plan) + + self.connection.request_with_orgId_api_1( + 'server/%s/backup/modify' % (target.address), + method='POST', + data=ET.tostring(request)).object + target.extra = extra + return target + + def delete_target(self, target): + """ + Delete a backup target + + :param target: Backup target to delete + :type target: Instance of :class:`BackupTarget` + """ + response = self.connection.request_with_orgId_api_1( + 'server/%s/backup?disable' % (target.address), + 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, target, job): + """ + Cancel a 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 cancel + :type job: Instance of :class:`BackupTargetJob` + + :rtype: ``bool`` + """ + raise NotImplementedError( + 'cancel_target_job not implemented for this driver') + + 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` + + :rtype: ``list`` of :class:`DimensionDataBackupClientType` + """ + response = self.connection.request_with_orgId_api_1( + 'server/%s/backup/client/type' % (target.address), + 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` + + :rtype: ``list`` of :class:`DimensionDataBackupStoragePolicy` + """ + response = self.connection.request_with_orgId_api_1( + 'server/%s/backup/client/storagePolicy' % (target.address), + 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` + + :rtype: ``list`` of :class:`DimensionDataBackupSchedulePolicy` + """ + response = self.connection.request_with_orgId_api_1( + 'server/%s/backup/client/schedulePolicy' % (target.address), + 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): + return DimensionDataBackupClientType( + type=element.get('type'), + description=element.get('description'), + is_file_system=bool(element.get('isFileSystem') == 'true') + ) + + 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 http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/backup/providers.py ---------------------------------------------------------------------- diff --git a/libcloud/backup/providers.py b/libcloud/backup/providers.py index a38187f..dea0cf5 100644 --- a/libcloud/backup/providers.py +++ b/libcloud/backup/providers.py @@ -24,6 +24,8 @@ DRIVERS = { ('libcloud.backup.drivers.ebs', 'EBSBackupDriver'), Provider.GCE: ('libcloud.backup.drivers.gce', 'GCEBackupDriver'), + Provider.DIMENSIONDATA: + ('libcloud.backup.drivers.dimensiondata', 'DimensionDataBackupDriver') } http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/backup/types.py ---------------------------------------------------------------------- diff --git a/libcloud/backup/types.py b/libcloud/backup/types.py index 6ffa6c8..0264268 100644 --- a/libcloud/backup/types.py +++ b/libcloud/backup/types.py @@ -24,6 +24,7 @@ class Provider(object): DUMMY = 'dummy' EBS = 'ebs' GCE = 'gce' + DIMENSIONDATA = 'dimensiondata' class BackupTargetType(object): http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/__init__.py ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/__init__.py b/libcloud/test/backup/__init__.py index e69de29..b31bcda 100644 --- a/libcloud/test/backup/__init__.py +++ b/libcloud/test/backup/__init__.py @@ -0,0 +1,37 @@ +# 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.backup.base import BackupTarget, BackupTargetType +from libcloud.pricing import get_pricing + + +class TestCaseMixin(object): + + def get_supported_target_types(self): + targets = self.driver.get_supported_target_types() + self.assertTrue(isinstance(targets, list)) + for target in targets: + self.assertTrue(isinstance(target, BackupTargetType)) + + def test_list_targets_response(self): + targets = self.driver.list_targets() + self.assertTrue(isinstance(targets, list)) + for target in targets: + self.assertTrue(isinstance(target, BackupTarget)) + + +if __name__ == "__main__": + import doctest + doctest.testmod() http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml b/libcloud/test/backup/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml new file mode 100644 index 0000000..c3d607f --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes"?> +<servers xmlns="urn:didata.com:api:cloud:types" pageNumber="1" pageCount="2" totalCount="2" pageSize="250"> + <!-- MCP 1.0 Server --> + <server id="e75ead52-692f-4314-8725-c8a4f4d13a87" datacenterId="NA1"> + <name>Production Web Server MCP 1</name> + <description>nopassword0</description> + <operatingSystem id="REDHAT632" displayName="REDHAT6/32" family="UNIX" /> + <cpu count="4" speed="STANDARD" coresPerSocket="1" /> + <memoryGb>2</memoryGb> + <disk id="74f81c56-96cc-4cca-b4d7-a88f641a6ea2" scsiId="0" sizeGb="10" speed="STANDARD" state="NORMAL" /> + <nic id="43b24e9e-c1c9-4d53-965b-89bcaa725103" privateIpv4="10.160.117.25" networkId="c550be0e-65c1-11e4-811f-005056806999" networkName="Test1" state="NORMAL" /> + <backup assetId="5579f3a7-4c32-4cf5-8a7e-b45c36a35c10" servicePlan="Enterprise" state="NORMAL" /> + <monitoring monitoringId="11049" servicePlan="ESSENTIALS" state="NORMAL" /> + <sourceImageId>e9ec6eb4-4634-49de-b914-01eb74da5fb9</sourceImageId> + <createTime>2015-08-11T16:51:05.000Z</createTime> + <deployed>true</deployed> + <started>true</started> + <state>NORMAL</state> + <vmwareTools versionStatus="NEED_UPGRADE" runningStatus="RUNNING" apiVersion="8389" /> + <virtualHardware version="vmx-08" upToDate="false" /> + </server> + <!-- MCP 2.0 Server --> + <server id="5a32d6e4-9707-4813-a269-56ab4d989f4d" datacenterId="NA9"> + <name>Production Web Server MCP 2</name> + <description>Server to host our main web application.</description> + <operatingSystem id="WIN2008S32" displayName="WIN2008S/32" family="WINDOWS" /> + <cpu count="2" speed="STANDARD" coresPerSocket="1" /> + <memoryGb>4</memoryGb> + <disk id="c2e1f199-116e-4dbc-9960-68720b832b0a" scsiId="0" sizeGb="50" speed="STANDARD" state="NORMAL" /> + <networkInfo networkDomainId="553f26b6-2a73-42c3-a78b-6116f11291d0"> + <primaryNic id="5e869800-df7b-4626-bcbf-8643b8be11fd" privateIpv4="10.0.4.8" ipv6="2607:f480:1111:1282:2960:fb72:7154:6160" vlanId="bc529e20-dc6f-42ba-be20-0ffe44d1993f" vlanName="Production VLAN" state="NORMAL" /> + </networkInfo> + <backup assetId="91002e08-8dc1-47a1-ad33-04f501c06f87" servicePlan="Advanced" state="NORMAL" /> + <monitoring monitoringId="11039" servicePlan="ESSENTIALS" state="NORMAL" /> + <softwareLabel>MSSQL2008R2S</softwareLabel> + <sourceImageId>3ebf3c0f-90fe-4a8b-8585-6e65b316592c</sourceImageId> + <createTime>2015-12-02T10:31:33.000Z</createTime> + <deployed>true</deployed> + <started>true</started> + <state>PENDING_CHANGE</state> + <progress> + <action>SHUTDOWN_SERVER</action> + <requestTime>2015-12-02T11:07:40.000Z</requestTime> + <userName>devuser1</userName> + </progress> + <vmwareTools versionStatus="CURRENT" runningStatus="RUNNING" apiVersion="9354" /> + <virtualHardware version="vmx-08" upToDate="false" /> + </server> +</servers> http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup.xml b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup.xml new file mode 100644 index 0000000..6012447 --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" + xmlns="http://oec.api.opsource.net/schemas/server" + xmlns:ns14="http://oec.api.opsource.net/schemas/support" + xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" + xmlns:ns9="http://oec.api.opsource.net/schemas/admin" + xmlns:ns5="http://oec.api.opsource.net/schemas/vip" + xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" + xmlns:ns13="http://oec.api.opsource.net/schemas/reset" + xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns10="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns8="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns11="http://oec.api.opsource.net/schemas/backup" xmlns:ns2="http://oec.api.opsource.net/schemas/directory" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/organization"> + <ns6:operation>Enable Backup for Server</ns6:operation> + <ns6:result>SUCCESS</ns6:result> + <ns6:resultDetail>Backup enabled for Server - see additional Information for Asset ID</ns6:resultDetail> + <ns6:resultCode>REASON_0</ns6:resultCode> + <ns6:additionalInformation name="assetId"> + <ns6:value>ee7c4b64-f7af-4a4f-8384-be362273530f</ns6:value> + </ns6:additionalInformation> +</ns6:Status> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy.xml b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy.xml new file mode 100644 index 0000000..dae3bfd --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<BackupSchedulePolicies xmlns="http://oec.api.opsource.net/schemas/backup"> + <!--One or more repetitions, all attributes mandatory --> + <schedulePolicy name="12AM - 6AM" description="Daily backup will start between 12AM - 6AM"/> +</BackupSchedulePolicies> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy.xml b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy.xml new file mode 100644 index 0000000..15e0aeb --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<BackupStoragePolicies xmlns="http://oec.api.opsource.net/schemas/backup"> + <!-- One or more repetitions. Attribute secondaryLocation is optional, all +others mandatory --> + <storagePolicy retentionPeriodInDays="30" name="30 Day Storage Policy + Secondary Copy" secondaryLocation="Primary"/> +</BackupStoragePolicies> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type.xml b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type.xml new file mode 100644 index 0000000..d7338bb --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<BackupClientTypes xmlns="http://oec.api.opsource.net/schemas/backup"> + <!--Zero or more repetitions, all attributes mandatory --> + <backupClientType type="FA.Linux" isFileSystem="true" description="Linux +File system"/> +</BackupClientTypes> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify.xml b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify.xml new file mode 100644 index 0000000..1e356a7 --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/support" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/admin" xmlns:ns5="http://oec.api.opsource.net/schemas/vip" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/reset" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns10="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns8="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns11="http://oec.api.opsource.net/schemas/backup" xmlns:ns2="http://oec.api.opsource.net/schemas/directory" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/organization"> + <ns6:operation>Change Backup Service Plan</ns6:operation> + <ns6:result>SUCCESS</ns6:result> + <ns6:resultDetail>Backup Service Plan changed</ns6:resultDetail> + <ns6:resultCode>REASON_0</ns6:resultCode> +</ns6:Status> http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_myaccount.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_myaccount.xml b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_myaccount.xml new file mode 100644 index 0000000..4f3b132 --- /dev/null +++ b/libcloud/test/backup/fixtures/dimensiondata/oec_0_9_myaccount.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<ns3:Account xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns9="http://oec.api.opsource.net/schemas/reset" xmlns:ns5="http://oec.api.opsource.net/schemas/vip" xmlns:ns12="http://oec.api.opsource.net/schemas/general" xmlns:ns6="http://oec.api.opsource.net/schemas/imageimportexport" xmlns:ns13="http://oec.api.opsource.net/schemas/support" xmlns:ns7="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns10="http://oec.api.opsource.net/schemas/ipplan" xmlns:ns8="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns11="http://oec.api.opsource.net/schemas/storage" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/directory"> + <ns3:userName>testuser</ns3:userName> + <ns3:fullName>Test User</ns3:fullName> + <ns3:firstName>Test</ns3:firstName> + <ns3:lastName>User</ns3:lastName> + <ns3:emailAddress>t...@example.com</ns3:emailAddress> + <ns3:orgId>8a8f6abc-2745-4d8a-9cbc-8dabe5a7d0e4</ns3:orgId> + <ns3:roles> + <ns3:role> + <ns3:name>create image</ns3:name> + </ns3:role> + <ns3:role> + <ns3:name>reports</ns3:name> + </ns3:role> + <ns3:role> + <ns3:name>server</ns3:name> + </ns3:role> + <ns3:role> + <ns3:name>primary administrator</ns3:name> + </ns3:role> + <ns3:role> + <ns3:name>network</ns3:name> + </ns3:role> + </ns3:roles> +</ns3:Account> http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/backup/test_dimensiondata.py ---------------------------------------------------------------------- diff --git a/libcloud/test/backup/test_dimensiondata.py b/libcloud/test/backup/test_dimensiondata.py new file mode 100644 index 0000000..0d9e37f --- /dev/null +++ b/libcloud/test/backup/test_dimensiondata.py @@ -0,0 +1,160 @@ +# 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. + +try: + from lxml import etree as ET +except ImportError: + from xml.etree import ElementTree as ET + +import sys +import unittest +from libcloud.utils.py3 import httplib + +from libcloud.common.types import InvalidCredsError +from libcloud.common.dimensiondata import DimensionDataAPIException, NetworkDomainServicePlan +from libcloud.backup.drivers.dimensiondata import DimensionDataBackupDriver as DimensionData + +from libcloud.test import MockHttp +from libcloud.test.backup import TestCaseMixin +from libcloud.test.file_fixtures import BackupFileFixtures + +from libcloud.test.secrets import DIMENSIONDATA_PARAMS + + +class DimensionDataTests(unittest.TestCase, TestCaseMixin): + + def setUp(self): + DimensionData.connectionCls.conn_classes = (None, DimensionDataMockHttp) + DimensionDataMockHttp.type = None + self.driver = DimensionData(*DIMENSIONDATA_PARAMS) + + def test_invalid_region(self): + try: + self.driver = DimensionData(*DIMENSIONDATA_PARAMS, region='blah') + except ValueError: + pass + + def test_invalid_creds(self): + DimensionDataMockHttp.type = 'UNAUTHORIZED' + try: + self.driver.list_targets() + self.assertTrue( + False) # Above command should have thrown an InvalidCredsException + except InvalidCredsError: + pass + + def test_list_targets(self): + targets = self.driver.list_targets() + self.assertEqual(len(targets), 2) + self.assertEqual(targets[0].id, '5579f3a7-4c32-4cf5-8a7e-b45c36a35c10') + self.assertEqual(targets[0].address, 'e75ead52-692f-4314-8725-c8a4f4d13a87') + self.assertEqual(targets[0].extra['servicePlan'], 'Enterprise') + + def test_create_target(self): + target = self.driver.create_target( + 'name', + 'e75ead52-692f-4314-8725-c8a4f4d13a87', + extra={'servicePlan': 'Enterprise'}) + self.assertEqual(target.id, 'ee7c4b64-f7af-4a4f-8384-be362273530f') + self.assertEqual(target.address, 'e75ead52-692f-4314-8725-c8a4f4d13a87') + self.assertEqual(target.extra['servicePlan'], 'Enterprise') + + def test_update_target(self): + target = self.driver.list_targets()[0] + extra = {'servicePlan': 'Enterprise'} + new_target = self.driver.update_target(target, extra=extra) + self.assertEqual(new_target.extra['servicePlan'], 'Enterprise') + + def test_delete_target(self): + target = self.driver.list_targets()[0] + self.assertTrue(self.driver.delete_target(target)) + + def test_ex_list_available_client_types(self): + target = self.driver.list_targets()[0] + answer = self.driver.ex_list_available_client_types(target) + self.assertEqual(len(answer), 1) + self.assertEqual(answer[0].type, 'FA.Linux') + self.assertEqual(answer[0].is_file_system, True) + self.assertEqual(answer[0].description, 'Linux File system') + + def test_ex_list_available_storage_policies(self): + target = self.driver.list_targets()[0] + answer = self.driver.ex_list_available_storage_policies(target) + self.assertEqual(len(answer), 1) + self.assertEqual(answer[0].name, + '30 Day Storage Policy + Secondary Copy') + self.assertEqual(answer[0].retention_period, 30) + self.assertEqual(answer[0].secondary_location, 'Primary') + + def test_ex_list_available_schedule_policies(self): + target = self.driver.list_targets()[0] + answer = self.driver.ex_list_available_schedule_policies(target) + self.assertEqual(len(answer), 1) + self.assertEqual(answer[0].name, '12AM - 6AM') + self.assertEqual(answer[0].description, 'Daily backup will start between 12AM - 6AM') + +class InvalidRequestError(Exception): + def __init__(self, tag): + super(InvalidRequestError, self).__init__("Invalid Request - %s" % tag) + + +class DimensionDataMockHttp(MockHttp): + + fixtures = BackupFileFixtures('dimensiondata') + + def _oec_0_9_myaccount_UNAUTHORIZED(self, method, url, body, headers): + return (httplib.UNAUTHORIZED, "", {}, httplib.responses[httplib.UNAUTHORIZED]) + + def _oec_0_9_myaccount(self, method, url, body, headers): + body = self.fixtures.load('oec_0_9_myaccount.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_myaccount_INPROGRESS(self, method, url, body, headers): + body = self.fixtures.load('oec_0_9_myaccount.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server(self, method, url, body, headers): + body = self.fixtures.load( + 'caas_2_1_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify(self, method, url, body, headers): + body = self.fixtures.load( + 'oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + +if __name__ == '__main__': + sys.exit(unittest.main()) http://git-wip-us.apache.org/repos/asf/libcloud/blob/ab2ce8f6/libcloud/test/file_fixtures.py ---------------------------------------------------------------------- diff --git a/libcloud/test/file_fixtures.py b/libcloud/test/file_fixtures.py index a09639c..5cb6509 100644 --- a/libcloud/test/file_fixtures.py +++ b/libcloud/test/file_fixtures.py @@ -27,6 +27,7 @@ FIXTURES_ROOT = { 'storage': 'storage/fixtures', 'loadbalancer': 'loadbalancer/fixtures', 'dns': 'dns/fixtures', + 'backup': 'backup/fixtures', 'openstack': 'compute/fixtures/openstack', } @@ -80,3 +81,9 @@ class OpenStackFixtures(FileFixtures): def __init__(self, sub_dir=''): super(OpenStackFixtures, self).__init__(fixtures_type='openstack', sub_dir=sub_dir) + + +class BackupFileFixtures(FileFixtures): + def __init__(self, sub_dir=''): + super(BackupFileFixtures, self).__init__(fixtures_type='backup', + sub_dir=sub_dir)