Repository: softlayer/softlayer-python
Pull request #830 (2017-06-06 20:14:45)

Add volume-duplicate command for file & block storage
[This pull request is being opened now for review, but it should not be merged in yet as some work is still pending on the backend for duplication features to be fully supported.]


Patch:
diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py
index d1461df2..b8ba8bed 100644
--- a/SoftLayer/CLI/block/detail.py
+++ b/SoftLayer/CLI/block/detail.py
@@ -99,4 +99,13 @@ def cli(env, volume_id):
             replicant_list.append(replicant_table)
         table.add_row(['Replicant Volumes', replicant_list])
 
+    if block_volume.get('originalVolumeSize'):
+        duplicate_info = formatting.Table(['Original Volume Name',
+                                           block_volume['originalVolumeName']])
+        duplicate_info.add_row(['Original Volume Size',
+                                block_volume['originalVolumeSize']])
+        duplicate_info.add_row(['Original Snapshot Name',
+                                block_volume['originalSnapshotName']])
+        table.add_row(['Duplicate Volume Properties', duplicate_info])
+
     env.fout(table)
diff --git a/SoftLayer/CLI/block/duplicate.py b/SoftLayer/CLI/block/duplicate.py
new file mode 100644
index 00000000..98ef1b79
--- /dev/null
+++ b/SoftLayer/CLI/block/duplicate.py
@@ -0,0 +1,83 @@
+"""Order a duplicate block storage volume."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+
+
+CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()}
+
+
+@click.command(context_settings=CONTEXT_SETTINGS)
+@click.argument('origin-volume-id')
+@click.option('--origin-snapshot-id', '-o',
+              type=int,
+              help="ID of an origin volume snapshot to use for duplcation.")
+@click.option('--duplicate-size', '-c',
+              type=int,
+              help='Size of duplicate block volume in GB. '
+                   '***If no size is specified, the size of '
+                   'the origin volume will be used.***\n'
+                   'Potential Sizes: [20, 40, 80, 100, 250, '
+                   '500, 1000, 2000, 4000, 8000, 12000] '
+                   'Minimum: [the size of the origin volume] '
+                   'Maximum: [the minimum of 12000 GB or '
+                   '10*(origin volume size)]')
+@click.option('--duplicate-iops', '-i',
+              type=int,
+              help='Performance Storage IOPS, between 100 and 6000 in '
+                   'multiples of 100 [only used for performance volumes] '
+                   '***If no IOPS value is specified, the IOPS value of the '
+                   'origin volume will be used.***\n'
+                   'Requirements: [If IOPS/GB for the origin volume is less '
+                   'than 0.3, IOPS/GB for the duplicate must also be less '
+                   'than 0.3. If IOPS/GB for the origin volume is greater '
+                   'than or equal to 0.3, IOPS/GB for the duplicate must '
+                   'also be greater than or equal to 0.3.]')
+@click.option('--duplicate-tier', '-t',
+              help='Endurance Storage Tier (IOPS per GB) [only used for '
+                   'endurance volumes] ***If no tier is specified, the tier '
+                   'of the origin volume will be used.***\n'
+                   'Requirements: [If IOPS/GB for the origin volume is 0.25, '
+                   'IOPS/GB for the duplicate must also be 0.25. If IOPS/GB '
+                   'for the origin volume is greater than 0.25, IOPS/GB '
+                   'for the duplicate must also be greater than 0.25.]',
+              type=click.Choice(['0.25', '2', '4', '10']))
+@click.option('--duplicate-snapshot-size', '-s',
+              type=int,
+              help='The size of snapshot space to order for the duplicate. '
+                   '***If no snapshot space size is specified, the snapshot '
+                   'space size of the origin volume will be used.***\n'
+                   'Input "0" for this parameter to order a duplicate volume '
+                   'with no snapshot space.')
+@environment.pass_env
+def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size,
+        duplicate_iops, duplicate_tier, duplicate_snapshot_size):
+    """Order a duplicate block storage volume."""
+    block_manager = SoftLayer.BlockStorageManager(env.client)
+
+    if duplicate_tier is not None:
+        duplicate_tier = float(duplicate_tier)
+
+    try:
+        order = block_manager.order_duplicate_volume(
+            origin_volume_id,
+            origin_snapshot_id=origin_snapshot_id,
+            duplicate_size=duplicate_size,
+            duplicate_iops=duplicate_iops,
+            duplicate_tier_level=duplicate_tier,
+            duplicate_snapshot_size=duplicate_snapshot_size
+        )
+    except ValueError as ex:
+        raise exceptions.ArgumentError(str(ex))
+
+    if 'placedOrder' in order.keys():
+        click.echo("Order #{0} placed successfully!".format(
+            order['placedOrder']['id']))
+        for item in order['placedOrder']['items']:
+            click.echo(" > %s" % item['description'])
+    else:
+        click.echo("Order could not be placed! Please verify your options " +
+                   "and try again.")
diff --git a/SoftLayer/CLI/file/detail.py b/SoftLayer/CLI/file/detail.py
index 8f7024aa..3c9af3f2 100644
--- a/SoftLayer/CLI/file/detail.py
+++ b/SoftLayer/CLI/file/detail.py
@@ -16,7 +16,8 @@ def cli(env, volume_id):
     file_manager = SoftLayer.FileStorageManager(env.client)
     file_volume = file_manager.get_file_volume_details(volume_id)
     file_volume = utils.NestedDict(file_volume)
-    used_space = int(file_volume['bytesUsed'])
+    used_space = int(file_volume['bytesUsed'])\
+        if file_volume['bytesUsed'] else 0
 
     table = formatting.KeyValueTable(['Name', 'Value'])
     table.align['Name'] = 'r'
@@ -114,4 +115,13 @@ def cli(env, volume_id):
             replicant_list.append(replicant_table)
         table.add_row(['Replicant Volumes', replicant_list])
 
+    if file_volume.get('originalVolumeSize'):
+        duplicate_info = formatting.Table(['Original Volume Name',
+                                           file_volume['originalVolumeName']])
+        duplicate_info.add_row(['Original Volume Size',
+                                file_volume['originalVolumeSize']])
+        duplicate_info.add_row(['Original Snapshot Name',
+                                file_volume['originalSnapshotName']])
+        table.add_row(['Duplicate Volume Properties', duplicate_info])
+
     env.fout(table)
diff --git a/SoftLayer/CLI/file/duplicate.py b/SoftLayer/CLI/file/duplicate.py
new file mode 100644
index 00000000..e0216977
--- /dev/null
+++ b/SoftLayer/CLI/file/duplicate.py
@@ -0,0 +1,79 @@
+"""Order a duplicate file storage volume."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+
+
+CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()}
+
+
+@click.command(context_settings=CONTEXT_SETTINGS)
+@click.argument('origin-volume-id')
+@click.option('--origin-snapshot-id', '-o',
+              type=int,
+              help="ID of an origin volume snapshot to use for duplcation.")
+@click.option('--duplicate-size', '-c',
+              type=int,
+              help='Size of duplicate file volume in GB. '
+                   '***If no size is specified, the size of '
+                   'the origin volume will be used.***\n'
+                   'Minimum: [the size of the origin volume]')
+@click.option('--duplicate-iops', '-i',
+              type=int,
+              help='Performance Storage IOPS, between 100 and 6000 in '
+                   'multiples of 100 [only used for performance volumes] '
+                   '***If no IOPS value is specified, the IOPS value of the '
+                   'origin volume will be used.***\n'
+                   'Requirements: [If IOPS/GB for the origin volume is less '
+                   'than 0.3, IOPS/GB for the duplicate must also be less '
+                   'than 0.3. If IOPS/GB for the origin volume is greater '
+                   'than or equal to 0.3, IOPS/GB for the duplicate must '
+                   'also be greater than or equal to 0.3.]')
+@click.option('--duplicate-tier', '-t',
+              help='Endurance Storage Tier (IOPS per GB) [only used for '
+                   'endurance volumes] ***If no tier is specified, the tier '
+                   'of the origin volume will be used.***\n'
+                   'Requirements: [If IOPS/GB for the origin volume is 0.25, '
+                   'IOPS/GB for the duplicate must also be 0.25. If IOPS/GB '
+                   'for the origin volume is greater than 0.25, IOPS/GB '
+                   'for the duplicate must also be greater than 0.25.]',
+              type=click.Choice(['0.25', '2', '4', '10']))
+@click.option('--duplicate-snapshot-size', '-s',
+              type=int,
+              help='The size of snapshot space to order for the duplicate. '
+                   '***If no snapshot space size is specified, the snapshot '
+                   'space size of the origin volume will be used.***\n'
+                   'Input "0" for this parameter to order a duplicate volume '
+                   'with no snapshot space.')
+@environment.pass_env
+def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size,
+        duplicate_iops, duplicate_tier, duplicate_snapshot_size):
+    """Order a duplicate file storage volume."""
+    file_manager = SoftLayer.FileStorageManager(env.client)
+
+    if duplicate_tier is not None:
+        duplicate_tier = float(duplicate_tier)
+
+    try:
+        order = file_manager.order_duplicate_volume(
+            origin_volume_id,
+            origin_snapshot_id=origin_snapshot_id,
+            duplicate_size=duplicate_size,
+            duplicate_iops=duplicate_iops,
+            duplicate_tier_level=duplicate_tier,
+            duplicate_snapshot_size=duplicate_snapshot_size
+        )
+    except ValueError as ex:
+        raise exceptions.ArgumentError(str(ex))
+
+    if 'placedOrder' in order.keys():
+        click.echo("Order #{0} placed successfully!".format(
+            order['placedOrder']['id']))
+        for item in order['placedOrder']['items']:
+            click.echo(" > %s" % item['description'])
+    else:
+        click.echo("Order could not be placed! Please verify your options " +
+                   "and try again.")
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 3c4555db..ae33226b 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -76,6 +76,7 @@
     ('block:snapshot-restore', 'SoftLayer.CLI.block.snapshot.restore:cli'),
     ('block:volume-cancel', 'SoftLayer.CLI.block.cancel:cli'),
     ('block:volume-detail', 'SoftLayer.CLI.block.detail:cli'),
+    ('block:volume-duplicate', 'SoftLayer.CLI.block.duplicate:cli'),
     ('block:volume-list', 'SoftLayer.CLI.block.list:cli'),
     ('block:volume-order', 'SoftLayer.CLI.block.order:cli'),
 
@@ -98,6 +99,7 @@
     ('file:snapshot-restore', 'SoftLayer.CLI.file.snapshot.restore:cli'),
     ('file:volume-cancel', 'SoftLayer.CLI.file.cancel:cli'),
     ('file:volume-detail', 'SoftLayer.CLI.file.detail:cli'),
+    ('file:volume-duplicate', 'SoftLayer.CLI.file.duplicate:cli'),
     ('file:volume-list', 'SoftLayer.CLI.file.list:cli'),
     ('file:volume-order', 'SoftLayer.CLI.file.order:cli'),
 
diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py
index ea5b91e6..90c04662 100644
--- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py
+++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py
@@ -1,3 +1,34 @@
+DUPLICATABLE_VOLUME = {
+    'accountId': 1234,
+    'activeTransactions': None,
+    'activeTransactionCount': 0,
+    'billingItem': {
+        'activeChildren': [{
+            'categoryCode': 'storage_snapshot_space',
+            'id': 125,
+            'cancellationDate': '',
+        }],
+        'cancellationDate': '',
+        'id': 454,
+        'location': {'id': 449500}
+    },
+    'capacityGb': 500,
+    'id': 102,
+    'iops': 1000,
+    'lunId': 2,
+    'osType': {'keyName': 'LINUX'},
+    'originalVolumeSize': '500',
+    'parentVolume': {'snapshotSizeBytes': 1024},
+    'provisionedIops': '1000',
+    'replicationPartnerCount': 0,
+    'serviceResource': {'datacenter': {'id': 449500, 'name': 'dal05'}},
+    'serviceResourceBackendIpAddress': '10.1.2.3',
+    'snapshotCapacityGb': '10',
+    'storageTierLevel': 'READHEAVY_TIER',
+    'storageType': {'keyName': 'ENDURANCE_BLOCK_STORAGE'},
+    'username': 'duplicatable_volume_username'
+}
+
 getObject = {
     'accountId': 1234,
     'billingItem': {
@@ -8,7 +39,8 @@
             'categoryCode': 'storage_snapshot_space',
             'id': 123,
             'cancellationDate': '',
-        }]
+        }],
+        'location': {'id': 449500}
     },
     'capacityGb': 20,
     'createDate': '2015:50:15-04:00',
@@ -25,6 +57,9 @@
     'snapshotCapacityGb': '10',
     'parentVolume': {'snapshotSizeBytes': 1024},
     'osType': {'keyName': 'LINUX'},
+    'originalSnapshotName': 'test-origin-snapshot-name',
+    'originalVolumeName': 'test-origin-volume-name',
+    'originalVolumeSize': '20',
     'schedules': [{
         'id': 978,
         'type': {'keyname': 'SNAPSHOT_WEEKLY'},
diff --git a/SoftLayer/fixtures/SoftLayer_Product_Package.py b/SoftLayer/fixtures/SoftLayer_Product_Package.py
index 1a5fbed0..237b64a7 100644
--- a/SoftLayer/fixtures/SoftLayer_Product_Package.py
+++ b/SoftLayer/fixtures/SoftLayer_Product_Package.py
@@ -156,6 +156,139 @@
                  'sort': 99}]}]
 
 
+SAAS_PACKAGE = {
+    'id': 759,
+    'name': 'Storage As A Service (StaaS)',
+    'categories': [{'categoryCode': 'storage_as_a_service'}],
+    'items': [
+        {
+            'capacity': '0',
+            'keyName': '',
+            'prices': [{'id': 189433,
+                        'categories': [{
+                            'categoryCode': 'storage_as_a_service'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'keyName': '',
+            'prices': [{'categories': [{'categoryCode': 'storage_block'}],
+                        'id': 189443,
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'keyName': '',
+            'prices': [{'categories': [{'categoryCode': 'storage_file'}],
+                        'id': 189453,
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'capacityMaximum': '999',
+            'capacityMinimum': '500',
+            'itemCategory': {'categoryCode': 'performance_storage_space'},
+            'keyName': '500_999_GBS',
+            'prices': [{'id': 189993,
+                        'categories': [{
+                            'categoryCode': 'performance_storage_space'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'capacityMaximum': '1999',
+            'capacityMinimum': '1000',
+            'itemCategory': {'categoryCode': 'performance_storage_space'},
+            'keyName': '1000_1999_GBS',
+            'prices': [{'id': 190113,
+                        'categories': [{
+                            'categoryCode': 'performance_storage_space'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'capacityMaximum': '12000',
+            'capacityMinimum': '1',
+            'keyName': 'STORAGE_SPACE_FOR_2_IOPS_PER_GB',
+            'prices': [{'id': 193433,
+                        'categories': [{
+                            'categoryCode': 'performance_storage_space'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'capacityMaximum': '12000',
+            'capacityMinimum': '1',
+            'keyName': 'STORAGE_SPACE_FOR_4_IOPS_PER_GB',
+            'prices': [{'id': 194763,
+                        'categories': [{
+                            'categoryCode': 'performance_storage_space'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'capacityMaximum': '10000',
+            'capacityMinimum': '100',
+            'keyName': '',
+            'itemCategory': {'categoryCode': 'performance_storage_iops'},
+            'prices': [{'capacityRestrictionMaximum': '999',
+                        'capacityRestrictionMinimum': '500',
+                        'capacityRestrictionType': 'STORAGE_SPACE',
+                        'categories': [{
+                            'categoryCode': 'performance_storage_iops'}],
+                        'id': 190053,
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '0',
+            'capacityMaximum': '20000',
+            'capacityMinimum': '100',
+            'keyName': '',
+            'itemCategory': {'categoryCode': 'performance_storage_iops'},
+            'prices': [{'capacityRestrictionMaximum': '1999',
+                        'capacityRestrictionMinimum': '1000',
+                        'capacityRestrictionType': 'STORAGE_SPACE',
+                        'categories': [{
+                            'categoryCode': 'performance_storage_iops'}],
+                        'id': 190173,
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '200',
+            'itemCategory': {'categoryCode': 'storage_tier_level'},
+            'keyName': '',
+            'prices': [{'id': 193373,
+                        'categories': [{
+                            'categoryCode': 'storage_tier_level'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '300',
+            'itemCategory': {'categoryCode': 'storage_tier_level'},
+            'keyName': '',
+            'prices': [{'id': 194703,
+                        'categories': [{
+                            'categoryCode': 'storage_tier_level'}],
+                        'locationGroupId': ''}]
+        }, {
+            'capacity': '10',
+            'keyName': '',
+            'prices': [{'capacityRestrictionMaximum': '48000',
+                        'capacityRestrictionMinimum': '100',
+                        'capacityRestrictionType': 'IOPS',
+                        'categories': [{
+                            'categoryCode': 'storage_snapshot_space'}],
+                        'id': 191193,
+                        'locationGroupId': ''},
+                       {'capacityRestrictionMaximum': '200',
+                        'capacityRestrictionMinimum': '200',
+                        'capacityRestrictionType': 'STORAGE_TIER_LEVEL',
+                        'categories': [{
+                            'categoryCode': 'storage_snapshot_space'}],
+                        'id': 193613,
+                        'locationGroupId': ''},
+                       {'capacityRestrictionMaximum': '300',
+                        'capacityRestrictionMinimum': '300',
+                        'capacityRestrictionType': 'STORAGE_TIER_LEVEL',
+                        'categories': [{
+                            'categoryCode': 'storage_snapshot_space'}],
+                        'id': 194943,
+                        'locationGroupId': ''}]
+        }
+    ]
+}
+
+
 getAllObjects = [{
     'activePresets': [{
         'description': 'Single Xeon 1270, 8GB Ram, 2x1TB SATA disks, Non-RAID',
diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py
index 89660d13..6b2dc9d3 100644
--- a/SoftLayer/managers/block.py
+++ b/SoftLayer/managers/block.py
@@ -9,6 +9,8 @@
 from SoftLayer.managers import storage_utils
 from SoftLayer import utils
 
+# pylint: disable=too-many-public-methods
+
 
 class BlockStorageManager(utils.IdentifierMixin, object):
     """Manages SoftLayer Block Storage volumes.
@@ -89,6 +91,9 @@ def get_block_volume_details(self, volume_id, **kwargs):
                 'storageTierLevel',
                 'iops',
                 'lunId',
+                'originalVolumeName',
+                'originalSnapshotName',
+                'originalVolumeSize',
                 'activeTransactionCount',
                 'activeTransactions.transactionStatus[friendlyName]',
                 'replicationPartnerCount',
@@ -252,6 +257,45 @@ def order_replicant_volume(self, volume_id, snapshot_schedule,
 
         return self.client.call('Product_Order', 'placeOrder', order)
 
+    def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
+                               duplicate_size=None, duplicate_iops=None,
+                               duplicate_tier_level=None,
+                               duplicate_snapshot_size=None):
+        """Places an order for a duplicate block volume.
+
+        :param origin_volume_id: The ID of the origin volume to be duplicated
+        :param origin_snapshot_id: Origin snapshot ID to use for duplication
+        :param duplicate_size: Size/capacity for the duplicate volume
+        :param duplicate_iops: The IOPS per GB for the duplicate volume
+        :param duplicate_tier_level: Tier level for the duplicate volume
+        :param duplicate_snapshot_size: Snapshot space size for the duplicate
+        :return: Returns a SoftLayer_Container_Product_Order_Receipt
+        """
+
+        block_mask = 'id,billingItem[location],snapshotCapacityGb,'\
+                     'storageType[keyName],capacityGb,originalVolumeSize,'\
+                     'provisionedIops,storageTierLevel,osType[keyName]'
+        origin_volume = self.get_block_volume_details(origin_volume_id,
+                                                      mask=block_mask)
+
+        if isinstance(utils.lookup(origin_volume, 'osType', 'keyName'), str):
+            os_type = origin_volume['osType']['keyName']
+        else:
+            raise exceptions.SoftLayerError(
+                "Cannot find origin volume's os-type")
+
+        order = storage_utils.prepare_duplicate_order_object(
+            self, origin_volume, duplicate_iops, duplicate_tier_level,
+            duplicate_size, duplicate_snapshot_size, 'block'
+        )
+
+        order['osFormatType'] = {'keyName': os_type}
+
+        if origin_snapshot_id is not None:
+            order['duplicateOriginSnapshotId'] = origin_snapshot_id
+
+        return self.client.call('Product_Order', 'placeOrder', order)
+
     def delete_snapshot(self, snapshot_id):
         """Deletes the specified snapshot object.
 
diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py
index 22c73b45..6caf1f5f 100644
--- a/SoftLayer/managers/file.py
+++ b/SoftLayer/managers/file.py
@@ -9,6 +9,8 @@
 from SoftLayer.managers import storage_utils
 from SoftLayer import utils
 
+# pylint: disable=too-many-public-methods
+
 
 class FileStorageManager(utils.IdentifierMixin, object):
     """Manages file Storage volumes."""
@@ -86,6 +88,9 @@ def get_file_volume_details(self, volume_id, **kwargs):
                 'storageTierLevel',
                 'iops',
                 'lunId',
+                'originalVolumeName',
+                'originalSnapshotName',
+                'originalVolumeSize',
                 'activeTransactionCount',
                 'activeTransactions.transactionStatus[friendlyName]',
                 'replicationPartnerCount',
@@ -240,6 +245,37 @@ def get_replication_locations(self, volume_id):
                                 'getValidReplicationTargetDatacenterLocations',
                                 id=volume_id)
 
+    def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
+                               duplicate_size=None, duplicate_iops=None,
+                               duplicate_tier_level=None,
+                               duplicate_snapshot_size=None):
+        """Places an order for a duplicate file volume.
+
+        :param origin_volume_id: The ID of the origin volume to be duplicated
+        :param origin_snapshot_id: Origin snapshot ID to use for duplication
+        :param duplicate_size: Size/capacity for the duplicate volume
+        :param duplicate_iops: The IOPS per GB for the duplicate volume
+        :param duplicate_tier_level: Tier level for the duplicate volume
+        :param duplicate_snapshot_size: Snapshot space size for the duplicate
+        :return: Returns a SoftLayer_Container_Product_Order_Receipt
+        """
+
+        file_mask = 'id,billingItem[location],snapshotCapacityGb,'\
+                    'storageType[keyName],capacityGb,originalVolumeSize,'\
+                    'provisionedIops,storageTierLevel'
+        origin_volume = self.get_file_volume_details(origin_volume_id,
+                                                     mask=file_mask)
+
+        order = storage_utils.prepare_duplicate_order_object(
+            self, origin_volume, duplicate_iops, duplicate_tier_level,
+            duplicate_size, duplicate_snapshot_size, 'file'
+        )
+
+        if origin_snapshot_id is not None:
+            order['duplicateOriginSnapshotId'] = origin_snapshot_id
+
+        return self.client.call('Product_Order', 'placeOrder', order)
+
     def delete_snapshot(self, snapshot_id):
         """Deletes the specified snapshot object.
 
diff --git a/SoftLayer/managers/storage_utils.py b/SoftLayer/managers/storage_utils.py
index dcd7f04f..7cb46ab6 100644
--- a/SoftLayer/managers/storage_utils.py
+++ b/SoftLayer/managers/storage_utils.py
@@ -350,6 +350,215 @@ def find_snapshot_space_price(package, size, tier_level):
     raise ValueError("Could not find price for snapshot space")
 
 
+def find_saas_price_by_category(package, price_category):
+    """Find a price in the SaaS package with the specified category
+
+    :param package: The Storage As A Service product package
+    :param price_category: The price category to search for
+    :return: Returns a price for the given category, or an error if not found
+    """
+    for item in package['items']:
+        for price in item['prices']:
+            if price['locationGroupId'] != '':
+                continue
+
+            if not _has_category(price['categories'], price_category):
+                continue
+
+            return {'id': price['id']}
+
+    raise ValueError("Could not find price with the category, %s"
+                     % price_category)
+
+
+def find_saas_endurance_space_price(package, size, tier_level):
+    """Find the SaaS endurance storage space price for the size and tier
+
+    :param package: The Storage As A Service product package
+    :param size: The volume size for which a price is desired
+    :param tier_level: The endurance tier for which a price is desired
+    :return: Returns the price for the size and tier, or an error if not found
+    """
+    key_name = 'STORAGE_SPACE_FOR_{0}_IOPS_PER_GB'.format(tier_level)
+    key_name = key_name.replace(".", "_")
+    for item in package['items']:
+        if item['keyName'] != key_name:
+            continue
+
+        if 'capacityMinimum' not in item or 'capacityMaximum' not in item:
+            continue
+
+        capacity_minimum = int(item['capacityMinimum'])
+        capacity_maximum = int(item['capacityMaximum'])
+        if size < capacity_minimum or size > capacity_maximum:
+            continue
+
+        for price in item['prices']:
+            # Only collect prices from valid location groups.
+            if price['locationGroupId'] != '':
+                continue
+
+            if not _has_category(price['categories'],
+                                 'performance_storage_space'):
+                continue
+
+            return {'id': price['id']}
+
+    raise ValueError("Could not find price for endurance storage space")
+
+
+def find_saas_endurance_tier_price(package, tier_level):
+    """Find the SaaS storage tier level price for the specified tier level
+
+    :param package: The Storage As A Service product package
+    :param tier_level: The endurance tier for which a price is desired
+    :return: Returns the price for the given tier, or an error if not found
+    """
+    target_capacity = ENDURANCE_TIERS.get(tier_level)
+    for item in package['items']:
+        if 'itemCategory' not in item\
+                or 'categoryCode' not in item['itemCategory']\
+                or item['itemCategory']['categoryCode']\
+                != 'storage_tier_level':
+            continue
+
+        if int(item['capacity']) != target_capacity:
+            continue
+
+        for price in item['prices']:
+            # Only collect prices from valid location groups.
+            if price['locationGroupId'] != '':
+                continue
+
+            if not _has_category(price['categories'], 'storage_tier_level'):
+                continue
+
+            return {'id': price['id']}
+
+    raise ValueError("Could not find price for endurance tier level")
+
+
+def find_saas_perform_space_price(package, size):
+    """Find the SaaS performance storage space price for the given size
+
+    :param package: The Storage As A Service product package
+    :param size: The volume size for which a price is desired
+    :return: Returns the price for the size and tier, or an error if not found
+    """
+    for item in package['items']:
+        if 'itemCategory' not in item\
+                or 'categoryCode' not in item['itemCategory']\
+                or item['itemCategory']['categoryCode']\
+                != 'performance_storage_space':
+            continue
+
+        if 'capacityMinimum' not in item or 'capacityMaximum' not in item:
+            continue
+
+        capacity_minimum = int(item['capacityMinimum'])
+        capacity_maximum = int(item['capacityMaximum'])
+        if size < capacity_minimum or size > capacity_maximum:
+            continue
+
+        key_name = '{0}_{1}_GBS'.format(capacity_minimum, capacity_maximum)
+        if item['keyName'] != key_name:
+            continue
+
+        for price in item['prices']:
+            # Only collect prices from valid location groups.
+            if price['locationGroupId'] != '':
+                continue
+
+            if not _has_category(price['categories'],
+                                 'performance_storage_space'):
+                continue
+
+            return {'id': price['id']}
+
+    raise ValueError("Could not find price for performance storage space")
+
+
+def find_saas_perform_iops_price(package, size, iops):
+    """Find the SaaS IOPS price for the specified size and iops
+
+    :param package: The Storage As A Service product package
+    :param size: The volume size for which a price is desired
+    :param iops: The number of IOPS for which a price is desired
+    :return: Returns the price for the size and IOPS, or an error if not found
+    """
+    for item in package['items']:
+        if 'itemCategory' not in item\
+                or 'categoryCode' not in item['itemCategory']\
+                or item['itemCategory']['categoryCode']\
+                != 'performance_storage_iops':
+            continue
+
+        if 'capacityMinimum' not in item or 'capacityMaximum' not in item:
+            continue
+
+        capacity_minimum = int(item['capacityMinimum'])
+        capacity_maximum = int(item['capacityMaximum'])
+        if iops < capacity_minimum or iops > capacity_maximum:
+            continue
+
+        for price in item['prices']:
+            # Only collect prices from valid location groups.
+            if price['locationGroupId'] != '':
+                continue
+
+            if not _has_category(price['categories'],
+                                 'performance_storage_iops'):
+                continue
+
+            if price['capacityRestrictionType'] != 'STORAGE_SPACE'\
+                    or size < int(price['capacityRestrictionMinimum'])\
+                    or size > int(price['capacityRestrictionMaximum']):
+                continue
+
+            return {'id': price['id']}
+
+    raise ValueError("Could not find price for iops for the given volume")
+
+
+def find_saas_snapshot_space_price(package, size, tier_level=None, iops=None):
+    """Find the price in the SaaS package for the desired snapshot space size
+
+    :param package: The product package of the endurance storage type
+    :param size: The snapshot space size for which a price is desired
+    :param tier_level: The tier of the volume for which space is being ordered
+    :param iops: The IOPS of the volume for which space is being ordered
+    :return: Returns the price for the given size, or an error if not found
+    """
+    if tier_level is not None:
+        target_value = ENDURANCE_TIERS.get(tier_level)
+        target_restriction_type = 'STORAGE_TIER_LEVEL'
+    else:
+        target_value = iops
+        target_restriction_type = 'IOPS'
+
+    for item in package['items']:
+        if int(item['capacity']) != size:
+            continue
+
+        for price in item['prices']:
+            # Only collect prices from valid location groups.
+            if price['locationGroupId'] != '':
+                continue
+
+            if target_restriction_type != price['capacityRestrictionType']\
+                    or target_value < int(price['capacityRestrictionMinimum'])\
+                    or target_value > int(price['capacityRestrictionMaximum']):
+                continue
+
+            if not _has_category(price['categories'],
+                                 'storage_snapshot_space'):
+                continue
+
+            return {'id': price['id']}
+
+    raise ValueError("Could not find price for snapshot space")
+
+
 def find_snapshot_schedule_id(volume, snapshot_schedule_keyname):
     """Find the snapshot schedule ID for the given volume and keyname
 
@@ -446,6 +655,211 @@ def prepare_replicant_order_object(manager, volume_id, snapshot_schedule,
     return replicant_order
 
 
+def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
+                                   duplicate_size,
+                                   duplicate_snapshot_size, volume_type):
+    """Prepare the duplicate order to submit to SoftLayer_Product::placeOrder()
+
+    :param manager: The File or Block manager calling this function
+    :param origin_volume: The origin volume which is being duplicated
+    :param iops: The IOPS per GB for the duplicant volume (performance)
+    :param tier: The tier level for the duplicant volume (endurance)
+    :param duplicate_size: The requested size for the duplicate volume
+    :param duplicate_snapshot_size: The size for the duplicate snapshot space
+    :param volume_type: The type of the origin volume ('file' or 'block')
+    :return: Returns the order object to be passed to the
+             placeOrder() method of the Product_Order service
+    """
+
+    # Verify that the origin volume has not been cancelled
+    if 'billingItem' not in origin_volume:
+        raise exceptions.SoftLayerError(
+            "The origin volume has been cancelled; "
+            "unable to order duplicate volume")
+
+    # Verify that the origin volume has snapshot space (needed for duplication)
+    if isinstance(utils.lookup(origin_volume, 'snapshotCapacityGb'), str):
+        origin_snapshot_size = int(origin_volume['snapshotCapacityGb'])
+    else:
+        raise exceptions.SoftLayerError(
+            "Snapshot space not found for the origin volume. "
+            "Origin snapshot space is needed for duplication.")
+
+    # Obtain the datacenter location ID for the duplicate
+    if isinstance(utils.lookup(origin_volume, 'billingItem',
+                               'location', 'id'), int):
+        location_id = origin_volume['billingItem']['location']['id']
+    else:
+        raise exceptions.SoftLayerError(
+            "Cannot find origin volume's location")
+
+    # If no specific snapshot space was requested for the duplicate,
+    # use the origin snapshot space size
+    if duplicate_snapshot_size is None:
+        duplicate_snapshot_size = origin_snapshot_size
+
+    # Validate the requested duplicate size, and set the size if none was given
+    duplicate_size = _validate_duplicate_size(
+        origin_volume, duplicate_size, volume_type)
+
+    # Get the appropriate package for the order
+    # ('storage_as_a_service' is currently used for duplicate volumes)
+    package = get_package(manager, 'storage_as_a_service')
+
+    # Determine the IOPS or tier level for the duplicate volume, along with
+    # the type and prices for the order
+    origin_storage_type = origin_volume['storageType']['keyName']
+    if origin_storage_type == 'PERFORMANCE_BLOCK_STORAGE'\
+            or origin_storage_type == 'PERFORMANCE_BLOCK_STORAGE_REPLICANT'\
+            or origin_storage_type == 'PERFORMANCE_FILE_STORAGE'\
+            or origin_storage_type == 'PERFORMANCE_FILE_STORAGE_REPLICANT':
+        volume_is_performance = True
+        iops = _validate_dupl_performance_iops(
+            origin_volume, iops, duplicate_size)
+        # Set up the price array for the order
+        prices = [
+            find_saas_price_by_category(package, 'storage_as_a_service'),
+            find_saas_price_by_category(package, 'storage_' + volume_type),
+            find_saas_perform_space_price(package, duplicate_size),
+            find_saas_perform_iops_price(package, duplicate_size, iops),
+        ]
+        # Add the price code for snapshot space as well, unless 0 GB was given
+        if duplicate_snapshot_size > 0:
+            prices.append(find_saas_snapshot_space_price(
+                package, duplicate_snapshot_size, iops=iops))
+
+    elif origin_storage_type == 'ENDURANCE_BLOCK_STORAGE'\
+            or origin_storage_type == 'ENDURANCE_BLOCK_STORAGE_REPLICANT'\
+            or origin_storage_type == 'ENDURANCE_FILE_STORAGE'\
+            or origin_storage_type == 'ENDURANCE_FILE_STORAGE_REPLICANT':
+        volume_is_performance = False
+        tier = _validate_dupl_endurance_tier(origin_volume, tier)
+        # Set up the price array for the order
+        prices = [
+            find_saas_price_by_category(package, 'storage_as_a_service'),
+            find_saas_price_by_category(package, 'storage_' + volume_type),
+            find_saas_endurance_space_price(package, duplicate_size, tier),
+            find_saas_endurance_tier_price(package, tier),
+        ]
+        # Add the price code for snapshot space as well, unless 0 GB was given
+        if duplicate_snapshot_size > 0:
+            prices.append(find_saas_snapshot_space_price(
+                package, duplicate_snapshot_size, tier_level=tier))
+
+    else:
+        raise exceptions.SoftLayerError(
+            "Origin volume does not have a valid storage type "
+            "(with an appropriate keyName to indicate the "
+            "volume is a PERFORMANCE or ENDURANCE volume)")
+
+    duplicate_order = {
+        'complexType': 'SoftLayer_Container_Product_Order_'
+                       'Network_Storage_AsAService',
+        'packageId': package['id'],
+        'prices': prices,
+        'volumeSize': duplicate_size,
+        'quantity': 1,
+        'location': location_id,
+        'duplicateOriginVolumeId': origin_volume['id'],
+    }
+
+    if volume_is_performance:
+        duplicate_order['iops'] = iops
+
+    return duplicate_order
+
+
+def _validate_duplicate_size(origin_volume, duplicate_volume_size,
+                             volume_type):
+    # Ensure the origin volume's size is found
+    if not isinstance(utils.lookup(origin_volume, 'capacityGb'), int):
+        raise exceptions.SoftLayerError("Cannot find origin volume's size.")
+
+    # Determine the volume size/capacity for the duplicate
+    if duplicate_volume_size is None:
+        duplicate_volume_size = origin_volume['capacityGb']
+    # Ensure the duplicate volume size is not below the minimum
+    elif duplicate_volume_size < origin_volume['capacityGb']:
+        raise exceptions.SoftLayerError(
+            "The requested duplicate volume size is too small. Duplicate "
+            "volumes must be at least as large as their origin volumes.")
+
+    # Ensure the duplicate volume size is not above the maximum
+    if volume_type == 'block':
+        # Determine the base size for validating the requested duplicate size
+        if 'originalVolumeSize' in origin_volume:
+            base_volume_size = int(origin_volume['originalVolumeSize'])
+        else:
+            base_volume_size = origin_volume['capacityGb']
+
+        # Current limit for block volumes: 10*[origin size]
+        if duplicate_volume_size > base_volume_size * 10:
+            raise exceptions.SoftLayerError(
+                "The requested duplicate volume size is too large. The "
+                "maximum size for duplicate block volumes is 10 times the "
+                "size of the origin volume or, if the origin volume was also "
+                "a duplicate, 10 times the size of the initial origin volume "
+                "(i.e. the origin volume from which the first duplicate was "
+                "created in the chain of duplicates). "
+                "Requested: %s GB. Base origin size: %s GB."
+                % (duplicate_volume_size, base_volume_size))
+
+    return duplicate_volume_size
+
+
+def _validate_dupl_performance_iops(origin_volume, duplicate_iops,
+                                    duplicate_size):
+    if not isinstance(utils.lookup(origin_volume, 'provisionedIops'), str):
+        raise exceptions.SoftLayerError(
+            "Cannot find origin volume's provisioned IOPS")
+
+    if duplicate_iops is None:
+        duplicate_iops = int(origin_volume['provisionedIops'])
+    else:
+        origin_iops_per_gb = float(origin_volume['provisionedIops'])\
+            / float(origin_volume['capacityGb'])
+        duplicate_iops_per_gb = float(duplicate_iops) / float(duplicate_size)
+        if origin_iops_per_gb < 0.3 and duplicate_iops_per_gb >= 0.3:
+            raise exceptions.SoftLayerError(
+                "Origin volume performance is < 0.3 IOPS/GB, "
+                "duplicate volume performance must also be < 0.3 "
+                "IOPS/GB. %s IOPS/GB (%s/%s) requested."
+                % (duplicate_iops_per_gb, duplicate_iops, duplicate_size))
+        elif origin_iops_per_gb >= 0.3 and duplicate_iops_per_gb < 0.3:
+            raise exceptions.SoftLayerError(
+                "Origin volume performance is >= 0.3 IOPS/GB, "
+                "duplicate volume performance must also be >= 0.3 "
+                "IOPS/GB. %s IOPS/GB (%s/%s) requested."
+                % (duplicate_iops_per_gb, duplicate_iops, duplicate_size))
+    return duplicate_iops
+
+
+def _validate_dupl_endurance_tier(origin_volume, duplicate_tier):
+    try:
+        origin_tier = find_endurance_tier_iops_per_gb(origin_volume)
+    except ValueError:
+        raise exceptions.SoftLayerError(
+            "Cannot find origin volume's tier level")
+
+    if duplicate_tier is None:
+        duplicate_tier = origin_tier
+    else:
+        if duplicate_tier != 0.25:
+            duplicate_tier = int(duplicate_tier)
+
+        if origin_tier == 0.25 and duplicate_tier != 0.25:
+            raise exceptions.SoftLayerError(
+                "Origin volume performance tier is 0.25 IOPS/GB, "
+                "duplicate volume performance tier must also be 0.25 "
+                "IOPS/GB. %s IOPS/GB requested." % duplicate_tier)
+        elif origin_tier != 0.25 and duplicate_tier == 0.25:
+            raise exceptions.SoftLayerError(
+                "Origin volume performance tier is above 0.25 IOPS/GB, "
+                "duplicate volume performance tier must also be above 0.25 "
+                "IOPS/GB. %s IOPS/GB requested." % duplicate_tier)
+    return duplicate_tier
+
+
 def _has_category(categories, category_code):
     return any(
         True


Test patch:
diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py
index 58c46a0e..87641216 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -67,6 +67,7 @@ def test_volume_cancel(self):
 
     def test_volume_detail(self):
         result = self.run_command(['block', 'volume-detail', '1234'])
+
         self.assert_no_fail(result)
         self.assertEqual({
             'Username': 'username',
@@ -94,7 +95,13 @@ def test_volume_detail(self):
                 {'Replicant ID': 'Target IP', '1785': '10.3.177.84'},
                 {'Replicant ID': 'Data Center', '1785': 'dal01'},
                 {'Replicant ID': 'Schedule', '1785': 'REPLICATION_DAILY'},
-            ]]
+            ]],
+            'Duplicate Volume Properties': [
+                {'Original Volume Name': 'Original Volume Size',
+                 'test-origin-volume-name': '20'},
+                {'Original Volume Name': 'Original Snapshot Name',
+                 'test-origin-volume-name': 'test-origin-snapshot-name'}
+            ]
         }, json.loads(result.output))
 
     def test_volume_list(self):
@@ -462,3 +469,45 @@ def test_replicant_order(self, order_mock):
                          ' > 20 GB Storage Space\n'
                          ' > 10 GB Storage Space (Snapshot Space)\n'
                          ' > 20 GB Storage Space Replicant of: TEST\n')
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_duplicate_volume')
+    def test_duplicate_order_exception_caught(self, order_mock):
+        order_mock.side_effect = ValueError('order attempt failed, oh noooo!')
+
+        result = self.run_command(['block', 'volume-duplicate', '102'])
+
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual('Argument Error: order attempt failed, oh noooo!',
+                         result.exception.message)
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_duplicate_volume')
+    def test_duplicate_order_order_not_placed(self, order_mock):
+        order_mock.return_value = {}
+
+        result = self.run_command(['block', 'volume-duplicate', '102',
+                                   '--duplicate-iops=1400'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order could not be placed! Please verify '
+                         'your options and try again.\n')
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_duplicate_volume')
+    def test_duplicate_order(self, order_mock):
+        order_mock.return_value = {
+            'placedOrder': {
+                'id': 24601,
+                'items': [{'description': 'Storage as a Service'}]
+            }
+        }
+
+        result = self.run_command(['block', 'volume-duplicate', '102',
+                                   '--origin-snapshot-id=470',
+                                   '--duplicate-size=250',
+                                   '--duplicate-tier=2',
+                                   '--duplicate-snapshot-size=20'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order #24601 placed successfully!\n'
+                         ' > Storage as a Service\n')
diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py
index c3b3b7a4..5d53210f 100644
--- a/tests/CLI/modules/file_tests.py
+++ b/tests/CLI/modules/file_tests.py
@@ -145,7 +145,13 @@ def test_volume_detail(self):
                 {'Replicant ID': 'Target IP', '1785': '10.3.177.84'},
                 {'Replicant ID': 'Data Center', '1785': 'dal01'},
                 {'Replicant ID': 'Schedule', '1785': 'REPLICATION_DAILY'},
-            ]]
+            ]],
+            'Duplicate Volume Properties': [
+                {'Original Volume Name': 'Original Volume Size',
+                 'test-origin-volume-name': '20'},
+                {'Original Volume Name': 'Original Snapshot Name',
+                 'test-origin-volume-name': 'test-origin-snapshot-name'}
+            ]
         }, json.loads(result.output))
 
     def test_volume_order_performance_iops_not_given(self):
@@ -466,3 +472,45 @@ def test_replication_partners_unsuccessful(self, partners_mock):
         self.assertEqual(
             'There are no replication partners for the given volume.\n',
             result.output)
+
+    @mock.patch('SoftLayer.FileStorageManager.order_duplicate_volume')
+    def test_duplicate_order_exception_caught(self, order_mock):
+        order_mock.side_effect = ValueError('order attempt failed, oh noooo!')
+
+        result = self.run_command(['file', 'volume-duplicate', '100'])
+
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual('Argument Error: order attempt failed, oh noooo!',
+                         result.exception.message)
+
+    @mock.patch('SoftLayer.FileStorageManager.order_duplicate_volume')
+    def test_duplicate_order_order_not_placed(self, order_mock):
+        order_mock.return_value = {}
+
+        result = self.run_command(['file', 'volume-duplicate', '100',
+                                   '--duplicate-iops=1400'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order could not be placed! Please verify '
+                         'your options and try again.\n')
+
+    @mock.patch('SoftLayer.FileStorageManager.order_duplicate_volume')
+    def test_duplicate_order(self, order_mock):
+        order_mock.return_value = {
+            'placedOrder': {
+                'id': 24602,
+                'items': [{'description': 'Storage as a Service'}]
+            }
+        }
+
+        result = self.run_command(['file', 'volume-duplicate', '100',
+                                   '--origin-snapshot-id=470',
+                                   '--duplicate-size=250',
+                                   '--duplicate-tier=2',
+                                   '--duplicate-snapshot-size=20'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order #24602 placed successfully!\n'
+                         ' > Storage as a Service\n')
diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py
index e336fbbb..91909f52 100644
--- a/tests/managers/block_tests.py
+++ b/tests/managers/block_tests.py
@@ -30,10 +30,35 @@ def test_get_block_volume_details(self):
 
         self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, result)
 
+        expected_mask = 'id,'\
+                        'username,'\
+                        'password,'\
+                        'capacityGb,'\
+                        'snapshotCapacityGb,'\
+                        'parentVolume.snapshotSizeBytes,'\
+                        'storageType.keyName,'\
+                        'serviceResource.datacenter[name],'\
+                        'serviceResourceBackendIpAddress,'\
+                        'storageTierLevel,'\
+                        'iops,'\
+                        'lunId,'\
+                        'originalVolumeName,'\
+                        'originalSnapshotName,'\
+                        'originalVolumeSize,'\
+                        'activeTransactionCount,'\
+                        'activeTransactions.transactionStatus[friendlyName],'\
+                        'replicationPartnerCount,'\
+                        'replicationStatus,'\
+                        'replicationPartners[id,username,'\
+                        'serviceResourceBackendIpAddress,'\
+                        'serviceResource[datacenter[name]],'\
+                        'replicationSchedule[type[keyname]]]'
+
         self.assert_called_with(
             'SoftLayer_Network_Storage',
             'getObject',
-            identifier=100
+            identifier=100,
+            mask='mask[%s]' % expected_mask
             )
 
     def test_list_block_volumes(self):
@@ -969,3 +994,184 @@ def test_order_block_replicant(self):
                     'setupFee': '1'}],
                 },
             )
+
+    def test_order_block_duplicate_origin_os_type_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_os_type = mock_volume['osType']
+        del mock_volume['osType']
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            self.block.order_duplicate_volume,
+            102
+        )
+
+        self.assertEqual(str(exception),
+                         "Cannot find origin volume's os-type")
+
+        mock_volume['osType'] = prev_os_type
+
+    def test_order_block_duplicate_performance_no_duplicate_snapshot(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.block.order_duplicate_volume(
+            102,
+            duplicate_snapshot_size=0)
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189443},
+                    {'id': 189993},
+                    {'id': 190053}
+                ],
+                'volumeSize': 500,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'osFormatType': {'keyName': 'LINUX'},
+                'iops': 1000
+            },))
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_order_block_duplicate_performance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.block.order_duplicate_volume(
+            102,
+            origin_snapshot_id=470,
+            duplicate_size=1000,
+            duplicate_iops=2000,
+            duplicate_tier_level=None,
+            duplicate_snapshot_size=10
+            )
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189443},
+                    {'id': 190113},
+                    {'id': 190173},
+                    {'id': 191193}
+                ],
+                'volumeSize': 1000,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'osFormatType': {'keyName': 'LINUX'},
+                'duplicateOriginSnapshotId': 470,
+                'iops': 2000
+            },))
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_order_block_duplicate_endurance_no_duplicate_snapshot(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.block.order_duplicate_volume(
+            102,
+            duplicate_snapshot_size=0)
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189443},
+                    {'id': 193433},
+                    {'id': 193373}
+                ],
+                'volumeSize': 500,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'osFormatType': {'keyName': 'LINUX'}
+            },))
+
+    def test_order_block_duplicate_endurance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.block.order_duplicate_volume(
+            102,
+            origin_snapshot_id=470,
+            duplicate_size=1000,
+            duplicate_iops=None,
+            duplicate_tier_level=4,
+            duplicate_snapshot_size=10
+            )
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189443},
+                    {'id': 194763},
+                    {'id': 194703},
+                    {'id': 194943}
+                ],
+                'volumeSize': 1000,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'osFormatType': {'keyName': 'LINUX'},
+                'duplicateOriginSnapshotId': 470
+            },))
diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py
index 3b761e0c..ea313b3d 100644
--- a/tests/managers/file_tests.py
+++ b/tests/managers/file_tests.py
@@ -105,10 +105,37 @@ def test_get_file_volume_details(self):
 
         self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, result)
 
+        expected_mask = 'id,'\
+                        'username,'\
+                        'password,'\
+                        'capacityGb,'\
+                        'bytesUsed,'\
+                        'snapshotCapacityGb,'\
+                        'parentVolume.snapshotSizeBytes,'\
+                        'storageType.keyName,'\
+                        'serviceResource.datacenter[name],'\
+                        'serviceResourceBackendIpAddress,'\
+                        'fileNetworkMountAddress,'\
+                        'storageTierLevel,'\
+                        'iops,'\
+                        'lunId,'\
+                        'originalVolumeName,'\
+                        'originalSnapshotName,'\
+                        'originalVolumeSize,'\
+                        'activeTransactionCount,'\
+                        'activeTransactions.transactionStatus[friendlyName],'\
+                        'replicationPartnerCount,'\
+                        'replicationStatus,'\
+                        'replicationPartners[id,username,'\
+                        'serviceResourceBackendIpAddress,'\
+                        'serviceResource[datacenter[name]],'\
+                        'replicationSchedule[type[keyname]]]'
+
         self.assert_called_with(
             'SoftLayer_Network_Storage',
             'getObject',
-            identifier=100)
+            identifier=100,
+            mask='mask[%s]' % expected_mask)
 
     def test_get_file_volume_snapshot_list(self):
         result = self.file.get_file_volume_snapshot_list(100)
@@ -902,3 +929,167 @@ def test_order_file_replicant(self):
                     'setupFee': '1'}],
                 },
             )
+
+    def test_order_file_duplicate_performance_no_duplicate_snapshot(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.file.order_duplicate_volume(
+            102,
+            duplicate_snapshot_size=0)
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189453},
+                    {'id': 189993},
+                    {'id': 190053}
+                ],
+                'volumeSize': 500,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'iops': 1000
+            },))
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_order_file_duplicate_performance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.file.order_duplicate_volume(
+            102,
+            origin_snapshot_id=470,
+            duplicate_size=1000,
+            duplicate_iops=2000,
+            duplicate_tier_level=None,
+            duplicate_snapshot_size=10
+            )
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189453},
+                    {'id': 190113},
+                    {'id': 190173},
+                    {'id': 191193}
+                ],
+                'volumeSize': 1000,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'duplicateOriginSnapshotId': 470,
+                'iops': 2000
+            },))
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_order_file_duplicate_endurance_no_duplicate_snapshot(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.file.order_duplicate_volume(
+            102,
+            duplicate_snapshot_size=0)
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189453},
+                    {'id': 193433},
+                    {'id': 193373}
+                ],
+                'volumeSize': 500,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102
+            },))
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_order_file_duplicate_endurance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.file.order_duplicate_volume(
+            102,
+            origin_snapshot_id=470,
+            duplicate_size=1000,
+            duplicate_iops=None,
+            duplicate_tier_level=4,
+            duplicate_snapshot_size=10
+            )
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189453},
+                    {'id': 194763},
+                    {'id': 194703},
+                    {'id': 194943}
+                ],
+                'volumeSize': 1000,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'duplicateOriginSnapshotId': 470
+            },))
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
diff --git a/tests/managers/storage_utils_tests.py b/tests/managers/storage_utils_tests.py
new file mode 100644
index 00000000..b875ccee
--- /dev/null
+++ b/tests/managers/storage_utils_tests.py
@@ -0,0 +1,1715 @@
+"""
+    SoftLayer.tests.managers.storage_utils_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :license: MIT, see LICENSE for more details.
+"""
+
+import SoftLayer
+from SoftLayer import exceptions
+from SoftLayer import fixtures
+from SoftLayer.managers import storage_utils
+from SoftLayer import testing
+
+
+class StorageUtilsTests(testing.TestCase):
+    def set_up(self):
+        self.block = SoftLayer.BlockStorageManager(self.client)
+        self.file = SoftLayer.FileStorageManager(self.client)
+
+    def test_find_saas_price_by_category_no_items_in_package(self):
+        package = {
+            'items': []}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_price_by_category,
+            package, 'storage_as_a_service'
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price with the category, "
+                         "storage_as_a_service")
+
+    def test_find_saas_price_by_category_no_prices_in_items(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'prices': []}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_price_by_category,
+            package, 'storage_as_a_service'
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price with the category, "
+                         "storage_as_a_service")
+
+    def test_find_saas_price_by_category_empty_location_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'prices': [
+                     {'id': 189433,
+                      'categories': [
+                          {'categoryCode': 'storage_as_a_service'}
+                      ],
+                      'locationGroupId': '77777777'}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_price_by_category,
+            package, 'storage_as_a_service'
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price with the category, "
+                         "storage_as_a_service")
+
+    def test_find_saas_price_by_category_category_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'prices': [
+                     {'id': 189433,
+                      'categories': [
+                          {'categoryCode': 'invalid_category_noooo'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_price_by_category,
+            package, 'storage_as_a_service'
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price with the category, "
+                         "storage_as_a_service")
+
+    def test_find_saas_price_by_category(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'prices': [
+                     {'id': 189433,
+                      'categories': [
+                          {'categoryCode': 'storage_as_a_service'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_price_by_category(
+            package, 'storage_as_a_service')
+
+        self.assertEqual({'id': 189433}, result)
+
+    def test_find_saas_endurance_space_price_no_items_in_package(self):
+        package = {
+            'items': []}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_no_matching_keyname(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'keyName': 'STORAGE_SPACE_FOR_2_IOPS_PER_GB'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_no_capacity_maximum(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_no_capacity_minimum(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_size_below_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 0, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_size_above_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 12001, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_no_prices_in_items(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB',
+                 'prices': []}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_empty_location_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB',
+                 'prices': [
+                     {'id': 192103,
+                      'categories': [
+                          {'categoryCode': 'performance_storage_space'}
+                      ],
+                      'locationGroupId': '77777777'}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price_category_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB',
+                 'prices': [
+                     {'id': 192103,
+                      'categories': [
+                          {'categoryCode': 'invalid_category_noooo'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_space_price,
+            package, 8000, 0.25
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance storage space")
+
+    def test_find_saas_endurance_space_price(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '12000',
+                 'capacityMinimum': '1',
+                 'keyName': 'STORAGE_SPACE_FOR_0_25_IOPS_PER_GB',
+                 'prices': [
+                     {'id': 192103,
+                      'categories': [
+                          {'categoryCode': 'performance_storage_space'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_endurance_space_price(
+            package, 8000, 0.25)
+
+        self.assertEqual({'id': 192103}, result)
+
+    def test_find_saas_endurance_tier_price_no_items_in_package(self):
+        package = {
+            'items': []}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_no_itemCategory(self):
+        package = {
+            'items': [
+                {'capacity': '200'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_no_itemCategory_code(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_no_matching_itemCategory(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {'categoryCode': 'invalid_category_noooo'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_no_matching_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {'categoryCode': 'storage_tier_level'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 10
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_no_prices_in_items(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {'categoryCode': 'storage_tier_level'},
+                 'prices': []}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_empty_location_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {'categoryCode': 'storage_tier_level'},
+                 'prices': [
+                     {'id': 193373,
+                      'categories': [
+                          {'categoryCode': 'storage_tier_level'}
+                      ],
+                      'locationGroupId': '77777777'}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price_category_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {'categoryCode': 'storage_tier_level'},
+                 'prices': [
+                     {'id': 193373,
+                      'categories': [
+                          {'categoryCode': 'invalid_category_noooo'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_endurance_tier_price,
+            package, 2
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for endurance tier level")
+
+    def test_find_saas_endurance_tier_price(self):
+        package = {
+            'items': [
+                {'capacity': '200',
+                 'itemCategory': {'categoryCode': 'storage_tier_level'},
+                 'prices': [
+                     {'id': 193373,
+                      'categories': [
+                          {'categoryCode': 'storage_tier_level'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_endurance_tier_price(
+            package, 2)
+
+        self.assertEqual({'id': 193373}, result)
+
+    def test_find_saas_perform_space_price_no_items_in_package(self):
+        package = {
+            'items': []}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_itemCategory(self):
+        package = {
+            'items': [
+                {'capacity': '0'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_itemCategory_code(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'itemCategory': {}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_matching_itemCategory(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'itemCategory': {'categoryCode': 'invalid_category_noooo'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_capacity_maximum(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_capacity_minimum(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_size_below_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 499
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_size_above_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 1000
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_matching_keyname(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': 'NOT_THE_CORRECT_KEYNAME'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_no_prices_in_items(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS',
+                 'prices': []}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_empty_location_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS',
+                 'prices': [
+                     {'id': 189993,
+                      'categories': [
+                          {'categoryCode': 'performance_storage_space'}
+                      ],
+                      'locationGroupId': '77777777'}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price_category_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS',
+                 'prices': [
+                     {'id': 189993,
+                      'categories': [
+                          {'categoryCode': 'invalid_category_noooo'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_space_price,
+            package, 500
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for performance storage space")
+
+    def test_find_saas_perform_space_price(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '999',
+                 'capacityMinimum': '500',
+                 'itemCategory': {'categoryCode': 'performance_storage_space'},
+                 'keyName': '500_999_GBS',
+                 'prices': [
+                     {'id': 189993,
+                      'categories': [
+                          {'categoryCode': 'performance_storage_space'}
+                      ],
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_perform_space_price(
+            package, 500)
+
+        self.assertEqual({'id': 189993}, result)
+
+    def test_find_saas_perform_iops_price_no_items_in_package(self):
+        package = {
+            'items': []}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_no_itemCategory(self):
+        package = {
+            'items': [
+                {'capacity': '0'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_no_itemCategory_code(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'itemCategory': {}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_no_matching_itemCategory(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'itemCategory': {'categoryCode': 'invalid_category_noooo'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_no_capacity_maximum(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_no_capacity_minimum(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_iops_below_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 99
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_iops_above_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'}}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 10001
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_no_prices_in_items(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': []}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_empty_location_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': [
+                     {'capacityRestrictionMaximum': '999',
+                      'capacityRestrictionMinimum': '500',
+                      'capacityRestrictionType': 'STORAGE_SPACE',
+                      'categories': [
+                          {'categoryCode': 'performance_storage_iops'}
+                      ],
+                      'id': 190053,
+                      'locationGroupId': '77777777'}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_category_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': [
+                     {'capacityRestrictionMaximum': '999',
+                      'capacityRestrictionMinimum': '500',
+                      'capacityRestrictionType': 'STORAGE_SPACE',
+                      'categories': [
+                          {'categoryCode': 'invalid_category_noooo'}
+                      ],
+                      'id': 190053,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_wrong_capacity_restriction(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': [
+                     {'capacityRestrictionMaximum': '999',
+                      'capacityRestrictionMinimum': '500',
+                      'capacityRestrictionType': 'NOT_THE_CORRECT_TYPE',
+                      'categories': [
+                          {'categoryCode': 'performance_storage_iops'}
+                      ],
+                      'id': 190053,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 500, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_size_below_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': [
+                     {'capacityRestrictionMaximum': '999',
+                      'capacityRestrictionMinimum': '500',
+                      'capacityRestrictionType': 'STORAGE_SPACE',
+                      'categories': [
+                          {'categoryCode': 'performance_storage_iops'}
+                      ],
+                      'id': 190053,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 499, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price_size_above_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': [
+                     {'capacityRestrictionMaximum': '999',
+                      'capacityRestrictionMinimum': '500',
+                      'capacityRestrictionType': 'STORAGE_SPACE',
+                      'categories': [
+                          {'categoryCode': 'performance_storage_iops'}
+                      ],
+                      'id': 190053,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_perform_iops_price,
+            package, 1000, 1700
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for iops for the given volume")
+
+    def test_find_saas_perform_iops_price(self):
+        package = {
+            'items': [
+                {'capacity': '0',
+                 'capacityMaximum': '10000',
+                 'capacityMinimum': '100',
+                 'itemCategory': {'categoryCode': 'performance_storage_iops'},
+                 'prices': [
+                     {'capacityRestrictionMaximum': '999',
+                      'capacityRestrictionMinimum': '500',
+                      'capacityRestrictionType': 'STORAGE_SPACE',
+                      'categories': [
+                          {'categoryCode': 'performance_storage_iops'}
+                      ],
+                      'id': 190053,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_perform_iops_price(
+            package, 500, 1700)
+
+        self.assertEqual({'id': 190053}, result)
+
+    def test_find_saas_snapshot_space_price_no_items_in_package(self):
+        package = {
+            'items': []}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=2100
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_no_matching_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '-1'}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=2100
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_no_prices_in_items(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': []}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=2100
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_empty_location_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '48000',
+                      'capacityRestrictionMinimum': '100',
+                      'capacityRestrictionType': 'IOPS',
+                      'categories': [
+                          {'categoryCode': 'storage_snapshot_space'}
+                      ],
+                      'id': 191193,
+                      'locationGroupId': '77777777'}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=2100
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_wrong_capacity_restriction(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '48000',
+                      'capacityRestrictionMinimum': '100',
+                      'capacityRestrictionType': 'NOT_THE_CORRECT_CATEGORY',
+                      'categories': [
+                          {'categoryCode': 'storage_snapshot_space'}
+                      ],
+                      'id': 191193,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=2100
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_target_value_below_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '48000',
+                      'capacityRestrictionMinimum': '100',
+                      'capacityRestrictionType': 'IOPS',
+                      'categories': [
+                          {'categoryCode': 'storage_snapshot_space'}
+                      ],
+                      'id': 191193,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=99
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_target_value_above_capacity(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '48000',
+                      'capacityRestrictionMinimum': '100',
+                      'capacityRestrictionType': 'IOPS',
+                      'categories': [
+                          {'categoryCode': 'storage_snapshot_space'}
+                      ],
+                      'id': 191193,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=48001
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_category_not_found(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '48000',
+                      'capacityRestrictionMinimum': '100',
+                      'capacityRestrictionType': 'IOPS',
+                      'categories': [
+                          {'categoryCode': 'invalid_category_noooooooo'}
+                      ],
+                      'id': 191193,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        exception = self.assertRaises(
+            ValueError,
+            storage_utils.find_saas_snapshot_space_price,
+            package, 10, iops=2100
+        )
+
+        self.assertEqual(str(exception),
+                         "Could not find price for snapshot space")
+
+    def test_find_saas_snapshot_space_price_with_iops(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '48000',
+                      'capacityRestrictionMinimum': '100',
+                      'capacityRestrictionType': 'IOPS',
+                      'categories': [
+                          {'categoryCode': 'storage_snapshot_space'}
+                      ],
+                      'id': 191193,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_snapshot_space_price(
+            package, 10, iops=2100)
+
+        self.assertEqual({'id': 191193}, result)
+
+    def test_find_saas_snapshot_space_price_with_tier_level(self):
+        package = {
+            'items': [
+                {'capacity': '10',
+                 'prices': [
+                     {'capacityRestrictionMaximum': '200',
+                      'capacityRestrictionMinimum': '200',
+                      'capacityRestrictionType': 'STORAGE_TIER_LEVEL',
+                      'categories': [
+                          {'categoryCode': 'storage_snapshot_space'}
+                      ],
+                      'id': 193613,
+                      'locationGroupId': ''}
+                 ]}
+            ]}
+
+        result = storage_utils.find_saas_snapshot_space_price(
+            package, 10, tier_level=2)
+
+        self.assertEqual({'id': 193613}, result)
+
+    def test_prep_duplicate_order_origin_volume_cancelled(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_billing_item = mock_volume['billingItem']
+        del mock_volume['billingItem']
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "The origin volume has been cancelled; "
+                         "unable to order duplicate volume")
+
+        mock_volume['billingItem'] = prev_billing_item
+
+    def test_prep_duplicate_order_origin_snapshot_capacity_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_snapshot_capacity_gb = mock_volume['snapshotCapacityGb']
+        del mock_volume['snapshotCapacityGb']
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Snapshot space not found for the origin volume. "
+                         "Origin snapshot space is needed for duplication.")
+
+        mock_volume['snapshotCapacityGb'] = prev_snapshot_capacity_gb
+
+    def test_prep_duplicate_order_origin_volume_location_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_location = mock_volume['billingItem']['location']
+        del mock_volume['billingItem']['location']
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Cannot find origin volume's location")
+
+        mock_volume['billingItem']['location'] = prev_location
+
+    def test_prep_duplicate_order_origin_volume_capacity_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_capacity_gb = mock_volume['capacityGb']
+        mock_volume['capacityGb'] = None
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception), "Cannot find origin volume's size.")
+
+        mock_volume['capacityGb'] = prev_capacity_gb
+
+    def test_prep_duplicate_order_origin_originalVolumeSize_empty_block(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_original_volume_size = mock_volume['originalVolumeSize']
+        del mock_volume['originalVolumeSize']
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189443},
+                {'id': 193433},
+                {'id': 193373},
+                {'id': 193613}
+            ],
+            'volumeSize': 500,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.block, mock_volume, None, None, None, None, 'block')
+
+        self.assertEqual(expected_object, result)
+
+        mock_volume['originalVolumeSize'] = prev_original_volume_size
+
+    def test_prep_duplicate_order_size_too_small(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, 250, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "The requested duplicate volume size is too small. "
+                         "Duplicate volumes must be at least as large as "
+                         "their origin volumes.")
+
+    def test_prep_duplicate_order_size_too_large_block(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, 8000, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "The requested duplicate volume size is too large. "
+                         "The maximum size for duplicate block volumes is 10 "
+                         "times the size of the origin volume or, if the "
+                         "origin volume was also a duplicate, 10 times the "
+                         "size of the initial origin volume (i.e. the origin "
+                         "volume from which the first duplicate was created "
+                         "in the chain of duplicates). "
+                         "Requested: 8000 GB. Base origin size: 500 GB.")
+
+    def test_prep_duplicate_order_performance_origin_iops_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        prev_provisioned_iops = mock_volume['provisionedIops']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_'\
+                                                'STORAGE_REPLICANT'
+        mock_volume['provisionedIops'] = None
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Cannot find origin volume's provisioned IOPS")
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+        mock_volume['provisionedIops'] = prev_provisioned_iops
+
+    def test_prep_duplicate_order_performance_iops_above_limit(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        prev_provisioned_iops = mock_volume['provisionedIops']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+        mock_volume['provisionedIops'] = '100'
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, 1000, None, 500, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Origin volume performance is < 0.3 IOPS/GB, "
+                         "duplicate volume performance must also be < 0.3 "
+                         "IOPS/GB. 2.0 IOPS/GB (1000/500) requested.")
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+        mock_volume['provisionedIops'] = prev_provisioned_iops
+
+    def test_prep_duplicate_order_performance_iops_below_limit(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, 200, None, 1000, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Origin volume performance is >= 0.3 IOPS/GB, "
+                         "duplicate volume performance must also be >= 0.3 "
+                         "IOPS/GB. 0.2 IOPS/GB (200/1000) requested.")
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_performance_use_default_origin_values(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_'\
+                                                'STORAGE_REPLICANT'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189453},
+                {'id': 189993},
+                {'id': 190053},
+                {'id': 191193}
+            ],
+            'volumeSize': 500,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102,
+            'iops': 1000}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.file, mock_volume, None, None, None, None, 'file')
+
+        self.assertEqual(expected_object, result)
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_performance_block(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189443},
+                {'id': 190113},
+                {'id': 190173},
+                {'id': 191193}
+            ],
+            'volumeSize': 1000,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102,
+            'iops': 2000}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.block, mock_volume, 2000, None, 1000, 10, 'block')
+
+        self.assertEqual(expected_object, result)
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_performance_file(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189453},
+                {'id': 190113},
+                {'id': 190173},
+                {'id': 191193}
+            ],
+            'volumeSize': 1000,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102,
+            'iops': 2000}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.file, mock_volume, 2000, None, 1000, 10, 'file')
+
+        self.assertEqual(expected_object, result)
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_endurance_origin_tier_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_tier_level = mock_volume['storageTierLevel']
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageTierLevel'] = 'NINJA_PENGUINS'
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_BLOCK_'\
+                                                'STORAGE_REPLICANT'
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Cannot find origin volume's tier level")
+
+        mock_volume['storageTierLevel'] = prev_tier_level
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_endurance_tier_above_limit(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_tier_level = mock_volume['storageTierLevel']
+        mock_volume['storageTierLevel'] = 'LOW_INTENSITY_TIER'
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, 2, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Origin volume performance tier is 0.25 IOPS/GB, "
+                         "duplicate volume performance tier must also be 0.25 "
+                         "IOPS/GB. 2 IOPS/GB requested.")
+
+        mock_volume['storageTierLevel'] = prev_tier_level
+
+    def test_prep_duplicate_order_endurance_tier_below_limit(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, 0.25, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Origin volume performance tier is above 0.25 "
+                         "IOPS/GB, duplicate volume performance tier must "
+                         "also be above 0.25 IOPS/GB. 0.25 IOPS/GB requested.")
+
+    def test_prep_duplicate_order_endurance_use_default_origin_values(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_'\
+                                                'STORAGE_REPLICANT'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189453},
+                {'id': 193433},
+                {'id': 193373},
+                {'id': 193613}
+            ],
+            'volumeSize': 500,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.file, mock_volume, None, None, None, None, 'file')
+
+        self.assertEqual(expected_object, result)
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_endurance_block(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189443},
+                {'id': 194763},
+                {'id': 194703},
+                {'id': 194943}
+            ],
+            'volumeSize': 1000,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.block, mock_volume, None, 4.0, 1000, 10, 'block')
+
+        self.assertEqual(expected_object, result)
+
+    def test_prep_duplicate_order_endurance_file(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_'
+                           'Network_Storage_AsAService',
+            'packageId': 759,
+            'prices': [
+                {'id': 189433},
+                {'id': 189453},
+                {'id': 194763},
+                {'id': 194703},
+                {'id': 194943}
+            ],
+            'volumeSize': 1000,
+            'quantity': 1,
+            'location': 449500,
+            'duplicateOriginVolumeId': 102}
+
+        result = storage_utils.prepare_duplicate_order_object(
+            self.file, mock_volume, None, 4.0, 1000, 10, 'file')
+
+        self.assertEqual(expected_object, result)
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname
+
+    def test_prep_duplicate_order_invalid_origin_storage_type(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = fixtures.SoftLayer_Network_Storage.DUPLICATABLE_VOLUME
+        prev_storage_type_keyname = mock_volume['storageType']['keyName']
+        mock_volume['storageType']['keyName'] = 'NINJA_CATS'
+
+        exception = self.assertRaises(
+            exceptions.SoftLayerError,
+            storage_utils.prepare_duplicate_order_object,
+            self.block, mock_volume, None, None, None, None, 'block'
+        )
+
+        self.assertEqual(str(exception),
+                         "Origin volume does not have a valid storage type "
+                         "(with an appropriate keyName to indicate the "
+                         "volume is a PERFORMANCE or ENDURANCE volume)")
+
+        mock_volume['storageType']['keyName'] = prev_storage_type_keyname