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)

Reply via email to