Repository: softlayer/softlayer-python
Pull request #1846 (2023-01-27 19:56:55)

Updated command slcli block replica-order
Issue link: #1845 
Note: Updated some descriptions in group command `slcli block replica`

```
softlayer-python/ (issue1845) $ slcli block replica-order -h
Usage: slcli block replica-order [OPTIONS] VOLUME_ID

        Order a block storage replica volume.

┌────┬─────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────┐
│    │ volume_id           │                                                                                                  │
│ -d │ --datacenter        │ Short name of the datacenter for the replica (e.g.: dal09)  [required]                           │
│ -i │ --iops              │ Performance Storage IOPs, between 100 and 6000 in multiples of 100. If no IOPS value is          │
│    │                     │ specified, the IOPS value of the original volume will be used.                                   │
│ -o │ --os-type           │ Operating System Type (eg. LINUX) of the primary volume for which a replica is ordered           │
│    │                     │ [optional]. Choices: HYPER_V, LINUX, VMWARE, WINDOWS_2008, WINDOWS_GPT, WINDOWS, XEN             │
│ -s │ --snapshot-schedule │ Snapshot schedule to use for replication. Options are: HOURLY, DAILY, WEEKLY  [required]         │
│    │                     │ Choices: HOURLY, DAILY, WEEKLY                                                                   │
│ -t │ --tier              │ Endurance Storage Tier (IOPS per GB) of the primary volume for which a replica is ordered        │
│    │                     │ [optional]. If no tier is specified, the tier of the original volume will be used Choices: 0.25, │
│    │                     │ 2, 4, 10                                                                                         │
│ -h │ --help              │ Show this message and exit.                                                                      │
└────┴─────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────┘
```


Patch:
diff --git a/SoftLayer/CLI/block/replication/disaster_recovery_failover.py b/SoftLayer/CLI/block/replication/disaster_recovery_failover.py
index 15add46a..221faf57 100644
--- a/SoftLayer/CLI/block/replication/disaster_recovery_failover.py
+++ b/SoftLayer/CLI/block/replication/disaster_recovery_failover.py
@@ -15,7 +15,7 @@
 After using this method, to failback to the original volume, please open a support ticket.
 If you wish to test failover, please use replica-failover.""")
 @click.argument('volume-id')
-@click.option('--replicant-id', help="ID of the replicant volume")
+@click.option('--replicant-id', help="ID of the replicant volume.")
 @environment.pass_env
 def cli(env, volume_id, replicant_id):
     """Failover an inaccessible block volume to its available replicant volume."""
diff --git a/SoftLayer/CLI/block/replication/failback.py b/SoftLayer/CLI/block/replication/failback.py
index 0bc83c35..e7f4dc73 100644
--- a/SoftLayer/CLI/block/replication/failback.py
+++ b/SoftLayer/CLI/block/replication/failback.py
@@ -10,7 +10,7 @@
 @click.argument('volume-id')
 @environment.pass_env
 def cli(env, volume_id):
-    """Failback a block volume from the given replicant volume."""
+    """Failback a block volume from the given replica volume."""
     block_storage_manager = SoftLayer.BlockStorageManager(env.client)
 
     success = block_storage_manager.failback_from_replicant(volume_id)
diff --git a/SoftLayer/CLI/block/replication/failover.py b/SoftLayer/CLI/block/replication/failover.py
index 5778438f..1bb63f84 100644
--- a/SoftLayer/CLI/block/replication/failover.py
+++ b/SoftLayer/CLI/block/replication/failover.py
@@ -8,10 +8,10 @@
 
 @click.command(cls=SoftLayer.CLI.command.SLCommand, )
 @click.argument('volume-id')
-@click.option('--replicant-id', help="ID of the replicant volume")
+@click.option('--replicant-id', help="ID of the replicant volume.")
 @environment.pass_env
 def cli(env, volume_id, replicant_id):
-    """Failover a block volume to the given replicant volume."""
+    """Failover a block volume to the given replica volume."""
     block_storage_manager = SoftLayer.BlockStorageManager(env.client)
 
     success = block_storage_manager.failover_to_replicant(
diff --git a/SoftLayer/CLI/block/replication/locations.py b/SoftLayer/CLI/block/replication/locations.py
index 07cf83ee..12475741 100644
--- a/SoftLayer/CLI/block/replication/locations.py
+++ b/SoftLayer/CLI/block/replication/locations.py
@@ -22,12 +22,12 @@
 
 @click.command(cls=SoftLayer.CLI.command.SLCommand, )
 @click.argument('volume-id')
-@click.option('--sortby', help='Column to sort by', default='Long Name')
 @click.option('--columns',
               callback=column_helper.get_formatter(COLUMNS),
               help='Columns to display. Options: {0}'.format(
                   ', '.join(column.name for column in COLUMNS)),
               default=','.join(DEFAULT_COLUMNS))
+@click.option('--sortby', help='Column to sort by', default='Long Name')
 @environment.pass_env
 def cli(env, columns, sortby, volume_id):
     """List suitable replication datacenters for the given volume."""
diff --git a/SoftLayer/CLI/block/replication/order.py b/SoftLayer/CLI/block/replication/order.py
index 8df3608b..c678a397 100644
--- a/SoftLayer/CLI/block/replication/order.py
+++ b/SoftLayer/CLI/block/replication/order.py
@@ -14,22 +14,16 @@
 
 @click.command(cls=SoftLayer.CLI.command.SLCommand, context_settings=CONTEXT_SETTINGS)
 @click.argument('volume_id')
-@click.option('--snapshot-schedule', '-s',
-              help='Snapshot schedule to use for replication, '
-              '(HOURLY | DAILY | WEEKLY)',
-              required=True,
-              type=click.Choice(['HOURLY', 'DAILY', 'WEEKLY']))
-@click.option('--location', '-l',
-              help='Short name of the data center for the replicant '
-              '(e.g.: dal09)',
+@click.option('--datacenter', '-d',
+              help='Short name of the datacenter for the replica (e.g.: dal09)',
               required=True)
-@click.option('--tier',
-              help='Endurance Storage Tier (IOPS per GB) of the primary'
-              ' volume for which a replicant is ordered [optional]',
-              type=click.Choice(['0.25', '2', '4', '10']))
-@click.option('--os-type',
-              help='Operating System Type (e.g.: LINUX) of the primary'
-              ' volume for which a replica is ordered [optional]',
+@click.option('--iops', '-i',
+              help='Performance Storage IOPs, between 100 and 6000 in multiples of 100. If no IOPS value is specified,'
+              ' the IOPS value of the original volume will be used.',
+              type=int)
+@click.option('--os-type', '-o',
+              help='Operating System Type (eg. LINUX) of the primary volume for '
+              'which a replica is ordered [optional].',
               type=click.Choice([
                   'HYPER_V',
                   'LINUX',
@@ -38,8 +32,17 @@
                   'WINDOWS_GPT',
                   'WINDOWS',
                   'XEN']))
+@click.option('--snapshot-schedule', '-s',
+              help='Snapshot schedule to use for replication. Options are: '
+              'HOURLY, DAILY, WEEKLY',
+              required=True,
+              type=click.Choice(['HOURLY', 'DAILY', 'WEEKLY']))
+@click.option('--tier', '-t',
+              help='Endurance Storage Tier (IOPS per GB) of the primary volume for which a replica is ordered '
+              '[optional]. If no tier is specified, the tier of the original volume will be used',
+              type=click.Choice(['0.25', '2', '4', '10']))
 @environment.pass_env
-def cli(env, volume_id, snapshot_schedule, location, tier, os_type):
+def cli(env, volume_id, snapshot_schedule, datacenter, tier, os_type, iops):
     """Order a block storage replica volume."""
     block_manager = SoftLayer.BlockStorageManager(env.client)
     block_volume_id = helpers.resolve_id(block_manager.resolve_ids, volume_id, 'Block Volume')
@@ -47,13 +50,21 @@ def cli(env, volume_id, snapshot_schedule, location, tier, os_type):
     if tier is not None:
         tier = float(tier)
 
+    if iops is not None:
+        if iops < 100 or iops > 6000:
+            raise exceptions.ArgumentError(f"Invalid value for '--iops' / '-i': '{iops}' is not one "
+                                           "of between 100 and 6000.")
+        if iops % 100 != 0:
+            raise exceptions.ArgumentError(f"Invalid value for '--iops' / '-i': '{iops}' is not a multiple of 100.")
+
     try:
         order = block_manager.order_replicant_volume(
             block_volume_id,
             snapshot_schedule=snapshot_schedule,
-            location=location,
+            location=datacenter,
             tier=tier,
             os_type=os_type,
+            iops=iops
         )
     except ValueError as ex:
         raise exceptions.ArgumentError(str(ex))
diff --git a/SoftLayer/CLI/block/replication/partners.py b/SoftLayer/CLI/block/replication/partners.py
index 9910520f..bece5e9c 100644
--- a/SoftLayer/CLI/block/replication/partners.py
+++ b/SoftLayer/CLI/block/replication/partners.py
@@ -14,15 +14,15 @@
 
 @click.command(cls=SoftLayer.CLI.command.SLCommand, )
 @click.argument('volume-id')
-@click.option('--sortby', help='Column to sort by', default='Username')
 @click.option('--columns',
               callback=column_helper.get_formatter(COLUMNS),
               help='Columns to display. Options: {0}'.format(
                   ', '.join(column.name for column in COLUMNS)),
               default=','.join(DEFAULT_COLUMNS))
+@click.option('--sortby', help='Column to sort by', default='Username')
 @environment.pass_env
 def cli(env, columns, sortby, volume_id):
-    """List existing replicant volumes for a block volume."""
+    """List existing replica volumes for a block volume."""
     block_storage_manager = SoftLayer.BlockStorageManager(env.client)
 
     legal_volumes = block_storage_manager.get_replication_partners(
diff --git a/SoftLayer/managers/storage.py b/SoftLayer/managers/storage.py
index fd6ff70e..a00f2c93 100644
--- a/SoftLayer/managers/storage.py
+++ b/SoftLayer/managers/storage.py
@@ -217,7 +217,8 @@ def order_replicant_volume(self,
                                snapshot_schedule,
                                location,
                                tier=None,
-                               os_type=None):
+                               os_type=None,
+                               iops=None):
         """Places an order for a replicant volume.
 
         :param volume_id: The ID of the primary volume to be replicated
@@ -239,9 +240,12 @@ def order_replicant_volume(self,
         storage_class = storage_utils.block_or_file(
             block_volume['storageType']['keyName'])
 
+        if iops is None:
+            iops = int(block_volume['provisionedIops'])
+
         order = storage_utils.prepare_replicant_order_object(
             self, snapshot_schedule, location, tier, block_volume,
-            storage_class)
+            storage_class, iops)
 
         if storage_class == 'block':
             if os_type is None:
diff --git a/SoftLayer/managers/storage_utils.py b/SoftLayer/managers/storage_utils.py
index 4419d4d6..4ce33e6c 100644
--- a/SoftLayer/managers/storage_utils.py
+++ b/SoftLayer/managers/storage_utils.py
@@ -645,7 +645,7 @@ def _get_order_type_and_category(service_offering, storage_type, volume_type):
 
 
 def prepare_replicant_order_object(manager, snapshot_schedule, location,
-                                   tier, volume, volume_type):
+                                   tier, volume, volume_type, iops):
     """Prepare the order object which is submitted to the placeOrder() method
 
     :param manager: The File or Block manager calling this function
@@ -730,7 +730,7 @@ def prepare_replicant_order_object(manager, snapshot_schedule, location,
                     "A replica volume cannot be ordered for this performance "
                     "volume since it does not support Encryption at Rest.")
             volume_is_performance = True
-            iops = int(volume['provisionedIops'])
+
             prices = [
                 find_price_by_category(package, billing_item_category_code),
                 find_price_by_category(package, 'storage_' + volume_type),


Test patch:
diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py
index aba9e592..332f5ca6 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -625,7 +625,8 @@ def test_replicant_order_order_not_placed(self, order_mock):
 
         result = self.run_command(['block', 'replica-order', '100',
                                    '--snapshot-schedule=DAILY',
-                                   '--location=dal05'])
+                                   '--datacenter=dal05',
+                                   '--iops=100'])
 
         self.assert_no_fail(result)
         self.assertEqual(result.output,
@@ -650,7 +651,8 @@ def test_replicant_order(self, order_mock):
 
         result = self.run_command(['block', 'replica-order', '100',
                                    '--snapshot-schedule=DAILY',
-                                   '--location=dal05', '--tier=2'])
+                                   '--datacenter=dal05', '--tier=2',
+                                   '--iops=100'])
 
         self.assert_no_fail(result)
         self.assertEqual(result.output,
diff --git a/tests/managers/storage_utils_tests.py b/tests/managers/storage_utils_tests.py
index 976f3749..f7bc5005 100644
--- a/tests/managers/storage_utils_tests.py
+++ b/tests/managers/storage_utils_tests.py
@@ -3259,7 +3259,7 @@ def test_prep_replicant_order_volume_cancelled(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3275,7 +3275,7 @@ def test_prep_replicant_order_volume_cancellation_date_set(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3292,7 +3292,7 @@ def test_prep_replicant_order_snapshot_space_cancelled(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3310,7 +3310,7 @@ def test_prep_replicant_order_invalid_location(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'hoth02', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'hoth02', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3329,7 +3329,7 @@ def test_prep_replicant_order_enterprise_offering_invalid_type(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3348,7 +3348,7 @@ def test_prep_replicant_order_snapshot_capacity_not_found(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3385,7 +3385,7 @@ def test_prep_replicant_order_saas_endurance(self):
         }
 
         result = storage_utils.prepare_replicant_order_object(
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(expected_object, result)
@@ -3419,7 +3419,7 @@ def test_prep_replicant_order_saas_endurance_tier_is_not_none(self):
         }
 
         result = storage_utils.prepare_replicant_order_object(
-            self.block, 'WEEKLY', 'wdc04', 2, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', 2, mock_volume, 'block', 100
         )
 
         self.assertEqual(expected_object, result)
@@ -3437,7 +3437,7 @@ def test_prep_replicant_order_saas_performance_volume_below_staas_v2(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3472,12 +3472,12 @@ def test_prep_replicant_order_saas_performance(self):
             'originVolumeId': 102,
             'originVolumeScheduleId': 978,
             'volumeSize': 500,
-            'iops': 1000,
+            'iops': 100,
             'useHourlyPricing': False
         }
 
         result = storage_utils.prepare_replicant_order_object(
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(expected_object, result)
@@ -3494,7 +3494,7 @@ def test_prep_replicant_order_saas_invalid_storage_type(self):
         exception = self.assertRaises(
             exceptions.SoftLayerError,
             storage_utils.prepare_replicant_order_object,
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(
@@ -3534,7 +3534,7 @@ def test_prep_replicant_order_ent_endurance(self):
         }
 
         result = storage_utils.prepare_replicant_order_object(
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(expected_object, result)
@@ -3569,7 +3569,7 @@ def test_prep_replicant_order_ent_endurance_tier_is_not_none(self):
         }
 
         result = storage_utils.prepare_replicant_order_object(
-            self.block, 'WEEKLY', 'wdc04', 2, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', 2, mock_volume, 'block', 100
         )
 
         self.assertEqual(expected_object, result)
@@ -3604,7 +3604,7 @@ def test_prep_replicant_order_hourly_billing(self):
         }
 
         result = storage_utils.prepare_replicant_order_object(
-            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block'
+            self.block, 'WEEKLY', 'wdc04', None, mock_volume, 'block', 100
         )
 
         self.assertEqual(expected_object, result)