Repository: softlayer/softlayer-python
Pull request #893 (2017-11-15 02:06:35)

Feature/fblock 58 - Added volume-modify feature for File/Block.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14259326/badge)](https://coveralls.io/builds/14259326)

Coverage decreased (-0.8%) to 84.774% when pulling **8e84d8c298b8231407ee13042bd62116e94a1123 on sghatty:feature/FBLOCK-58** into **dc97e7ec9526c682c18ca16c0bedce2510a94a0f on softlayer:master**.

allmightyspiff: the CI build is still complaining about the following.
```
SoftLayer/managers/storage_utils.py:1083:9: E265 block comment should start with '# '
SoftLayer/managers/storage_utils.py:1183:1: E302 expected 2 blank lines, found 1
```

Also, please try to improve the test coverage as well.

Thanks.
sghatty: Hi Chris,
 
I fixed those errors, and a couple of test cases.
 
Regards,Surya Ghatty"This too shall pass"________________________________________________________________________________________________________Surya Ghatty | Software Engineer | IBM Cloud Infrastructure Services Development | tel: (507) 316-0559 | ghatty@us.ibm.com
 
 
----- Original message -----From: Christopher Gallo <notifications@github.com>To: softlayer/softlayer-python <softlayer-python@noreply.github.com>Cc: sghatty <ghatty@us.ibm.com>, Author <author@noreply.github.com>Subject: Re: [softlayer/softlayer-python] Feature/fblock 58 - Added volume-modify feature for File/Block. (#893)Date: Fri, Nov 17, 2017 2:56 PM 
the CI build is still complaining about the following.
SoftLayer/managers/storage_utils.py:1083:9: E265 block comment should start with '# 'SoftLayer/managers/storage_utils.py:1183:1: E302 expected 2 blank lines, found 1
Also, please try to improve the test coverage as well.
Thanks.
—You are receiving this because you authored the thread.Reply to this email directly, view it on GitHub, or mute the thread.

 
dpickle2: @allmightyspiff regarding the commands you were trying to run above, were you pointing to a current production API endpoint? The API changes are still in development/QE, so you would need to use a custom endpoint for these commands to work properly.
Also, regarding item 3 in the changes you requested, I apologize that the helper functions that these were based on were not preferable as it is. Had I known that several months ago when they were written, I would have been happy to modify them them. If I reduce or eliminate these in the way I mentioned in my review comment above, will that likely bring them into an acceptable state, or is further action also desired? Please let me know, and feel free to send me a direct message through some other medium if that is easier. Thank you!

coveralls: 
[![Coverage Status](https://coveralls.io/builds/14311937/badge)](https://coveralls.io/builds/14311937)

Coverage decreased (-0.8%) to 84.78% when pulling **f38259aca54739a5453f219d4df24d763d2e139b on sghatty:feature/FBLOCK-58** into **dc97e7ec9526c682c18ca16c0bedce2510a94a0f on softlayer:master**.

allmightyspiff: @dpickle2  yeah removing those validation functions would  solve a lot of my complaints about the pull request. Once that is done I'll test again against the the testing url.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/14343617/badge)](https://coveralls.io/builds/14343617)

Coverage decreased (-0.8%) to 84.772% when pulling **3002c46ab51d61f011a92ff69fb2b744113b9817 on sghatty:feature/FBLOCK-58** into **dc97e7ec9526c682c18ca16c0bedce2510a94a0f on softlayer:master**.


Patch:
diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py
index 70a41c0c..ecfd5c4d 100644
--- a/SoftLayer/CLI/block/detail.py
+++ b/SoftLayer/CLI/block/detail.py
@@ -62,12 +62,10 @@ def cli(env, volume_id):
 
     if block_volume['activeTransactions']:
         for trans in block_volume['activeTransactions']:
-            table.add_row([
-                'Ongoing Transactions',
-                trans['transactionStatus']['friendlyName']])
+            if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']:
+                table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']])
 
-    table.add_row(['Replicant Count', "%u"
-                   % block_volume['replicationPartnerCount']])
+    table.add_row(['Replicant Count', "%u" % block_volume.get('replicationPartnerCount', 0)])
 
     if block_volume['replicationPartnerCount'] > 0:
         # This if/else temporarily handles a bug in which the SL API
@@ -102,12 +100,12 @@ def cli(env, volume_id):
         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])
+        original_volume_info = formatting.Table(['Property', 'Value'])
+        original_volume_info.add_row(['Original Volume Size', block_volume['originalVolumeSize']])
+        if block_volume.get('originalVolumeName'):
+            original_volume_info.add_row(['Original Volume Name', block_volume['originalVolumeName']])
+        if block_volume.get('originalSnapshotName'):
+            original_volume_info.add_row(['Original Snapshot Name', block_volume['originalSnapshotName']])
+        table.add_row(['Original Volume Properties', original_volume_info])
 
     env.fout(table)
diff --git a/SoftLayer/CLI/block/duplicate.py b/SoftLayer/CLI/block/duplicate.py
index 0ecf5911..ec728f87 100644
--- a/SoftLayer/CLI/block/duplicate.py
+++ b/SoftLayer/CLI/block/duplicate.py
@@ -22,9 +22,7 @@
                    '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)]')
+                   'Minimum: [the size of the origin volume]')
 @click.option('--duplicate-iops', '-i',
               type=int,
               help='Performance Storage IOPS, between 100 and 6000 in '
diff --git a/SoftLayer/CLI/block/modify.py b/SoftLayer/CLI/block/modify.py
new file mode 100644
index 00000000..3697ddd7
--- /dev/null
+++ b/SoftLayer/CLI/block/modify.py
@@ -0,0 +1,57 @@
+"""Modify an existing 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('volume-id')
+@click.option('--new-size', '-c',
+              type=int,
+              help='New Size of block volume in GB. ***If no size is given, the original size of volume is used.***\n'
+                   'Potential Sizes: [20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000]\n'
+                   'Minimum: [the original size of the volume]')
+@click.option('--new-iops', '-i',
+              type=int,
+              help='Performance Storage IOPS, between 100 and 6000 in multiples of 100 [only for performance volumes] '
+                   '***If no IOPS value is specified, the original IOPS value of the volume will be used.***\n'
+                   'Requirements: [If original IOPS/GB for the volume is less than 0.3, new IOPS/GB must also be '
+                   'less than 0.3. If original IOPS/GB for the volume is greater than or equal to 0.3, new IOPS/GB '
+                   'for the volume must also be greater than or equal to 0.3.]')
+@click.option('--new-tier', '-t',
+              help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] '
+                   '***If no tier is specified, the original tier of the volume will be used.***\n'
+                   'Requirements: [If original IOPS/GB for the volume is 0.25, new IOPS/GB for the volume must also '
+                   'be 0.25. If original IOPS/GB for the volume is greater than 0.25, new IOPS/GB for the volume '
+                   'must also be greater than 0.25.]',
+              type=click.Choice(['0.25', '2', '4', '10']))
+@environment.pass_env
+def cli(env, volume_id, new_size, new_iops, new_tier):
+    """Modify an existing block storage volume."""
+    block_manager = SoftLayer.BlockStorageManager(env.client)
+
+    if new_tier is not None:
+        new_tier = float(new_tier)
+
+    try:
+        order = block_manager.order_modified_volume(
+            volume_id,
+            new_size=new_size,
+            new_iops=new_iops,
+            new_tier_level=new_tier,
+        )
+    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 96437dc3..cb712dc9 100644
--- a/SoftLayer/CLI/file/detail.py
+++ b/SoftLayer/CLI/file/detail.py
@@ -78,12 +78,10 @@ def cli(env, volume_id):
 
     if file_volume['activeTransactions']:
         for trans in file_volume['activeTransactions']:
-            table.add_row([
-                'Ongoing Transactions',
-                trans['transactionStatus']['friendlyName']])
+            if 'transactionStatus' in trans and 'friendlyName' in trans['transactionStatus']:
+                table.add_row(['Ongoing Transaction', trans['transactionStatus']['friendlyName']])
 
-    table.add_row(['Replicant Count', "%u"
-                   % file_volume['replicationPartnerCount']])
+    table.add_row(['Replicant Count', "%u" % file_volume.get('replicationPartnerCount', 0)])
 
     if file_volume['replicationPartnerCount'] > 0:
         # This if/else temporarily handles a bug in which the SL API
@@ -118,12 +116,12 @@ def cli(env, volume_id):
         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])
+        original_volume_info = formatting.Table(['Property', 'Value'])
+        original_volume_info.add_row(['Original Volume Size', file_volume['originalVolumeSize']])
+        if file_volume.get('originalVolumeName'):
+            original_volume_info.add_row(['Original Volume Name', file_volume['originalVolumeName']])
+        if file_volume.get('originalSnapshotName'):
+            original_volume_info.add_row(['Original Snapshot Name', file_volume['originalSnapshotName']])
+        table.add_row(['Original Volume Properties', original_volume_info])
 
     env.fout(table)
diff --git a/SoftLayer/CLI/file/modify.py b/SoftLayer/CLI/file/modify.py
new file mode 100644
index 00000000..5e0c097b
--- /dev/null
+++ b/SoftLayer/CLI/file/modify.py
@@ -0,0 +1,57 @@
+"""Modify an existing 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('volume-id')
+@click.option('--new-size', '-c',
+              type=int,
+              help='New Size of file volume in GB. ***If no size is given, the original size of volume is used.***\n'
+                   'Potential Sizes: [20, 40, 80, 100, 250, 500, 1000, 2000, 4000, 8000, 12000]\n'
+                   'Minimum: [the original size of the volume]')
+@click.option('--new-iops', '-i',
+              type=int,
+              help='Performance Storage IOPS, between 100 and 6000 in multiples of 100 [only for performance volumes] '
+                   '***If no IOPS value is specified, the original IOPS value of the volume will be used.***\n'
+                   'Requirements: [If original IOPS/GB for the volume is less than 0.3, new IOPS/GB must also be '
+                   'less than 0.3. If original IOPS/GB for the volume is greater than or equal to 0.3, new IOPS/GB '
+                   'for the volume must also be greater than or equal to 0.3.]')
+@click.option('--new-tier', '-t',
+              help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] '
+                   '***If no tier is specified, the original tier of the volume will be used.***\n'
+                   'Requirements: [If original IOPS/GB for the volume is 0.25, new IOPS/GB for the volume must also '
+                   'be 0.25. If original IOPS/GB for the volume is greater than 0.25, new IOPS/GB for the volume '
+                   'must also be greater than 0.25.]',
+              type=click.Choice(['0.25', '2', '4', '10']))
+@environment.pass_env
+def cli(env, volume_id, new_size, new_iops, new_tier):
+    """Modify an existing file storage volume."""
+    file_manager = SoftLayer.FileStorageManager(env.client)
+
+    if new_tier is not None:
+        new_tier = float(new_tier)
+
+    try:
+        order = file_manager.order_modified_volume(
+            volume_id,
+            new_size=new_size,
+            new_iops=new_iops,
+            new_tier_level=new_tier,
+        )
+    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 2ff3379a..3b406ec2 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -80,6 +80,7 @@
     ('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-modify', 'SoftLayer.CLI.block.modify:cli'),
     ('block:volume-order', 'SoftLayer.CLI.block.order:cli'),
     ('block:volume-set-lun-id', 'SoftLayer.CLI.block.lun:cli'),
 
@@ -105,6 +106,7 @@
     ('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-modify', 'SoftLayer.CLI.file.modify:cli'),
     ('file:volume-order', 'SoftLayer.CLI.file.order:cli'),
 
     ('firewall', 'SoftLayer.CLI.firewall'),
diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py
index b4dd0b75..80996cd7 100644
--- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py
+++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py
@@ -39,8 +39,10 @@
 
 getObject = {
     'accountId': 1234,
-    'activeTransactionCount': 0,
-    'activeTransactions': None,
+    'activeTransactionCount': 1,
+    'activeTransactions': [{
+        'transactionStatus': {'friendlyName': 'This is a buffer time in which the customer may cancel the server'}
+    }],
     'allowedHardware': [{
         'allowedHost': {
             'credential': {'username': 'joe', 'password': '12345'},
@@ -104,8 +106,8 @@
     'lunId': 2,
     'nasType': 'ISCSI',
     'notes': """{'status': 'available'}""",
-    'originalSnapshotName': 'test-origin-snapshot-name',
-    'originalVolumeName': 'test-origin-volume-name',
+    'originalSnapshotName': 'test-original-snapshot-name',
+    'originalVolumeName': 'test-original-volume-name',
     'originalVolumeSize': '20',
     'osType': {'keyName': 'LINUX'},
     'parentVolume': {'snapshotSizeBytes': 1024},
diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py
index 76631201..1e5931fc 100644
--- a/SoftLayer/managers/block.py
+++ b/SoftLayer/managers/block.py
@@ -303,6 +303,35 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
 
         return self.client.call('Product_Order', 'placeOrder', order)
 
+    def order_modified_volume(self, volume_id, new_size=None, new_iops=None, new_tier_level=None):
+        """Places an order for modifying an existing block volume.
+
+        :param volume_id: The ID of the volume to be modified
+        :param new_size: The new size/capacity for the volume
+        :param new_iops: The new IOPS for the volume
+        :param new_tier_level: The new tier level for the volume
+        :return: Returns a SoftLayer_Container_Product_Order_Receipt
+        """
+
+        mask_items = [
+            'id',
+            'billingItem',
+            'storageType[keyName]',
+            'capacityGb',
+            'provisionedIops',
+            'storageTierLevel',
+            'staasVersion',
+            'hasEncryptionAtRest',
+        ]
+        block_mask = ','.join(mask_items)
+        volume = self.get_block_volume_details(volume_id, mask=block_mask)
+
+        order = storage_utils.prepare_modify_order_object(
+            self, volume, new_iops, new_tier_level, new_size
+        )
+
+        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 2ba39cca..e4a5ac23 100644
--- a/SoftLayer/managers/file.py
+++ b/SoftLayer/managers/file.py
@@ -283,6 +283,35 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
 
         return self.client.call('Product_Order', 'placeOrder', order)
 
+    def order_modified_volume(self, volume_id, new_size=None, new_iops=None, new_tier_level=None):
+        """Places an order for modifying an existing file volume.
+
+        :param volume_id: The ID of the volume to be modified
+        :param new_size: The new size/capacity for the volume
+        :param new_iops: The new IOPS for the volume
+        :param new_tier_level: The new tier level for the volume
+        :return: Returns a SoftLayer_Container_Product_Order_Receipt
+        """
+
+        mask_items = [
+            'id',
+            'billingItem',
+            'storageType[keyName]',
+            'capacityGb',
+            'provisionedIops',
+            'storageTierLevel',
+            'staasVersion',
+            'hasEncryptionAtRest',
+        ]
+        file_mask = ','.join(mask_items)
+        volume = self.get_file_volume_details(volume_id, mask=file_mask)
+
+        order = storage_utils.prepare_modify_order_object(
+            self, volume, new_iops, new_tier_level, new_size
+        )
+
+        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 5040078e..b6e1a3d4 100644
--- a/SoftLayer/managers/storage_utils.py
+++ b/SoftLayer/managers/storage_utils.py
@@ -947,8 +947,7 @@ def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
         if iops is None:
             iops = int(origin_volume.get('provisionedIops', 0))
             if iops <= 0:
-                raise exceptions.SoftLayerError(
-                    "Cannot find origin volume's provisioned IOPS")
+                raise exceptions.SoftLayerError("Cannot find origin volume's provisioned IOPS")
         # Set up the price array for the order
         prices = [
             find_price_by_category(package, 'storage_as_a_service'),
@@ -1001,6 +1000,85 @@ def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
     return duplicate_order
 
 
+def prepare_modify_order_object(manager, volume, new_iops, new_tier, new_size):
+    """Prepare the modification order to submit to SoftLayer_Product::placeOrder()
+
+    :param manager: The File or Block manager calling this function
+    :param volume: The volume which is being modified
+    :param new_iops: The new IOPS for the volume (performance)
+    :param new_tier: The new tier level for the volume (endurance)
+    :param new_size: The requested new size for the volume
+    :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 volume:
+        raise exceptions.SoftLayerError("The volume has been cancelled; unable to modify volume.")
+
+    # Ensure the origin volume is STaaS v2 or higher and supports Encryption at Rest
+    if not _staas_version_is_v2_or_above(volume):
+        raise exceptions.SoftLayerError("This volume cannot be modified since it does not support Encryption at Rest.")
+
+    # Get the appropriate package for the order ('storage_as_a_service' is currently used for modifying volumes)
+    package = get_package(manager, 'storage_as_a_service')
+
+    # Based on volume storage type, ensure at least one volume property is being modified,
+    # use current values if some are not specified, and lookup price codes for the order
+    volume_storage_type = volume['storageType']['keyName']
+    if 'PERFORMANCE' in volume_storage_type:
+        volume_is_performance = True
+        if new_size is None and new_iops is None:
+            raise exceptions.SoftLayerError("A size or IOPS value must be given to modify this performance volume.")
+
+        if new_size is None:
+            new_size = volume['capacityGb']
+        elif new_iops is None:
+            new_iops = int(volume.get('provisionedIops', 0))
+            if new_iops <= 0:
+                raise exceptions.SoftLayerError("Cannot find volume's provisioned IOPS.")
+
+        # Set up the prices array for the order
+        prices = [
+            find_price_by_category(package, 'storage_as_a_service'),
+            find_saas_perform_space_price(package, new_size),
+            find_saas_perform_iops_price(package, new_size, new_iops),
+        ]
+
+    elif 'ENDURANCE' in volume_storage_type:
+        volume_is_performance = False
+        if new_size is None and new_tier is None:
+            raise exceptions.SoftLayerError("A size or tier value must be given to modify this endurance volume.")
+
+        if new_size is None:
+            new_size = volume['capacityGb']
+        elif new_tier is None:
+            new_tier = find_endurance_tier_iops_per_gb(volume)
+
+        # Set up the prices array for the order
+        prices = [
+            find_price_by_category(package, 'storage_as_a_service'),
+            find_saas_endurance_space_price(package, new_size, new_tier),
+            find_saas_endurance_tier_price(package, new_tier),
+        ]
+
+    else:
+        raise exceptions.SoftLayerError("Volume does not have a valid storage type (with an appropriate "
+                                        "keyName to indicate the volume is a PERFORMANCE or an ENDURANCE volume).")
+
+    modify_order = {
+        'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+        'packageId': package['id'],
+        'prices': prices,
+        'volume': {'id': volume['id']},
+        'volumeSize': new_size
+    }
+
+    if volume_is_performance:
+        modify_order['iops'] = new_iops
+
+    return modify_order
+
+
 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 d7ad2c3c..352871b1 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -71,7 +71,8 @@ def test_volume_detail(self):
             'Data Center': 'dal05',
             'Type': 'ENDURANCE',
             'ID': 100,
-            '# of Active Transactions': '0',
+            '# of Active Transactions': '1',
+            'Ongoing Transaction': 'This is a buffer time in which the customer may cancel the server',
             'Replicant Count': '1',
             'Replication Status': 'Replicant Volume Provisioning '
                                   'has completed.',
@@ -86,11 +87,13 @@ def test_volume_detail(self):
                 {'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'}
+            'Original Volume Properties': [
+                {'Property': 'Original Volume Size',
+                 'Value': '20'},
+                {'Property': 'Original Volume Name',
+                 'Value': 'test-original-volume-name'},
+                {'Property': 'Original Snapshot Name',
+                 'Value': 'test-original-snapshot-name'}
             ]
         }, json.loads(result.output))
 
@@ -599,6 +602,37 @@ def test_duplicate_order_hourly_billing(self, order_mock):
                          'Order #24602 placed successfully!\n'
                          ' > Storage as a Service\n')
 
+    @mock.patch('SoftLayer.BlockStorageManager.order_modified_volume')
+    def test_modify_order_exception_caught(self, order_mock):
+        order_mock.side_effect = ValueError('order attempt failed, noooo!')
+
+        result = self.run_command(['block', 'volume-modify', '102', '--new-size=1000'])
+
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual('Argument Error: order attempt failed, noooo!', result.exception.message)
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_modified_volume')
+    def test_modify_order_order_not_placed(self, order_mock):
+        order_mock.return_value = {}
+
+        result = self.run_command(['block', 'volume-modify', '102', '--new-iops=1400'])
+
+        self.assert_no_fail(result)
+        self.assertEqual('Order could not be placed! Please verify your options and try again.\n', result.output)
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_modified_volume')
+    def test_modify_order(self, order_mock):
+        order_mock.return_value = {'placedOrder': {'id': 24602, 'items': [{'description': 'Storage as a Service'},
+                                                                          {'description': '1000 GBs'},
+                                                                          {'description': '4 IOPS per GB'}]}}
+
+        result = self.run_command(['block', 'volume-modify', '102', '--new-size=1000', '--new-tier=4'])
+
+        order_mock.assert_called_with('102', new_size=1000, new_iops=None, new_tier_level=4)
+        self.assert_no_fail(result)
+        self.assertEqual('Order #24602 placed successfully!\n > Storage as a Service\n > 1000 GBs\n > 4 IOPS per GB\n',
+                         result.output)
+
     def test_set_password(self):
         result = self.run_command(['block', 'access-password', '1234', '--password=AAAAA'])
         self.assert_no_fail(result)
diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py
index 6aad4564..6614e115 100644
--- a/tests/CLI/modules/file_tests.py
+++ b/tests/CLI/modules/file_tests.py
@@ -111,7 +111,8 @@ def test_volume_detail(self):
             'Data Center': 'dal05',
             'Type': 'ENDURANCE',
             'ID': 100,
-            '# of Active Transactions': '0',
+            '# of Active Transactions': '1',
+            'Ongoing Transaction': 'This is a buffer time in which the customer may cancel the server',
             'Replicant Count': '1',
             'Replication Status': 'Replicant Volume Provisioning '
                                   'has completed.',
@@ -126,11 +127,13 @@ def test_volume_detail(self):
                 {'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'}
+            'Original Volume Properties': [
+                {'Property': 'Original Volume Size',
+                 'Value': '20'},
+                {'Property': 'Original Volume Name',
+                 'Value': 'test-original-volume-name'},
+                {'Property': 'Original Snapshot Name',
+                 'Value': 'test-original-snapshot-name'}
             ]
         }, json.loads(result.output))
 
@@ -577,3 +580,34 @@ def test_duplicate_order_hourly_billing(self, order_mock):
         self.assertEqual(result.output,
                          'Order #24602 placed successfully!\n'
                          ' > Storage as a Service\n')
+
+    @mock.patch('SoftLayer.FileStorageManager.order_modified_volume')
+    def test_modify_order_exception_caught(self, order_mock):
+        order_mock.side_effect = ValueError('order attempt failed, noooo!')
+
+        result = self.run_command(['file', 'volume-modify', '102', '--new-size=1000'])
+
+        self.assertEqual(2, result.exit_code)
+        self.assertEqual('Argument Error: order attempt failed, noooo!', result.exception.message)
+
+    @mock.patch('SoftLayer.FileStorageManager.order_modified_volume')
+    def test_modify_order_order_not_placed(self, order_mock):
+        order_mock.return_value = {}
+
+        result = self.run_command(['file', 'volume-modify', '102', '--new-iops=1400'])
+
+        self.assert_no_fail(result)
+        self.assertEqual('Order could not be placed! Please verify your options and try again.\n', result.output)
+
+    @mock.patch('SoftLayer.FileStorageManager.order_modified_volume')
+    def test_modify_order(self, order_mock):
+        order_mock.return_value = {'placedOrder': {'id': 24602, 'items': [{'description': 'Storage as a Service'},
+                                                                          {'description': '1000 GBs'},
+                                                                          {'description': '4 IOPS per GB'}]}}
+
+        result = self.run_command(['file', 'volume-modify', '102', '--new-size=1000', '--new-tier=4'])
+
+        order_mock.assert_called_with('102', new_size=1000, new_iops=None, new_tier_level=4)
+        self.assert_no_fail(result)
+        self.assertEqual('Order #24602 placed successfully!\n > Storage as a Service\n > 1000 GBs\n > 4 IOPS per GB\n',
+                         result.output)
diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py
index d3ebe922..2f6cf2ab 100644
--- a/tests/managers/block_tests.py
+++ b/tests/managers/block_tests.py
@@ -832,6 +832,50 @@ def test_order_block_duplicate_endurance(self):
                 'useHourlyPricing': False
             },))
 
+    def test_order_block_modified_performance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.block.order_modified_volume(102, new_size=1000, new_iops=2000, new_tier_level=None)
+
+        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_Upgrade',
+                   'packageId': 759,
+                   'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}],
+                   'volume': {'id': 102},
+                   'volumeSize': 1000,
+                   'iops': 2000},)
+        )
+
+    def test_order_block_modified_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.STAAS_TEST_VOLUME
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.block.order_modified_volume(102, new_size=1000, new_iops=None, new_tier_level=4)
+
+        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_Upgrade',
+                   'packageId': 759,
+                   'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}],
+                   'volume': {'id': 102},
+                   'volumeSize': 1000},)
+        )
+
     def test_setCredentialPassword(self):
         mock = self.set_mock('SoftLayer_Network_Storage_Allowed_Host', 'setCredentialPassword')
         mock.return_value = True
diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py
index 6a6d14d8..389682cd 100644
--- a/tests/managers/file_tests.py
+++ b/tests/managers/file_tests.py
@@ -785,3 +785,48 @@ def test_order_file_duplicate_endurance(self):
                 'duplicateOriginSnapshotId': 470,
                 'useHourlyPricing': False
             },))
+
+    def test_order_file_modified_performance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.file.order_modified_volume(102, new_size=1000, new_iops=2000, new_tier_level=None)
+
+        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_Upgrade',
+                   'packageId': 759,
+                   'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}],
+                   'volume': {'id': 102},
+                   'volumeSize': 1000,
+                   'iops': 2000},)
+        )
+
+    def test_order_file_modified_endurance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE'
+        mock = self.set_mock('SoftLayer_Network_Storage', 'getObject')
+        mock.return_value = mock_volume
+
+        result = self.file.order_modified_volume(102, new_size=1000, new_iops=None, new_tier_level=4)
+
+        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_Upgrade',
+                   'packageId': 759,
+                   'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}],
+                   'volume': {'id': 102},
+                   'volumeSize': 1000},)
+        )
diff --git a/tests/managers/storage_utils_tests.py b/tests/managers/storage_utils_tests.py
index c3676d9c..7c32e083 100644
--- a/tests/managers/storage_utils_tests.py
+++ b/tests/managers/storage_utils_tests.py
@@ -3851,3 +3851,187 @@ def test_prep_duplicate_order_invalid_origin_storage_type(self):
                          "Origin volume does not have a valid storage type "
                          "(with an appropriate keyName to indicate the "
                          "volume is a PERFORMANCE or an ENDURANCE volume)")
+
+    # ---------------------------------------------------------------------
+    # Tests for prepare_modify_order_object()
+    # ---------------------------------------------------------------------
+    def test_prep_modify_order_origin_volume_cancelled(self):
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        del mock_volume['billingItem']
+
+        exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object,
+                                      self.block, mock_volume, None, None, None)
+
+        self.assertEqual("The volume has been cancelled; unable to modify volume.", str(exception))
+
+    def test_prep_modify_order_origin_volume_staas_version_below_v2(self):
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['staasVersion'] = 1
+
+        exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object,
+                                      self.block, mock_volume, None, None, None)
+
+        self.assertEqual("This volume cannot be modified since it does not support Encryption at Rest.",
+                         str(exception))
+
+    def test_prep_modify_order_performance_values_not_given(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+
+        exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object,
+                                      self.block, mock_volume, None, None, None)
+
+        self.assertEqual("A size or IOPS value must be given to modify this performance volume.", str(exception))
+
+    def test_prep_modify_order_performance_iops_not_found(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+        del mock_volume['provisionedIops']
+
+        exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object,
+                                      self.block, mock_volume, None, None, 40)
+
+        self.assertEqual("Cannot find volume's provisioned IOPS.", str(exception))
+
+    def test_prep_modify_order_performance_use_existing_iops(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+            'packageId': 759,
+            'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}],
+            'volume': {'id': 102},
+            'volumeSize': 1000,
+            'iops': 1000
+        }
+
+        result = storage_utils.prepare_modify_order_object(self.file, mock_volume, None, None, 1000)
+        self.assertEqual(expected_object, result)
+
+    def test_prep_modify_order_performance_use_existing_size(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_BLOCK_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+            'packageId': 759,
+            'prices': [{'id': 189433}, {'id': 189993}, {'id': 190053}],
+            'volume': {'id': 102},
+            'volumeSize': 500,
+            'iops': 2000
+        }
+
+        result = storage_utils.prepare_modify_order_object(self.block, mock_volume, 2000, None, None)
+        self.assertEqual(expected_object, result)
+
+    def test_prep_modify_order_performance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'PERFORMANCE_FILE_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+            'packageId': 759,
+            'prices': [{'id': 189433}, {'id': 190113}, {'id': 190173}],
+            'volume': {'id': 102},
+            'volumeSize': 1000,
+            'iops': 2000
+        }
+
+        result = storage_utils.prepare_modify_order_object(self.file, mock_volume, 2000, None, 1000)
+        self.assertEqual(expected_object, result)
+
+    def test_prep_modify_order_endurance_values_not_given(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_BLOCK_STORAGE'
+
+        exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object,
+                                      self.block, mock_volume, None, None, None)
+
+        self.assertEqual("A size or tier value must be given to modify this endurance volume.", str(exception))
+
+    def test_prep_modify_order_endurance_use_existing_tier(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+            'packageId': 759,
+            'prices': [{'id': 189433}, {'id': 193433}, {'id': 193373}],
+            'volume': {'id': 102},
+            'volumeSize': 1000
+        }
+
+        result = storage_utils.prepare_modify_order_object(self.file, mock_volume, None, None, 1000)
+        self.assertEqual(expected_object, result)
+
+    def test_prep_modify_order_endurance_use_existing_size(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_BLOCK_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+            'packageId': 759,
+            'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}],
+            'volume': {'id': 102},
+            'volumeSize': 500
+        }
+
+        result = storage_utils.prepare_modify_order_object(self.block, mock_volume, None, 4, None)
+        self.assertEqual(expected_object, result)
+
+    def test_prep_modify_order_endurance(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'ENDURANCE_FILE_STORAGE'
+
+        expected_object = {
+            'complexType': 'SoftLayer_Container_Product_Order_Network_Storage_AsAService_Upgrade',
+            'packageId': 759,
+            'prices': [{'id': 189433}, {'id': 194763}, {'id': 194703}],
+            'volume': {'id': 102},
+            'volumeSize': 1000
+        }
+
+        result = storage_utils.prepare_modify_order_object(self.file, mock_volume, None, 4, 1000)
+        self.assertEqual(expected_object, result)
+
+    def test_prep_modify_order_invalid_volume_storage_type(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
+
+        mock_volume = copy.deepcopy(fixtures.SoftLayer_Network_Storage.STAAS_TEST_VOLUME)
+        mock_volume['storageType']['keyName'] = 'NINJA_PENGUINS'
+
+        exception = self.assertRaises(exceptions.SoftLayerError, storage_utils.prepare_modify_order_object,
+                                      self.block, mock_volume, None, None, None)
+
+        self.assertEqual("Volume does not have a valid storage type (with an appropriate "
+                         "keyName to indicate the volume is a PERFORMANCE or an ENDURANCE volume).",
+                         str(exception))