Repository: softlayer/softlayer-python
Pull request #1228 (2020-02-17 21:39:43)

Support Ordering Dependent Duplicate storage volumes
supports order dep. dupe. volumes for file/block


Patch:
diff --git a/.travis.yml b/.travis.yml
index 35c987a6..e40cb2a5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,5 @@
 # https://docs.travis-ci.com/user/languages/python/#python-37-and-higher
-dist: bionic
+dist: xenial
 language: python
 sudo: false
 matrix:
@@ -12,7 +12,7 @@ matrix:
       env: TOX_ENV=py37
     - python: "3.8"
       env: TOX_ENV=py38
-    - python: "pypy3"
+    - python: "pypy3.5"
       env: TOX_ENV=pypy3
     - python: "3.6"
       env: TOX_ENV=analysis
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 4de814b5..5b475dc7 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -6,6 +6,7 @@ chechuironman <chechuironman@gmail.com>
 Christopher Gallo <chrisagallo@gmail.com>
 David Ibarra <dtibarra@gmail.com>
 Hans Kristian Moen <hans@ububeet.(none)>
+Ian Sutton <ians@openbsd.org>
 Jake Williams <jwilliams@softlayer.com>
 Jason Johnson <spligak@gmail.com>
 Kevin Landreth <klandreth@softlayer.com>
diff --git a/SoftLayer/CLI/block/duplicate.py b/SoftLayer/CLI/block/duplicate.py
index ec728f87..ff9ae961 100644
--- a/SoftLayer/CLI/block/duplicate.py
+++ b/SoftLayer/CLI/block/duplicate.py
@@ -54,9 +54,16 @@
               type=click.Choice(['hourly', 'monthly']),
               default='monthly',
               help="Optional parameter for Billing rate (default to monthly)")
+@click.option('--dependent-duplicate',
+              type=click.BOOL,
+              default=False,
+              show_default=True,
+              help='Whether or not this duplicate will be a dependent duplicate '
+                   'of the origin volume.')
 @environment.pass_env
 def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size,
-        duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing):
+        duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing,
+        dependent_duplicate):
     """Order a duplicate block storage volume."""
     block_manager = SoftLayer.BlockStorageManager(env.client)
 
@@ -75,7 +82,8 @@ def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size,
             duplicate_iops=duplicate_iops,
             duplicate_tier_level=duplicate_tier,
             duplicate_snapshot_size=duplicate_snapshot_size,
-            hourly_billing_flag=hourly_billing_flag
+            hourly_billing_flag=hourly_billing_flag,
+            dependent_duplicate=dependent_duplicate
         )
     except ValueError as ex:
         raise exceptions.ArgumentError(str(ex))
diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py
index fe86f714..3a552df7 100644
--- a/SoftLayer/CLI/core.py
+++ b/SoftLayer/CLI/core.py
@@ -14,7 +14,6 @@
 
 import click
 
-import requests
 import SoftLayer
 from SoftLayer.CLI import environment
 from SoftLayer.CLI import exceptions
@@ -32,7 +31,6 @@
     3: logging.DEBUG
 }
 
-PROG_NAME = "slcli (SoftLayer Command-line)"
 VALID_FORMATS = ['table', 'raw', 'json', 'jsonraw']
 DEFAULT_FORMAT = 'raw'
 if sys.stdout.isatty():
@@ -71,28 +69,6 @@ def get_command(self, ctx, name):
             return module
 
 
-def get_latest_version():
-    """Gets the latest version of the Softlayer library."""
-    try:
-        result = requests.get('https://pypi.org/pypi/SoftLayer/json')
-        json_result = result.json()
-        latest = 'v{}'.format(json_result['info']['version'])
-    except Exception:
-        latest = "Unable to get version from pypi."
-    return latest
-
-
-def get_version_message(ctx, param, value):
-    """Gets current and latest release versions message."""
-    if not value or ctx.resilient_parsing:
-        return
-    current = SoftLayer.consts.VERSION
-    latest = get_latest_version()
-    click.secho("Current: {prog} {current}\nLatest:  {prog} {latest}".format(
-        prog=PROG_NAME, current=current, latest=latest))
-    ctx.exit()
-
-
 @click.group(help="SoftLayer Command-line Client",
              epilog="""To use most commands your SoftLayer
 username and api_key need to be configured. The easiest way to do that is to
@@ -127,8 +103,7 @@ def get_version_message(ctx, param, value):
               is_flag=True,
               required=False,
               help="Use demo data instead of actually making API calls")
-@click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=get_version_message,
-              help="Show version information.")
+@click.version_option(prog_name="slcli (SoftLayer Command-line)")
 @environment.pass_env
 def cli(env,
         format='table',
diff --git a/SoftLayer/CLI/file/duplicate.py b/SoftLayer/CLI/file/duplicate.py
index a3b4c801..13e3dcfc 100644
--- a/SoftLayer/CLI/file/duplicate.py
+++ b/SoftLayer/CLI/file/duplicate.py
@@ -52,9 +52,16 @@
               type=click.Choice(['hourly', 'monthly']),
               default='monthly',
               help="Optional parameter for Billing rate (default to monthly)")
+@click.option('--dependent-duplicate',
+              type=click.BOOL,
+              default=False,
+              show_default=True,
+              help='Whether or not this duplicate will be a dependent duplicate'
+                   'of the origin volume.')
 @environment.pass_env
 def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size,
-        duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing):
+        duplicate_iops, duplicate_tier, duplicate_snapshot_size, billing,
+        dependent_duplicate):
     """Order a duplicate file storage volume."""
     file_manager = SoftLayer.FileStorageManager(env.client)
 
@@ -73,7 +80,8 @@ def cli(env, origin_volume_id, origin_snapshot_id, duplicate_size,
             duplicate_iops=duplicate_iops,
             duplicate_tier_level=duplicate_tier,
             duplicate_snapshot_size=duplicate_snapshot_size,
-            hourly_billing_flag=hourly_billing_flag
+            hourly_billing_flag=hourly_billing_flag,
+            dependent_duplicate=dependent_duplicate
         )
     except ValueError as ex:
         raise exceptions.ArgumentError(str(ex))
diff --git a/SoftLayer/CLI/user/permissions.py b/SoftLayer/CLI/user/permissions.py
index bf77c41a..b1851acb 100644
--- a/SoftLayer/CLI/user/permissions.py
+++ b/SoftLayer/CLI/user/permissions.py
@@ -11,7 +11,7 @@
 @click.argument('identifier')
 @environment.pass_env
 def cli(env, identifier):
-    """User Permissions."""
+    """User Permissions. TODO change to list all permissions, and which users have them"""
 
     mgr = SoftLayer.UserManager(env.client)
     user_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'username')
diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py
index 11b99893..25e6839f 100644
--- a/SoftLayer/managers/block.py
+++ b/SoftLayer/managers/block.py
@@ -310,7 +310,8 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
                                duplicate_size=None, duplicate_iops=None,
                                duplicate_tier_level=None,
                                duplicate_snapshot_size=None,
-                               hourly_billing_flag=False):
+                               hourly_billing_flag=False,
+                               dependent_duplicate=False):
         """Places an order for a duplicate block volume.
 
         :param origin_volume_id: The ID of the origin volume to be duplicated
@@ -321,6 +322,7 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
         :param duplicate_snapshot_size: Snapshot space size for the duplicate
         :param hourly_billing_flag: Billing type, monthly (False)
             or hourly (True), default to monthly.
+        :param dependent_duplicate: Duplicate type, normal (False) or dependent duplicate (True)
         :return: Returns a SoftLayer_Container_Product_Order_Receipt
         """
 
@@ -348,6 +350,9 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
         if origin_snapshot_id is not None:
             order['duplicateOriginSnapshotId'] = origin_snapshot_id
 
+        if dependent_duplicate:
+            order['isDependentDuplicateFlag'] = 1
+
         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):
diff --git a/SoftLayer/managers/file.py b/SoftLayer/managers/file.py
index 95cf85c8..0a5214e6 100644
--- a/SoftLayer/managers/file.py
+++ b/SoftLayer/managers/file.py
@@ -259,7 +259,8 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
                                duplicate_size=None, duplicate_iops=None,
                                duplicate_tier_level=None,
                                duplicate_snapshot_size=None,
-                               hourly_billing_flag=False):
+                               hourly_billing_flag=False,
+                               dependent_duplicate=False):
         """Places an order for a duplicate file volume.
 
         :param origin_volume_id: The ID of the origin volume to be duplicated
@@ -270,6 +271,7 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
         :param duplicate_snapshot_size: Snapshot space size for the duplicate
         :param hourly_billing_flag: Billing type, monthly (False)
             or hourly (True), default to monthly.
+        :param dependent_duplicate: Duplicate type, normal (False) or dependent duplicate (True)
         :return: Returns a SoftLayer_Container_Product_Order_Receipt
         """
 
@@ -289,6 +291,9 @@ def order_duplicate_volume(self, origin_volume_id, origin_snapshot_id=None,
         if origin_snapshot_id is not None:
             order['duplicateOriginSnapshotId'] = origin_snapshot_id
 
+        if dependent_duplicate:
+            order['isDependentDuplicateFlag'] = 1
+
         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):
diff --git a/SoftLayer/managers/storage_utils.py b/SoftLayer/managers/storage_utils.py
index 80ec6036..b49e0eb4 100644
--- a/SoftLayer/managers/storage_utils.py
+++ b/SoftLayer/managers/storage_utils.py
@@ -788,7 +788,8 @@ def prepare_replicant_order_object(manager, snapshot_schedule, location,
 
 def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
                                    duplicate_size, duplicate_snapshot_size,
-                                   volume_type, hourly_billing_flag=False):
+                                   volume_type, hourly_billing_flag=False,
+                                   dependent_duplicate=False):
     """Prepare the duplicate order to submit to SoftLayer_Product::placeOrder()
 
     :param manager: The File or Block manager calling this function
@@ -799,6 +800,8 @@ def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
     :param duplicate_snapshot_size: The size for the duplicate snapshot space
     :param volume_type: The type of the origin volume ('file' or 'block')
     :param hourly_billing_flag: Billing type, monthly (False) or hourly (True)
+    :param dependent_duplicate: Duplicate type, normal (False) or dependent
+        duplicate (True)
     :return: Returns the order object to be passed to the
              placeOrder() method of the Product_Order service
     """
@@ -903,6 +906,9 @@ def prepare_duplicate_order_object(manager, origin_volume, iops, tier,
     if volume_is_performance:
         duplicate_order['iops'] = iops
 
+    if dependent_duplicate:
+        duplicate_order['isDependentDuplicateFlag'] = 1
+
     return duplicate_order
 
 
diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst
index 3cd899d4..c11402bd 100644
--- a/docs/cli/hardware.rst
+++ b/docs/cli/hardware.rst
@@ -41,17 +41,9 @@ Provides some basic functionality to order a server. `slcli order` has a more fu
    :prog: hw edit
    :show-nested:
 
-**Note :** Using multiple ' **:** ' can cause an error.
-
-     $ slcli hw edit 123456 --tag "cloud:service:db2whoc, cloud:svcplan:flex, cloud:svcenv:prod, cloud:bmixenv:fra"
-
-         TransportError(0): ('Connection aborted.',
-
-         RemoteDisconnected('Remote end closed connection without response',))
-
-
 When setting port speed, use "-1" to indicate best possible configuration. Using 10/100/1000/10000 on a server with a redundant interface may result the interface entering a degraded state. See `setPublicNetworkInterfaceSpeed <http://sldn.softlayer.com/reference/services/SoftLayer_Hardware_Server/setPublicNetworkInterfaceSpeed/>`_ for more information.
 
+
 .. click:: SoftLayer.CLI.hardware.list:cli
    :prog: hw list
    :show-nested:


Test patch:
diff --git a/tests/CLI/core_tests.py b/tests/CLI/core_tests.py
index f230a351..321f78b1 100644
--- a/tests/CLI/core_tests.py
+++ b/tests/CLI/core_tests.py
@@ -10,7 +10,6 @@
 import click
 import mock
 
-from requests.models import Response
 import SoftLayer
 from SoftLayer.CLI import core
 from SoftLayer.CLI import environment
@@ -53,28 +52,6 @@ def test_diagnostics(self):
         self.assertIn('"python_version"', result.output)
         self.assertIn('"library_location"', result.output)
 
-    @mock.patch('requests.get')
-    def test_get_latest_version(self, request_get):
-        response = Response()
-        response.status_code = 200
-        response.json = mock.MagicMock(return_value={"info": {"version": "1.1.1"}})
-        request_get.return_value = response
-        version = core.get_latest_version()
-        self.assertIn('1.1.1', version)
-
-    @mock.patch('requests.get')
-    def test_unable_get_latest_version(self, request_get):
-        request_get.side_effect = Exception
-        version = core.get_latest_version()
-        self.assertIn('Unable', version)
-
-    @mock.patch('SoftLayer.CLI.core.get_latest_version')
-    def test_get_version_message(self, get_latest_version_mock):
-        get_latest_version_mock.return_value = '1.1.1'
-        env = environment.Environment()
-        result = self.run_command(['--version'], env=env)
-        self.assert_no_fail(result)
-
 
 class CoreMainTests(testing.TestCase):
 
diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py
index f42a864e..e5928898 100644
--- a/tests/CLI/modules/block_tests.py
+++ b/tests/CLI/modules/block_tests.py
@@ -642,7 +642,8 @@ def test_duplicate_order_hourly_billing(self, order_mock):
                                       duplicate_size=250, duplicate_iops=None,
                                       duplicate_tier_level=2,
                                       duplicate_snapshot_size=20,
-                                      hourly_billing_flag=True)
+                                      hourly_billing_flag=True,
+                                      dependent_duplicate=False)
         self.assert_no_fail(result)
         self.assertEqual(result.output,
                          'Order #24602 placed successfully!\n'
diff --git a/tests/CLI/modules/file_tests.py b/tests/CLI/modules/file_tests.py
index 0fc4ffc0..e1d8628c 100644
--- a/tests/CLI/modules/file_tests.py
+++ b/tests/CLI/modules/file_tests.py
@@ -629,7 +629,8 @@ def test_duplicate_order_hourly_billing(self, order_mock):
                                       duplicate_size=250, duplicate_iops=None,
                                       duplicate_tier_level=2,
                                       duplicate_snapshot_size=20,
-                                      hourly_billing_flag=True)
+                                      hourly_billing_flag=True,
+                                      dependent_duplicate=False)
         self.assert_no_fail(result)
         self.assertEqual(result.output,
                          'Order #24602 placed successfully!\n'
diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py
index acb0dc6f..e7862210 100644
--- a/tests/managers/block_tests.py
+++ b/tests/managers/block_tests.py
@@ -846,6 +846,52 @@ def test_order_block_duplicate_performance(self):
                 'useHourlyPricing': False
             },))
 
+    def test_order_block_duplicate_depdupe(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_duplicate_volume(
+            102,
+            origin_snapshot_id=470,
+            duplicate_size=1000,
+            duplicate_iops=2000,
+            duplicate_tier_level=None,
+            duplicate_snapshot_size=10,
+            dependent_duplicate=True
+        )
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189443},
+                    {'id': 190113},
+                    {'id': 190173},
+                    {'id': 191193}
+                ],
+                'volumeSize': 1000,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'osFormatType': {'keyName': 'LINUX'},
+                'duplicateOriginSnapshotId': 470,
+                'iops': 2000,
+                'useHourlyPricing': False,
+                'isDependentDuplicateFlag': 1
+            },))
+
     def test_order_block_duplicate_endurance_no_duplicate_snapshot(self):
         mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
         mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]
diff --git a/tests/managers/file_tests.py b/tests/managers/file_tests.py
index d6ca66c6..69c9c60e 100644
--- a/tests/managers/file_tests.py
+++ b/tests/managers/file_tests.py
@@ -742,6 +742,50 @@ def test_order_file_duplicate_endurance_no_duplicate_snapshot(self):
                 'useHourlyPricing': False
             },))
 
+    def test_order_file_duplicate_depdupe(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_duplicate_volume(
+            102,
+            origin_snapshot_id=470,
+            duplicate_size=1000,
+            duplicate_iops=None,
+            duplicate_tier_level=4,
+            duplicate_snapshot_size=10,
+            dependent_duplicate=True
+        )
+
+        self.assertEqual(fixtures.SoftLayer_Product_Order.placeOrder, result)
+
+        self.assert_called_with(
+            'SoftLayer_Product_Order',
+            'placeOrder',
+            args=({
+                'complexType': 'SoftLayer_Container_Product_Order_'
+                               'Network_Storage_AsAService',
+                'packageId': 759,
+                'prices': [
+                    {'id': 189433},
+                    {'id': 189453},
+                    {'id': 194763},
+                    {'id': 194703},
+                    {'id': 194943}
+                ],
+                'volumeSize': 1000,
+                'quantity': 1,
+                'location': 449500,
+                'duplicateOriginVolumeId': 102,
+                'duplicateOriginSnapshotId': 470,
+                'useHourlyPricing': False,
+                'isDependentDuplicateFlag': 1
+            },))
+
     def test_order_file_duplicate_endurance(self):
         mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects')
         mock.return_value = [fixtures.SoftLayer_Product_Package.SAAS_PACKAGE]