Repository: softlayer/softlayer-python
Pull request #1075 (2018-11-16 00:08:11)

dedicatedhost cancel, cancel-guests, and list-guests commands
`slcli dh cancel` makes a simple call to the deleteObject method of dedicated host. 
`slcli dh cancel-guests` cancels all guests into the dedicated hosts
`slcli dh list-guests`  lists guests in the dedicated host, it should be possible to filter, sortby and display more columns in the table

It should cover the request #1041


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17b9b49d..7c408f0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,5 @@
 # Change Log
 
-## [5.6.3] - 2018-11-07
-
-- Changes: https://github.com/softlayer/softlayer-python/compare/v5.6.0...v5.6.3
-
-+ #1065 Updated urllib3 and requests libraries due to CVE-2018-18074
-+ #1070 Fixed an ordering bug
-+ Updated release process and fab-file
-
 ## [5.6.0] - 2018-10-16
 - Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.3...v5.6.0
 
diff --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py
new file mode 100644
index 00000000..58ed85d3
--- /dev/null
+++ b/SoftLayer/CLI/dedicatedhost/cancel.py
@@ -0,0 +1,28 @@
+"""Cancel a dedicated host."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import formatting
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Cancel a dedicated host server immediately"""
+
+    mgr = SoftLayer.DedicatedHostManager(env.client)
+
+    host_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'dedicated host')
+
+    if not (env.skip_confirmations or formatting.no_going_back(host_id)):
+        raise exceptions.CLIAbort('Aborted')
+
+    mgr.cancel_host(host_id)
+
+    click.secho('Dedicated Host %s was cancelled' % host_id, fg='green')
diff --git a/SoftLayer/CLI/dedicatedhost/cancel_guests.py b/SoftLayer/CLI/dedicatedhost/cancel_guests.py
new file mode 100644
index 00000000..537828de
--- /dev/null
+++ b/SoftLayer/CLI/dedicatedhost/cancel_guests.py
@@ -0,0 +1,43 @@
+"""Cancel a dedicated host."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import formatting
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Cancel all virtual guests of the dedicated host immediately.
+
+       Use the 'slcli vs cancel' command to cancel an specific guest
+    """
+
+    dh_mgr = SoftLayer.DedicatedHostManager(env.client)
+
+    host_id = helpers.resolve_id(dh_mgr.resolve_ids, identifier, 'dedicated host')
+
+    if not (env.skip_confirmations or formatting.no_going_back(host_id)):
+        raise exceptions.CLIAbort('Aborted')
+
+    table = formatting.Table(['id', 'server name', 'status'])
+
+    result = dh_mgr.cancel_guests(host_id)
+
+    if result:
+        for status in result:
+            table.add_row([
+                status['id'],
+                status['fqdn'],
+                status['status']
+            ])
+
+        env.fout(table)
+    else:
+        click.secho('There is not any guest into the dedicated host %s' % host_id, fg='red')
diff --git a/SoftLayer/CLI/dedicatedhost/list_guests.py b/SoftLayer/CLI/dedicatedhost/list_guests.py
new file mode 100644
index 00000000..bec37a89
--- /dev/null
+++ b/SoftLayer/CLI/dedicatedhost/list_guests.py
@@ -0,0 +1,76 @@
+"""List guests which are in a dedicated host server."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import columns as column_helper
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+from SoftLayer.CLI import helpers
+
+COLUMNS = [
+    column_helper.Column('guid', ('globalIdentifier',)),
+    column_helper.Column('cpu', ('maxCpu',)),
+    column_helper.Column('memory', ('maxMemory',)),
+    column_helper.Column('datacenter', ('datacenter', 'name')),
+    column_helper.Column('primary_ip', ('primaryIpAddress',)),
+    column_helper.Column('backend_ip', ('primaryBackendIpAddress',)),
+    column_helper.Column(
+        'created_by',
+        ('billingItem', 'orderItem', 'order', 'userRecord', 'username')),
+    column_helper.Column('power_state', ('powerState', 'name')),
+    column_helper.Column(
+        'tags',
+        lambda server: formatting.tags(server.get('tagReferences')),
+        mask="tagReferences.tag.name"),
+]
+
+DEFAULT_COLUMNS = [
+    'id',
+    'hostname',
+    'domain',
+    'primary_ip',
+    'backend_ip',
+    'power_state'
+]
+
+
+@click.command()
+@click.argument('identifier')
+@click.option('--cpu', '-c', help='Number of CPU cores', type=click.INT)
+@click.option('--domain', '-D', help='Domain portion of the FQDN')
+@click.option('--hostname', '-H', help='Host portion of the FQDN')
+@click.option('--memory', '-m', help='Memory in mebibytes', type=click.INT)
+@helpers.multi_option('--tag', help='Filter by tags')
+@click.option('--sortby',
+              help='Column to sort by',
+              default='hostname',
+              show_default=True)
+@click.option('--columns',
+              callback=column_helper.get_formatter(COLUMNS),
+              help='Columns to display. [options: %s]'
+              % ', '.join(column.name for column in COLUMNS),
+              default=','.join(DEFAULT_COLUMNS),
+              show_default=True)
+@environment.pass_env
+def cli(env, identifier, sortby, cpu, domain, hostname, memory, tag, columns):
+    """List guests which are in a dedicated host server."""
+
+    mgr = SoftLayer.DedicatedHostManager(env.client)
+    guests = mgr.list_guests(host_id=identifier,
+                             cpus=cpu,
+                             hostname=hostname,
+                             domain=domain,
+                             memory=memory,
+                             tags=tag,
+                             mask=columns.mask())
+
+    table = formatting.Table(columns.columns)
+    table.sortby = sortby
+
+    for guest in guests:
+        table.add_row([value or formatting.blank()
+                       for value in columns.row(guest)])
+
+    env.fout(table)
diff --git a/SoftLayer/CLI/image/export.py b/SoftLayer/CLI/image/export.py
index eb9081ac..375de784 100644
--- a/SoftLayer/CLI/image/export.py
+++ b/SoftLayer/CLI/image/export.py
@@ -24,7 +24,7 @@ def cli(env, identifier, uri, ibm_api_key):
 
     The URI for an object storage object (.vhd/.iso file) of the format:
     swift://<objectStorageAccount>@<cluster>/<container>/<objectPath>
-    or cos://<regionName>/<bucketName>/<objectPath> if using IBM Cloud
+    or cos://<clusterName>/<bucketName>/<objectPath> if using IBM Cloud
     Object Storage
     """
 
diff --git a/SoftLayer/CLI/image/import.py b/SoftLayer/CLI/image/import.py
index 7f1b1e83..52556441 100644
--- a/SoftLayer/CLI/image/import.py
+++ b/SoftLayer/CLI/image/import.py
@@ -52,7 +52,7 @@ def cli(env, name, note, os_code, uri, ibm_api_key, root_key_id, wrapped_dek,
 
     The URI for an object storage object (.vhd/.iso file) of the format:
     swift://<objectStorageAccount>@<cluster>/<container>/<objectPath>
-    or cos://<regionName>/<bucketName>/<objectPath> if using IBM Cloud
+    or cos://<clusterName>/<bucketName>/<objectPath> if using IBM Cloud
     Object Storage
     """
 
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index fa46f36a..e80a0b50 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -37,6 +37,9 @@
     ('dedicatedhost:create', 'SoftLayer.CLI.dedicatedhost.create:cli'),
     ('dedicatedhost:create-options', 'SoftLayer.CLI.dedicatedhost.create_options:cli'),
     ('dedicatedhost:detail', 'SoftLayer.CLI.dedicatedhost.detail:cli'),
+    ('dedicatedhost:cancel', 'SoftLayer.CLI.dedicatedhost.cancel:cli'),
+    ('dedicatedhost:cancel-guests', 'SoftLayer.CLI.dedicatedhost.cancel_guests:cli'),
+    ('dedicatedhost:list-guests', 'SoftLayer.CLI.dedicatedhost.list_guests:cli'),
 
     ('cdn', 'SoftLayer.CLI.cdn'),
     ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'),
diff --git a/SoftLayer/CLI/securitygroup/rule.py b/SoftLayer/CLI/securitygroup/rule.py
index 81526231..4f624308 100644
--- a/SoftLayer/CLI/securitygroup/rule.py
+++ b/SoftLayer/CLI/securitygroup/rule.py
@@ -15,9 +15,7 @@
            'ethertype',
            'portRangeMin',
            'portRangeMax',
-           'protocol',
-           'createDate',
-           'modifyDate']
+           'protocol']
 
 
 @click.command()
@@ -51,9 +49,7 @@ def rule_list(env, securitygroup_id, sortby):
             rule.get('ethertype') or formatting.blank(),
             port_min,
             port_max,
-            rule.get('protocol') or formatting.blank(),
-            rule.get('createDate') or formatting.blank(),
-            rule.get('modifyDate') or formatting.blank()
+            rule.get('protocol') or formatting.blank()
         ])
 
     env.fout(table)
diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py
index b50fde3d..7bacd8bf 100644
--- a/SoftLayer/CLI/virt/create_options.py
+++ b/SoftLayer/CLI/virt/create_options.py
@@ -1,6 +1,5 @@
 """Virtual server order options."""
 # :license: MIT, see LICENSE for more details.
-# pylint: disable=too-many-statements
 import os
 import os.path
 
diff --git a/SoftLayer/CLI/vpn/ipsec/translation/add.py b/SoftLayer/CLI/vpn/ipsec/translation/add.py
index 952f7fd6..a0b7a35e 100644
--- a/SoftLayer/CLI/vpn/ipsec/translation/add.py
+++ b/SoftLayer/CLI/vpn/ipsec/translation/add.py
@@ -11,10 +11,12 @@
 
 @click.command()
 @click.argument('context_id', type=int)
+# todo: Update to utilize custom IP address type
 @click.option('-s',
               '--static-ip',
               required=True,
               help='Static IP address value')
+# todo: Update to utilize custom IP address type
 @click.option('-r',
               '--remote-ip',
               required=True,
diff --git a/SoftLayer/CLI/vpn/ipsec/translation/update.py b/SoftLayer/CLI/vpn/ipsec/translation/update.py
index 4f070900..b78585db 100644
--- a/SoftLayer/CLI/vpn/ipsec/translation/update.py
+++ b/SoftLayer/CLI/vpn/ipsec/translation/update.py
@@ -15,10 +15,12 @@
               required=True,
               type=int,
               help='Translation identifier to update')
+# todo: Update to utilize custom IP address type
 @click.option('-s',
               '--static-ip',
               default=None,
               help='Static IP address value')
+# todo: Update to utilize custom IP address type
 @click.option('-r',
               '--remote-ip',
               default=None,
diff --git a/SoftLayer/CLI/vpn/ipsec/update.py b/SoftLayer/CLI/vpn/ipsec/update.py
index 738a1d9f..4056f3b8 100644
--- a/SoftLayer/CLI/vpn/ipsec/update.py
+++ b/SoftLayer/CLI/vpn/ipsec/update.py
@@ -13,6 +13,7 @@
 @click.option('--friendly-name',
               default=None,
               help='Friendly name value')
+# todo: Update to utilize custom IP address type
 @click.option('--remote-peer',
               default=None,
               help='Remote peer IP address value')
diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py
index ee5f29ab..29e3f58d 100644
--- a/SoftLayer/consts.py
+++ b/SoftLayer/consts.py
@@ -5,7 +5,7 @@
 
     :license: MIT, see LICENSE for more details.
 """
-VERSION = 'v5.6.3'
+VERSION = 'v5.6.0'
 API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/'
 API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/'
 API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/'
diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py b/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py
index 94c8e5cc..43ab0ec3 100644
--- a/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py
+++ b/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py
@@ -76,3 +76,59 @@
     'id': 12345,
     'createDate': '2017-11-02T11:40:56-07:00'
 }
+
+deleteObject = True
+
+getGuests = [{
+    'id': 200,
+    'hostname': 'vs-test1',
+    'domain': 'test.sftlyr.ws',
+    'fullyQualifiedDomainName': 'vs-test1.test.sftlyr.ws',
+    'status': {'keyName': 'ACTIVE', 'name': 'Active'},
+    'datacenter': {'id': 50, 'name': 'TEST00',
+                   'description': 'Test Data Center'},
+    'powerState': {'keyName': 'RUNNING', 'name': 'Running'},
+    'maxCpu': 2,
+    'maxMemory': 1024,
+    'primaryIpAddress': '172.16.240.2',
+    'globalIdentifier': '1a2b3c-1701',
+    'primaryBackendIpAddress': '10.45.19.37',
+    'hourlyBillingFlag': False,
+    'billingItem': {
+        'id': 6327,
+        'recurringFee': 1.54,
+        'orderItem': {
+            'order': {
+                'userRecord': {
+                    'username': 'chechu',
+                }
+            }
+        }
+    },
+}, {
+    'id': 202,
+    'hostname': 'vs-test2',
+    'domain': 'test.sftlyr.ws',
+    'fullyQualifiedDomainName': 'vs-test2.test.sftlyr.ws',
+    'status': {'keyName': 'ACTIVE', 'name': 'Active'},
+    'datacenter': {'id': 50, 'name': 'TEST00',
+                   'description': 'Test Data Center'},
+    'powerState': {'keyName': 'RUNNING', 'name': 'Running'},
+    'maxCpu': 4,
+    'maxMemory': 4096,
+    'primaryIpAddress': '172.16.240.7',
+    'globalIdentifier': '05a8ac-6abf0',
+    'primaryBackendIpAddress': '10.45.19.35',
+    'hourlyBillingFlag': True,
+    'billingItem': {
+        'id': 6327,
+        'recurringFee': 1.54,
+        'orderItem': {
+            'order': {
+                'userRecord': {
+                    'username': 'chechu',
+                }
+            }
+        }
+    }
+}]
diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py
index e041e8d7..86258416 100644
--- a/SoftLayer/managers/dedicated_host.py
+++ b/SoftLayer/managers/dedicated_host.py
@@ -33,10 +33,144 @@ def __init__(self, client, ordering_manager=None):
         self.client = client
         self.account = client['Account']
         self.host = client['Virtual_DedicatedHost']
+        self.guest = client['Virtual_Guest']
 
         if ordering_manager is None:
             self.ordering_manager = ordering.OrderingManager(client)
 
+    def cancel_host(self, host_id):
+        """Cancel a dedicated host immediately, it fails if there are still guests in the host.
+
+        :param host_id: The ID of the dedicated host to be cancelled.
+        :return: True on success or an exception
+
+        Example::
+            # Cancels dedicated host id 12345
+            result = mgr.cancel_host(12345)
+
+        """
+        return self.host.deleteObject(id=host_id)
+
+    def cancel_guests(self, host_id):
+        """Cancel all guests into the dedicated host immediately.
+
+        To cancel an specified guest use the method VSManager.cancel_instance()
+
+        :param host_id: The ID of the dedicated host.
+        :return: The id, fqdn and status of all guests into a dictionary. The status
+                 could be 'Cancelled' or an exception message, The dictionary is empty
+                 if there isn't any guest in the dedicated host.
+
+        Example::
+            # Cancel guests of dedicated host id 12345
+            result = mgr.cancel_guests(12345)
+        """
+        result = []
+
+        guests = self.host.getGuests(id=host_id, mask='id,fullyQualifiedDomainName')
+
+        if guests:
+            for vs in guests:
+                status_info = {
+                    'id': vs['id'],
+                    'fqdn': vs['fullyQualifiedDomainName'],
+                    'status': self._delete_guest(vs['id'])
+                }
+                result.append(status_info)
+
+        return result
+
+    def list_guests(self, host_id, tags=None, cpus=None, memory=None, hostname=None,
+                    domain=None, local_disk=None, nic_speed=None, public_ip=None,
+                    private_ip=None, **kwargs):
+        """Retrieve a list of all virtual servers on the dedicated host.
+
+        Example::
+
+            # Print out a list of instances with 4 cpu cores in the host id 12345.
+
+            for vsi in mgr.list_guests(host_id=12345, cpus=4):
+               print vsi['fullyQualifiedDomainName'], vsi['primaryIpAddress']
+
+            # Using a custom object-mask. Will get ONLY what is specified
+            object_mask = "mask[hostname,monitoringRobot[robotStatus]]"
+            for vsi in mgr.list_guests(mask=object_mask,cpus=4):
+                print vsi
+
+        :param integer host_id: the identifier of dedicated host
+        :param list tags: filter based on list of tags
+        :param integer cpus: filter based on number of CPUS
+        :param integer memory: filter based on amount of memory
+        :param string hostname: filter based on hostname
+        :param string domain: filter based on domain
+        :param string local_disk: filter based on local_disk
+        :param integer nic_speed: filter based on network speed (in MBPS)
+        :param string public_ip: filter based on public ip address
+        :param string private_ip: filter based on private ip address
+        :param dict \\*\\*kwargs: response-level options (mask, limit, etc.)
+        :returns: Returns a list of dictionaries representing the matching
+                  virtual servers
+        """
+        if 'mask' not in kwargs:
+            items = [
+                'id',
+                'globalIdentifier',
+                'hostname',
+                'domain',
+                'fullyQualifiedDomainName',
+                'primaryBackendIpAddress',
+                'primaryIpAddress',
+                'lastKnownPowerState.name',
+                'hourlyBillingFlag',
+                'powerState',
+                'maxCpu',
+                'maxMemory',
+                'datacenter',
+                'activeTransaction.transactionStatus[friendlyName,name]',
+                'status',
+            ]
+            kwargs['mask'] = "mask[%s]" % ','.join(items)
+
+        _filter = utils.NestedDict(kwargs.get('filter') or {})
+
+        if tags:
+            _filter['guests']['tagReferences']['tag']['name'] = {
+                'operation': 'in',
+                'options': [{'name': 'data', 'value': tags}],
+            }
+
+        if cpus:
+            _filter['guests']['maxCpu'] = utils.query_filter(cpus)
+
+        if memory:
+            _filter['guests']['maxMemory'] = utils.query_filter(memory)
+
+        if hostname:
+            _filter['guests']['hostname'] = utils.query_filter(hostname)
+
+        if domain:
+            _filter['guests']['domain'] = utils.query_filter(domain)
+
+        if local_disk is not None:
+            _filter['guests']['localDiskFlag'] = (
+                utils.query_filter(bool(local_disk)))
+
+        if nic_speed:
+            _filter['guests']['networkComponents']['maxSpeed'] = (
+                utils.query_filter(nic_speed))
+
+        if public_ip:
+            _filter['guests']['primaryIpAddress'] = (
+                utils.query_filter(public_ip))
+
+        if private_ip:
+            _filter['guests']['primaryBackendIpAddress'] = (
+                utils.query_filter(private_ip))
+
+        kwargs['filter'] = _filter.to_dict()
+        kwargs['iter'] = True
+        return self.host.getGuests(id=host_id, **kwargs)
+
     def list_instances(self, tags=None, cpus=None, memory=None, hostname=None,
                        disk=None, datacenter=None, **kwargs):
         """Retrieve a list of all dedicated hosts on the account
@@ -384,3 +518,13 @@ def get_router_options(self, datacenter=None, flavor=None):
         item = self._get_item(package, flavor)
 
         return self._get_backend_router(location['location']['locationPackageDetails'], item)
+
+    def _delete_guest(self, guest_id):
+        """Deletes a guest and returns 'Cancelled' or and Exception message"""
+        msg = 'Cancelled'
+        try:
+            self.guest.deleteObject(id=guest_id)
+        except SoftLayer.SoftLayerAPIError as e:
+            msg = 'Exception: ' + e.faultString
+
+        return msg
diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py
index 9f97a1d3..6980f439 100644
--- a/SoftLayer/managers/hardware.py
+++ b/SoftLayer/managers/hardware.py
@@ -92,7 +92,7 @@ def cancel_hardware(self, hardware_id, reason='unneeded', comment='', immediate=
         billing_id = hw_billing['billingItem']['id']
 
         if immediate and not hw_billing['hourlyBillingFlag']:
-            LOGGER.warning("Immediate cancelation of montly servers is not guaranteed."
+            LOGGER.warning("Immediate cancelation of montly servers is not guaranteed. " +
                            "Please check the cancelation ticket for updates.")
 
             result = self.client.call('Billing_Item', 'cancelItem',
diff --git a/SoftLayer/managers/image.py b/SoftLayer/managers/image.py
index 35f7c60c..abd60ba8 100644
--- a/SoftLayer/managers/image.py
+++ b/SoftLayer/managers/image.py
@@ -131,15 +131,15 @@ def import_image_from_uri(self, name, uri, os_code=None, note=None,
             (.vhd/.iso file) of the format:
             swift://<objectStorageAccount>@<cluster>/<container>/<objectPath>
             or (.vhd/.iso/.raw file) of the format:
-            cos://<regionName>/<bucketName>/<objectPath> if using IBM Cloud
+            cos://<clusterName>/<bucketName>/<objectPath> if using IBM Cloud
             Object Storage
         :param string os_code: The reference code of the operating system
         :param string note: Note to add to the image
         :param string ibm_api_key: Ibm Api Key needed to communicate with ICOS
-            and Key Protect
+        and Key Protect
         :param string root_key_id: ID of the root key in Key Protect
         :param string wrapped_dek: Wrapped Data Encryption Key provided by
-            IBM KeyProtect
+        IBM KeyProtect
         :param string kp_id: ID of the IBM Key Protect Instance
         :param boolean cloud_init: Specifies if image is cloud-init
         :param boolean byol: Specifies if image is bring your own license
@@ -173,10 +173,10 @@ def export_image_to_uri(self, image_id, uri, ibm_api_key=None):
         :param int image_id: The ID of the image
         :param string uri: The URI for object storage of the format
             swift://<objectStorageAccount>@<cluster>/<container>/<objectPath>
-            or cos://<regionName>/<bucketName>/<objectPath> if using IBM Cloud
+            or cos://<clusterName>/<bucketName>/<objectPath> if using IBM Cloud
             Object Storage
         :param string ibm_api_key: Ibm Api Key needed to communicate with IBM
-            Cloud Object Storage
+        Cloud Object Storage
         """
         if 'cos://' in uri:
             return self.vgbdtg.copyToIcos({
diff --git a/SoftLayer/managers/ipsec.py b/SoftLayer/managers/ipsec.py
index 29317516..130623c4 100644
--- a/SoftLayer/managers/ipsec.py
+++ b/SoftLayer/managers/ipsec.py
@@ -227,6 +227,10 @@ def update_translation(self, context_id, translation_id, static_ip=None,
             translation.pop('customerIpAddressId', None)
         if notes is not None:
             translation['notes'] = notes
+        # todo: Update this signature to return the updated translation
+        #       once internal and customer IP addresses can be fetched
+        #       and set on the translation object, i.e. that which is
+        #       currently being handled in get_translations
         self.context.editAddressTranslation(translation, id=context_id)
         return True
 
diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py
index 4223143a..b568e889 100644
--- a/SoftLayer/managers/network.py
+++ b/SoftLayer/managers/network.py
@@ -372,7 +372,7 @@ def get_securitygroup(self, group_id, **kwargs):
                 'description,'
                 '''rules[id, remoteIp, remoteGroupId,
                          direction, ethertype, portRangeMin,
-                         portRangeMax, protocol, createDate, modifyDate],'''
+                         portRangeMax, protocol],'''
                 '''networkComponentBindings[
                     networkComponent[
                         id,
diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py
index c0eca1dc..18a606b0 100644
--- a/SoftLayer/managers/ordering.py
+++ b/SoftLayer/managers/ordering.py
@@ -378,10 +378,8 @@ def get_item_price_id(core, prices):
             if not price['locationGroupId']:
                 capacity_min = int(price.get('capacityRestrictionMinimum', -1))
                 capacity_max = int(price.get('capacityRestrictionMaximum', -1))
-                # return first match if no restirction, or no core to check
-                if capacity_min == -1 or core is None:
+                if capacity_min == -1:
                     price_id = price['id']
-                # this check is mostly to work nicely with preset configs
                 elif capacity_min <= int(core) <= capacity_max:
                     price_id = price['id']
         return price_id
diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py
index dbbaa98c..03595d8d 100644
--- a/SoftLayer/managers/vs.py
+++ b/SoftLayer/managers/vs.py
@@ -503,16 +503,15 @@ def verify_create_instance(self, **kwargs):
                 'domain': u'test01.labs.sftlyr.ws',
                 'hostname': u'minion05',
                 'datacenter': u'hkg02',
-                'flavor': 'BL1_1X2X100'
                 'dedicated': False,
                 'private': False,
+                'cpus': 1,
                 'os_code' : u'UBUNTU_LATEST',
                 'hourly': True,
                 'ssh_keys': [1234],
                 'disks': ('100','25'),
                 'local_disk': True,
-                'tags': 'test, pleaseCancel',
-                'public_security_groups': [12, 15]
+                'memory': 1024
             }
 
             vsi = mgr.verify_create_instance(**new_vsi)
@@ -537,14 +536,15 @@ def create_instance(self, **kwargs):
                 'domain': u'test01.labs.sftlyr.ws',
                 'hostname': u'minion05',
                 'datacenter': u'hkg02',
-                'flavor': 'BL1_1X2X100'
                 'dedicated': False,
                 'private': False,
+                'cpus': 1,
                 'os_code' : u'UBUNTU_LATEST',
                 'hourly': True,
                 'ssh_keys': [1234],
                 'disks': ('100','25'),
                 'local_disk': True,
+                'memory': 1024,
                 'tags': 'test, pleaseCancel',
                 'public_security_groups': [12, 15]
             }
@@ -607,16 +607,17 @@ def create_instances(self, config_list):
             # Define the instance we want to create.
             new_vsi = {
                 'domain': u'test01.labs.sftlyr.ws',
-                'hostname': u'minion05',
+                'hostname': u'multi-test',
                 'datacenter': u'hkg02',
-                'flavor': 'BL1_1X2X100'
                 'dedicated': False,
                 'private': False,
+                'cpus': 1,
                 'os_code' : u'UBUNTU_LATEST',
                 'hourly': True,
-                'ssh_keys': [1234],
+                'ssh_keys': [87634],
                 'disks': ('100','25'),
                 'local_disk': True,
+                'memory': 1024,
                 'tags': 'test, pleaseCancel',
                 'public_security_groups': [12, 15]
             }
diff --git a/fabfile.py b/fabfile.py
index cd6a968f..c864c537 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -12,8 +12,8 @@ def make_html():
 
 def upload():
     "Upload distribution to PyPi"
-    local('python setup.py sdist bdist_wheel')
-    local('twine upload dist/*')
+    local('python setup.py sdist upload')
+    local('python setup.py bdist_wheel upload')
 
 
 def clean():
diff --git a/setup.py b/setup.py
index e99860a9..b4c86c2f 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@
 
 setup(
     name='SoftLayer',
-    version='5.6.3',
+    version='5.6.0',
     description=DESCRIPTION,
     long_description=LONG_DESCRIPTION,
     author='SoftLayer Technologies, Inc.',
@@ -33,12 +33,12 @@
         'six >= 1.7.0',
         'ptable >= 0.9.2',
         'click >= 7',
-        'requests >= 2.20.0',
+        'requests == 2.19.1',
         'prompt_toolkit >= 0.53',
         'pygments >= 2.0.0',
-        'urllib3 >= 1.24'
+        'urllib3 == 1.22'
     ],
-    keywords=['softlayer', 'cloud', 'slcli'],
+    keywords=['softlayer', 'cloud'],
     classifiers=[
         'Environment :: Console',
         'Environment :: Web Environment',
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index f5ec61df..224313b0 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,5 +1,5 @@
 name: slcli # check to see if it's available
-version: '5.6.3+git' # check versioning
+version: '5.6.0+git' # check versioning
 summary: Python based SoftLayer API Tool. # 79 char long summary
 description: |
     A command-line interface is also included and can be used to manage various SoftLayer products and services.
diff --git a/tools/requirements.txt b/tools/requirements.txt
index cd4a8942..17bba946 100644
--- a/tools/requirements.txt
+++ b/tools/requirements.txt
@@ -1,7 +1,6 @@
+requests == 2.19.1
+click >= 5, < 7
+prettytable >= 0.7.0
 six >= 1.7.0
-ptable >= 0.9.2
-click >= 7
-requests >= 2.20.0
-prompt_toolkit >= 0.53
-pygments >= 2.0.0
-urllib3 >= 1.24
\ No newline at end of file
+prompt_toolkit
+urllib3 == 1.22
diff --git a/tox.ini b/tox.ini
index e5c7c2f6..ae456665 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,10 +36,6 @@ commands =
            -d locally-disabled \
            -d no-else-return \ 
            -d len-as-condition \
-           -d useless-object-inheritance \
-           -d consider-using-in \
-           -d consider-using-dict-comprehension \
-           -d useless-import-alias \
            --max-args=25 \
            --max-branches=20 \
            --max-statements=65 \


Test patch:
diff --git a/tests/CLI/modules/dedicatedhost_tests.py b/tests/CLI/modules/dedicatedhost_tests.py
index 1769d8cb..077c3f03 100644
--- a/tests/CLI/modules/dedicatedhost_tests.py
+++ b/tests/CLI/modules/dedicatedhost_tests.py
@@ -337,3 +337,70 @@ def test_create_verify_no_price_or_more_than_one(self):
             'quantity': 1},)
 
         self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=args)
+
+    @mock.patch('SoftLayer.DedicatedHostManager.cancel_host')
+    def test_cancel_host(self, cancel_mock):
+        result = self.run_command(['--really', 'dedicatedhost', 'cancel', '12345'])
+
+        self.assert_no_fail(result)
+        cancel_mock.assert_called_with(12345)
+
+        self.assertEqual(str(result.output), 'Dedicated Host 12345 was cancelled\n')
+
+    def test_cancel_host_abort(self):
+        result = self.run_command(['dedicatedhost', 'cancel', '12345'])
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.CLIAbort)
+
+    def test_cancel_guests(self):
+        vs1 = {'id': 987, 'fullyQualifiedDomainName': 'foobar.example.com'}
+        vs2 = {'id': 654, 'fullyQualifiedDomainName': 'wombat.example.com'}
+        guests = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getGuests')
+        guests.return_value = [vs1, vs2]
+
+        vs_status1 = {'id': 987, 'server name': 'foobar.example.com', 'status': 'Cancelled'}
+        vs_status2 = {'id': 654, 'server name': 'wombat.example.com', 'status': 'Cancelled'}
+        expected_result = [vs_status1, vs_status2]
+
+        result = self.run_command(['--really', 'dedicatedhost', 'cancel-guests', '12345'])
+        self.assert_no_fail(result)
+
+        self.assertEqual(expected_result, json.loads(result.output))
+
+    def test_cancel_guests_empty_list(self):
+        guests = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getGuests')
+        guests.return_value = []
+
+        result = self.run_command(['--really', 'dedicatedhost', 'cancel-guests', '12345'])
+        self.assert_no_fail(result)
+
+        self.assertEqual(str(result.output), 'There is not any guest into the dedicated host 12345\n')
+
+    def test_cancel_guests_abort(self):
+        result = self.run_command(['dedicatedhost', 'cancel-guests', '12345'])
+        self.assertEqual(result.exit_code, 2)
+
+        self.assertIsInstance(result.exception, exceptions.CLIAbort)
+
+    def test_list_guests(self):
+        result = self.run_command(['dh', 'list-guests', '123', '--tag=tag'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(json.loads(result.output),
+                         [{'hostname': 'vs-test1',
+                           'domain': 'test.sftlyr.ws',
+                           'primary_ip': '172.16.240.2',
+                           'id': 200,
+                           'power_state': 'Running',
+                           'backend_ip': '10.45.19.37'},
+                          {'hostname': 'vs-test2',
+                           'domain': 'test.sftlyr.ws',
+                           'primary_ip': '172.16.240.7',
+                           'id': 202,
+                           'power_state': 'Running',
+                           'backend_ip': '10.45.19.35'}])
+
+    def _get_cancel_guests_return(self):
+        vs_status1 = {'id': 123, 'fqdn': 'foobar.example.com', 'status': 'Cancelled'}
+        vs_status2 = {'id': 456, 'fqdn': 'wombat.example.com', 'status': 'Cancelled'}
+        return [vs_status1, vs_status2]
diff --git a/tests/CLI/modules/securitygroup_tests.py b/tests/CLI/modules/securitygroup_tests.py
index 9b7e4c6a..65c496b6 100644
--- a/tests/CLI/modules/securitygroup_tests.py
+++ b/tests/CLI/modules/securitygroup_tests.py
@@ -118,9 +118,7 @@ def test_securitygroup_rule_list(self):
                            'remoteGroupId': None,
                            'protocol': None,
                            'portRangeMin': None,
-                           'portRangeMax': None,
-                           'createDate': None,
-                           'modifyDate': None}],
+                           'portRangeMax': None}],
                          json.loads(result.output))
 
     def test_securitygroup_rule_add(self):
diff --git a/tests/managers/dedicated_host_tests.py b/tests/managers/dedicated_host_tests.py
index bdfa6d3f..6888db3c 100644
--- a/tests/managers/dedicated_host_tests.py
+++ b/tests/managers/dedicated_host_tests.py
@@ -540,6 +540,47 @@ def test_get_default_router_no_router_found(self):
         self.assertRaises(exceptions.SoftLayerError,
                           self.dedicated_host._get_default_router, routers, 'notFound')
 
+    def test_cancel_host(self):
+        result = self.dedicated_host.cancel_host(789)
+
+        self.assertEqual(result, True)
+        self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'deleteObject', identifier=789)
+
+    def test_cancel_guests(self):
+        vs1 = {'id': 987, 'fullyQualifiedDomainName': 'foobar.example.com'}
+        vs2 = {'id': 654, 'fullyQualifiedDomainName': 'wombat.example.com'}
+        self.dedicated_host.host = mock.Mock()
+        self.dedicated_host.host.getGuests.return_value = [vs1, vs2]
+
+        # Expected result
+        vs_status1 = {'id': 987, 'fqdn': 'foobar.example.com', 'status': 'Cancelled'}
+        vs_status2 = {'id': 654, 'fqdn': 'wombat.example.com', 'status': 'Cancelled'}
+        delete_status = [vs_status1, vs_status2]
+
+        result = self.dedicated_host.cancel_guests(789)
+
+        self.assertEqual(result, delete_status)
+
+    def test_cancel_guests_empty_list(self):
+        self.dedicated_host.host = mock.Mock()
+        self.dedicated_host.host.getGuests.return_value = []
+
+        result = self.dedicated_host.cancel_guests(789)
+
+        self.assertEqual(result, [])
+
+    def test_delete_guest(self):
+        result = self.dedicated_host._delete_guest(123)
+        self.assertEqual(result, 'Cancelled')
+
+        # delete_guest should return the exception message in case it fails
+        error_raised = SoftLayer.SoftLayerAPIError('SL Exception', 'SL message')
+        self.dedicated_host.guest = mock.Mock()
+        self.dedicated_host.guest.deleteObject.side_effect = error_raised
+
+        result = self.dedicated_host._delete_guest(369)
+        self.assertEqual(result, 'Exception: SL message')
+
     def _get_routers_sample(self):
         routers = [
             {
@@ -647,3 +688,34 @@ def _get_package(self):
         }
 
         return package
+
+    def test_list_guests(self):
+        results = self.dedicated_host.list_guests(12345)
+
+        for result in results:
+            self.assertIn(result['id'], [200, 202])
+        self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getGuests', identifier=12345)
+
+    def test_list_guests_with_filters(self):
+        self.dedicated_host.list_guests(12345, tags=['tag1', 'tag2'], cpus=2, memory=1024,
+                                        hostname='hostname', domain='example.com', nic_speed=100,
+                                        public_ip='1.2.3.4', private_ip='4.3.2.1')
+
+        _filter = {
+            'guests': {
+                'domain': {'operation': '_= example.com'},
+                'tagReferences': {
+                    'tag': {'name': {
+                        'operation': 'in',
+                        'options': [{
+                            'name': 'data', 'value': ['tag1', 'tag2']}]}}},
+                'maxCpu': {'operation': 2},
+                'maxMemory': {'operation': 1024},
+                'hostname': {'operation': '_= hostname'},
+                'networkComponents': {'maxSpeed': {'operation': 100}},
+                'primaryIpAddress': {'operation': '_= 1.2.3.4'},
+                'primaryBackendIpAddress': {'operation': '_= 4.3.2.1'}
+            }
+        }
+        self.assert_called_with('SoftLayer_Virtual_DedicatedHost', 'getGuests',
+                                identifier=12345, filter=_filter)
diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py
index b5d2aaa4..4b2c431c 100644
--- a/tests/managers/ordering_tests.py
+++ b/tests/managers/ordering_tests.py
@@ -569,31 +569,3 @@ def test_get_item_price_id_with_capacity_restriction(self):
         price_id = self.ordering.get_item_price_id("8", price1)
 
         self.assertEqual(1234, price_id)
-
-    def test_issues1067(self):
-        # https://github.com/softlayer/softlayer-python/issues/1067
-        item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems')
-        item_mock_return = [
-            {
-                'id': 10453,
-                'itemCategory': {'categoryCode': 'server'},
-                'keyName': 'INTEL_INTEL_XEON_4110_2_10',
-                'prices': [
-                    {
-                        'capacityRestrictionMaximum': '2',
-                        'capacityRestrictionMinimum': '2',
-                        'capacityRestrictionType': 'PROCESSOR',
-                        'categories': [{'categoryCode': 'os'}],
-                        'id': 201161,
-                        'locationGroupId': None,
-                        'recurringFee': '250',
-                        'setupFee': '0'
-                    }
-                ]
-            }
-        ]
-        item_mock.return_value = item_mock_return
-        item_keynames = ['INTEL_INTEL_XEON_4110_2_10']
-        package = 'DUAL_INTEL_XEON_PROCESSOR_SCALABLE_FAMILY_4_DRIVES'
-        result = self.ordering.get_price_id_list(package, item_keynames, None)
-        self.assertIn(201161, result)
diff --git a/tools/test-requirements.txt b/tools/test-requirements.txt
index 56ba8fe6..138fe84d 100644
--- a/tools/test-requirements.txt
+++ b/tools/test-requirements.txt
@@ -4,10 +4,5 @@ pytest-cov
 mock
 sphinx
 testtools
-six >= 1.7.0
-ptable >= 0.9.2
-click >= 7
-requests >= 2.20.0
-prompt_toolkit >= 0.53
-pygments >= 2.0.0
-urllib3 >= 1.24
\ No newline at end of file
+urllib3 == 1.22
+requests == 2.19.1