Repository: softlayer/softlayer-python
Pull request #758 (2016-08-18 04:36:35)

Add snapshot space option in volume-order commands
Add the --snapshot-size option for File and Block
'volume-order' commands. If specified, this option
attempts to include snapshot space of the given size
as part of the order for a new storage volume.

Add additional CLI module tests for File and Block
to improve upon previous unit testing coverage.

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

Coverage increased (+0.3%) to 81.314% when pulling **dbf037daf84be282cf3e583f2cddbfdb1c9b6d81 on kyubifire:volume-order-add-snapshots** into **6581b2c4d0e9365a3f528da0d036e20ff887cb6e on softlayer:master**.


Patch:
diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py
index 4393a282..22430e42 100644
--- a/SoftLayer/CLI/block/order.py
+++ b/SoftLayer/CLI/block/order.py
@@ -7,7 +7,7 @@
 from SoftLayer.CLI import exceptions
 
 
-CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.upper())
+CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()}
 
 
 @click.command(context_settings=CONTEXT_SETTINGS)
@@ -42,8 +42,14 @@
 @click.option('--location',
               help='Datacenter short name (e.g.: dal09)',
               required=True)
+@click.option('--snapshot-size',
+              type=int,
+              help='Optional parameter for ordering snapshot '
+              'space along with endurance block storage; specifies '
+              'the size (in GB) of snapshot space to order')
 @environment.pass_env
-def cli(env, storage_type, size, iops, tier, os_type, location):
+def cli(env, storage_type, size, iops, tier, os_type,
+        location, snapshot_size):
     """Order a block storage volume."""
     block_manager = SoftLayer.BlockStorageManager(env.client)
     storage_type = storage_type.lower()
@@ -62,6 +68,12 @@ def cli(env, storage_type, size, iops, tier, os_type, location):
                 'Option --iops must be a multiple of 100'
             )
 
+        if snapshot_size is not None:
+            raise exceptions.CLIAbort(
+                'Option --snapshot-size not allowed for performance volumes.'
+                ' Snapshots are only available for endurance storage.'
+            )
+
         try:
             order = block_manager.order_block_volume(
                 storage_type='performance_storage_iscsi',
@@ -84,7 +96,8 @@ def cli(env, storage_type, size, iops, tier, os_type, location):
                 location=location,
                 size=size,
                 tier_level=float(tier),
-                os_type=os_type
+                os_type=os_type,
+                snapshot_size=snapshot_size
             )
         except ValueError as ex:
             raise exceptions.ArgumentError(str(ex))
diff --git a/SoftLayer/CLI/file/order.py b/SoftLayer/CLI/file/order.py
index bb200f27..0219efdb 100644
--- a/SoftLayer/CLI/file/order.py
+++ b/SoftLayer/CLI/file/order.py
@@ -7,7 +7,7 @@
 from SoftLayer.CLI import exceptions
 
 
-CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.upper())
+CONTEXT_SETTINGS = {'token_normalize_func': lambda x: x.upper()}
 
 
 @click.command(context_settings=CONTEXT_SETTINGS)
@@ -42,8 +42,14 @@
 @click.option('--location',
               help='Datacenter short name (e.g.: dal09)',
               required=True)
+@click.option('--snapshot-size',
+              type=int,
+              help='Optional parameter for ordering snapshot '
+              'space along with endurance file storage; specifies '
+              'the size (in GB) of snapshot space to order')
 @environment.pass_env
-def cli(env, storage_type, size, iops, tier, os_type, location):
+def cli(env, storage_type, size, iops, tier, os_type,
+        location, snapshot_size):
     """Order a file storage volume."""
     file_manager = SoftLayer.FileStorageManager(env.client)
     storage_type = storage_type.lower()
@@ -62,6 +68,12 @@ def cli(env, storage_type, size, iops, tier, os_type, location):
                 'Option --iops must be a multiple of 100'
             )
 
+        if snapshot_size is not None:
+            raise exceptions.CLIAbort(
+                'Option --snapshot-size not allowed for performance volumes.'
+                ' Snapshots are only available for endurance storage.'
+            )
+
         try:
             order = file_manager.order_file_volume(
                 storage_type='performance_storage_nfs',
@@ -84,7 +96,8 @@ def cli(env, storage_type, size, iops, tier, os_type, location):
                 location=location,
                 size=size,
                 tier_level=float(tier),
-                os_type=os_type
+                os_type=os_type,
+                snapshot_size=snapshot_size
             )
         except ValueError as ex:
             raise exceptions.ArgumentError(str(ex))
diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py
index 10f170f3..308b4326 100644
--- a/SoftLayer/managers/block.py
+++ b/SoftLayer/managers/block.py
@@ -193,7 +193,7 @@ def delete_snapshot(self, snapshot_id):
                                 id=snapshot_id)
 
     def order_block_volume(self, storage_type, location, size, os_type,
-                           iops=None, tier_level=None):
+                           iops=None, tier_level=None, snapshot_size=None):
         """Places an order for a block volume.
 
         :param storage_type: "performance_storage_iscsi" (performance)
@@ -203,6 +203,8 @@ def order_block_volume(self, storage_type, location, size, os_type,
         :param os_type: OS Type to use for volume alignment, see help for list
         :param iops: Number of IOPs for a "Performance" order
         :param tier_level: Tier level to use for an "Endurance" order
+        :param snapshot_size: The size of optional snapshot space,
+        if snapshot space should also be ordered (None if not ordered)
         """
 
         try:
@@ -239,6 +241,9 @@ def order_block_volume(self, storage_type, location, size, os_type,
                     ),
                 storage_utils.find_endurance_tier_price(package, tier_level),
             ]
+            if snapshot_size is not None:
+                prices.append(storage_utils.find_snapshot_space_price(
+                    package, snapshot_size, tier_level))
         else:
             raise exceptions.SoftLayerError(
                 "Block volume storage_type must be either "
diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py
index 9759b1c5..ce3a16dc 100644
--- a/SoftLayer/managers/file.py
+++ b/SoftLayer/managers/file.py
@@ -196,7 +196,7 @@ def delete_snapshot(self, snapshot_id):
                                 id=snapshot_id)
 
     def order_file_volume(self, storage_type, location, size, os_type,
-                          iops=None, tier_level=None):
+                          iops=None, tier_level=None, snapshot_size=None):
         """Places an order for a file volume.
 
         :param storage_type: "performance_storage_iscsi" (performance)
@@ -206,6 +206,8 @@ def order_file_volume(self, storage_type, location, size, os_type,
         :param os_type: OS Type to use for volume alignment, see help for list
         :param iops: Number of IOPs for a "Performance" order
         :param tier_level: Tier level to use for an "Endurance" order
+        :param snapshot_size: The size of optional snapshot space,
+        if snapshot space should also be ordered (None if not ordered)
         """
 
         try:
@@ -242,6 +244,9 @@ def order_file_volume(self, storage_type, location, size, os_type,
                     ),
                 storage_utils.find_endurance_tier_price(package, tier_level),
             ]
+            if snapshot_size is not None:
+                prices.append(storage_utils.find_snapshot_space_price(
+                    package, snapshot_size, tier_level))
         else:
             raise exceptions.SoftLayerError(
                 "File volume storage_type must be either "


Test patch:
diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py
index b413a695..3998d03c 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -101,29 +101,107 @@ def test_volume_list(self):
             }],
             json.loads(result.output))
 
+    def test_volume_order_performance_iops_not_given(self):
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--os-type=linux', '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
+    def test_volume_order_performance_iops_out_of_range(self):
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=80000', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
+    def test_volume_order_performance_iops_not_multiple_of_100(self):
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=122', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
+    def test_volume_order_performance_snapshot_error(self):
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=100', '--os-type=linux',
+                                   '--location=dal05', '--snapshot-size=10'])
+
+        self.assertEqual(2, result.exit_code)
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_block_volume')
+    def test_volume_order_performance(self, order_mock):
+        order_mock.return_value = {
+            'placedOrder': {
+                'id': 478,
+                'items': [
+                    {'description': 'Performance Storage'},
+                    {'description': 'Block Storage'},
+                    {'description': '0.25 IOPS per GB'},
+                    {'description': '20 GB Storage Space'}]
+            }
+        }
+
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=100', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order #478 placed successfully!\n'
+                         ' > Performance Storage\n > Block Storage\n'
+                         ' > 0.25 IOPS per GB\n > 20 GB Storage Space\n')
+
+    def test_volume_order_endurance_tier_not_given(self):
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=endurance', '--size=20',
+                                   '--os-type=linux', '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
     @mock.patch('SoftLayer.BlockStorageManager.order_block_volume')
-    def test_volume_order(self, order_mock):
+    def test_volume_order_endurance(self, order_mock):
         order_mock.return_value = {
             'placedOrder': {
                 'id': 478,
-                'items': [{'description': 'Endurance Storage'},
-                          {'description': 'Block Storage'},
-                          {'description': '0.25 IOPS per GB'},
-                          {'description': '20 GB Storage Space'},
-                          ]
+                'items': [
+                    {'description': 'Endurance Storage'},
+                    {'description': 'Block Storage'},
+                    {'description': '0.25 IOPS per GB'},
+                    {'description': '20 GB Storage Space'},
+                    {'description': '10 GB Storage Space (Snapshot Space)'}]
                 }
         }
 
         result = self.run_command(['block', 'volume-order',
                                    '--storage-type=endurance', '--size=20',
                                    '--tier=0.25', '--os-type=linux',
-                                   '--location=dal05'])
+                                   '--location=dal05', '--snapshot-size=10'])
 
         self.assert_no_fail(result)
         self.assertEqual(result.output,
                          'Order #478 placed successfully!\n'
                          ' > Endurance Storage\n > Block Storage\n'
-                         ' > 0.25 IOPS per GB\n > 20 GB Storage Space\n')
+                         ' > 0.25 IOPS per GB\n > 20 GB Storage Space\n'
+                         ' > 10 GB Storage Space (Snapshot Space)\n')
+
+    @mock.patch('SoftLayer.BlockStorageManager.order_block_volume')
+    def test_volume_order_order_not_placed(self, order_mock):
+        order_mock.return_value = {}
+
+        result = self.run_command(['block', 'volume-order',
+                                   '--storage-type=endurance', '--size=20',
+                                   '--tier=0.25', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order could not be placed! Please verify '
+                         'your options and try again.\n')
 
     def test_enable_snapshots(self):
         result = self.run_command(['block', 'snapshot-enable', '12345678',
diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py
index 6f509d57..8ae667ea 100644
--- a/tests/CLI/modules/file_tests.py
+++ b/tests/CLI/modules/file_tests.py
@@ -131,29 +131,107 @@ def test_volume_detail(self):
                           '# of Active Transactions': '0'
                           }, json.loads(result.output))
 
+    def test_volume_order_performance_iops_not_given(self):
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--os-type=linux', '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
+    def test_volume_order_performance_iops_out_of_range(self):
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=80000', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
+    def test_volume_order_performance_iops_not_multiple_of_100(self):
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=122', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
+    def test_volume_order_performance_snapshot_error(self):
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=100', '--os-type=linux',
+                                   '--location=dal05', '--snapshot-size=10'])
+
+        self.assertEqual(2, result.exit_code)
+
+    @mock.patch('SoftLayer.FileStorageManager.order_file_volume')
+    def test_volume_order_performance(self, order_mock):
+        order_mock.return_value = {
+            'placedOrder': {
+                'id': 478,
+                'items': [
+                    {'description': 'Performance Storage'},
+                    {'description': 'File Storage'},
+                    {'description': '0.25 IOPS per GB'},
+                    {'description': '20 GB Storage Space'}]
+            }
+        }
+
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=performance', '--size=20',
+                                   '--iops=100', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order #478 placed successfully!\n'
+                         ' > Performance Storage\n > File Storage\n'
+                         ' > 0.25 IOPS per GB\n > 20 GB Storage Space\n')
+
+    def test_volume_order_endurance_tier_not_given(self):
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=endurance', '--size=20',
+                                   '--os-type=linux', '--location=dal05'])
+
+        self.assertEqual(2, result.exit_code)
+
     @mock.patch('SoftLayer.FileStorageManager.order_file_volume')
-    def test_volume_order(self, order_mock):
+    def test_volume_order_endurance(self, order_mock):
         order_mock.return_value = {
             'placedOrder': {
                 'id': 478,
-                'items': [{'description': 'Endurance Storage'},
-                          {'description': 'File Storage'},
-                          {'description': '0.25 IOPS per GB'},
-                          {'description': '20 GB Storage Space'},
-                          ]
+                'items': [
+                    {'description': 'Endurance Storage'},
+                    {'description': 'File Storage'},
+                    {'description': '0.25 IOPS per GB'},
+                    {'description': '20 GB Storage Space'},
+                    {'description': '10 GB Storage Space (Snapshot Space)'}]
                 }
         }
 
         result = self.run_command(['file', 'volume-order',
                                    '--storage-type=endurance', '--size=20',
                                    '--tier=0.25', '--os-type=linux',
-                                   '--location=dal05'])
+                                   '--location=dal05', '--snapshot-size=10'])
 
         self.assert_no_fail(result)
         self.assertEqual(result.output,
                          'Order #478 placed successfully!\n'
                          ' > Endurance Storage\n > File Storage\n'
-                         ' > 0.25 IOPS per GB\n > 20 GB Storage Space\n')
+                         ' > 0.25 IOPS per GB\n > 20 GB Storage Space\n'
+                         ' > 10 GB Storage Space (Snapshot Space)\n')
+
+    @mock.patch('SoftLayer.FileStorageManager.order_file_volume')
+    def test_volume_order_order_not_placed(self, order_mock):
+        order_mock.return_value = {}
+
+        result = self.run_command(['file', 'volume-order',
+                                   '--storage-type=endurance', '--size=20',
+                                   '--tier=0.25', '--os-type=linux',
+                                   '--location=dal05'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(result.output,
+                         'Order could not be placed! Please verify '
+                         'your options and try again.\n')
 
     def test_enable_snapshots(self):
         result = self.run_command(['file', 'snapshot-enable', '12345678',
diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py
index 45337478..86b12e4e 100644
--- a/tests/managers/block_tests.py
+++ b/tests/managers/block_tests.py
@@ -155,7 +155,7 @@ def test_order_block_volume_performance(self):
             "dal05",
             100,
             "LINUX",
-            iops=100
+            iops=100,
             )
 
         self.assertEqual(
@@ -231,7 +231,95 @@ def test_order_block_volume_endurance(self):
             "dal05",
             100,
             "LINUX",
-            tier_level=0.25
+            tier_level=0.25,
+            )
+
+        self.assertEqual(
+            result,
+            {
+                'orderDate': '2013-08-01 15:23:45',
+                'orderId': 1234,
+                'prices': [{
+                    'hourlyRecurringFee': '2',
+                    'id': 1,
+                    'item': {'description': 'this is a thing', 'id': 1},
+                    'laborFee': '2',
+                    'oneTimeFee': '2',
+                    'oneTimeFeeTax': '.1',
+                    'quantity': 1,
+                    'recurringFee': '2',
+                    'recurringFeeTax': '.1',
+                    'setupFee': '1'}],
+                },
+            )
+
+    def test_order_block_volume_endurance_with_snapshot(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [{
+            'id': 1,
+            'name': 'Endurance',
+            'items': [{
+                'capacity': '1',
+                'prices': [{
+                    'id': 1,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_block',
+                    }],
+                }],
+            }, {
+                'capacity': '1',
+                'prices': [{
+                    'id': 2,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_service_enterprise',
+                    }],
+                }],
+            }, {
+                'capacity': '100',
+                'prices': [{
+                    'id': 3,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'performance_storage_space',
+                    }],
+                    'capacityRestrictionMinimum': '100',
+                    'capacityRestrictionMaximum': '100',
+                }],
+            }, {
+                'capacity': '100',
+                'attributes': [{
+                    'value': '100',
+                }],
+                'prices': [{
+                    'id': 4,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_tier_level',
+                    }],
+                }],
+            }, {
+                'capacity': '10',
+                'prices': [{
+                    'id': 5,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_snapshot_space',
+                    }],
+                    'capacityRestrictionMinimum': '100',
+                    'capacityRestrictionMaximum': '100',
+                }],
+            }],
+        }]
+
+        result = self.block.order_block_volume(
+            "storage_service_enterprise",
+            "dal05",
+            100,
+            "LINUX",
+            tier_level=0.25,
+            snapshot_size=10,
             )
 
         self.assertEqual(
diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py
index 7187a7d8..42c6ff10 100644
--- a/tests/managers/file_tests.py
+++ b/tests/managers/file_tests.py
@@ -222,7 +222,7 @@ def test_order_file_volume_performance(self):
             "dal05",
             100,
             "LINUX",
-            iops=100
+            iops=100,
             )
 
         self.assertEqual(
@@ -306,7 +306,95 @@ def test_order_file_volume_endurance(self):
             "dal05",
             100,
             "LINUX",
-            tier_level=0.25
+            tier_level=0.25,
+            )
+
+        self.assertEqual(
+            result,
+            {
+                'orderDate': '2013-08-01 15:23:45',
+                'orderId': 1234,
+                'prices': [{
+                    'hourlyRecurringFee': '2',
+                    'id': 1,
+                    'item': {'description': 'this is a thing', 'id': 1},
+                    'laborFee': '2',
+                    'oneTimeFee': '2',
+                    'oneTimeFeeTax': '.1',
+                    'quantity': 1,
+                    'recurringFee': '2',
+                    'recurringFeeTax': '.1',
+                    'setupFee': '1'}],
+                },
+            )
+
+    def test_order_file_volume_endurance_with_snapshot(self):
+        mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
+        mock.return_value = [{
+            'id': 1,
+            'name': 'Endurance',
+            'items': [{
+                'capacity': '1',
+                'prices': [{
+                    'id': 1,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_file',
+                    }],
+                }],
+            }, {
+                'capacity': '1',
+                'prices': [{
+                    'id': 2,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_service_enterprise',
+                    }],
+                }],
+            }, {
+                'capacity': '100',
+                'prices': [{
+                    'id': 3,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'performance_storage_space',
+                    }],
+                    'capacityRestrictionMinimum': '100',
+                    'capacityRestrictionMaximum': '100',
+                }],
+            }, {
+                'capacity': '100',
+                'attributes': [{
+                    'value': '100',
+                }],
+                'prices': [{
+                    'id': 4,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_tier_level',
+                    }],
+                }],
+            }, {
+                'capacity': '10',
+                'prices': [{
+                    'id': 5,
+                    'locationGroupId': '',
+                    'categories': [{
+                        'categoryCode': 'storage_snapshot_space',
+                    }],
+                    'capacityRestrictionMinimum': '100',
+                    'capacityRestrictionMaximum': '100',
+                }],
+            }],
+        }]
+
+        result = self.file.order_file_volume(
+            "storage_service_enterprise",
+            "dal05",
+            100,
+            "LINUX",
+            tier_level=0.25,
+            snapshot_size=10,
             )
 
         self.assertEqual(