Repository: softlayer/softlayer-python
Pull request #1150 (2019-05-29 22:08:44)

Bandwidth usage details for vs and server
#1003 

## New Commands
- vs bandwidth
- hw bandwidth

## Updated Commands
Added bandwidth allotment details
- vs detail 
- hw detail



Patch:
diff --git a/.travis.yml b/.travis.yml
index 735f4f69..d69b42d6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,18 +1,20 @@
+# https://docs.travis-ci.com/user/languages/python/#python-37-and-higher
+dist: xenial
 language: python
 sudo: false
 matrix:
   include:
-    - python: "2.7"
-      env: TOX_ENV=py27
     - python: "3.5"
       env: TOX_ENV=py35
     - python: "3.6"
       env: TOX_ENV=py36
-    - python: "pypy2.7-5.8.0"
+    - python: "3.7"
+      env: TOX_ENV=py37
+    - python: "pypy3.5"
       env: TOX_ENV=pypy
-    - python: "2.7"
+    - python: "3.6"
       env: TOX_ENV=analysis
-    - python: "2.7"
+    - python: "3.6"
       env: TOX_ENV=coverage
 install:
  - pip install tox
diff --git a/README.rst b/README.rst
index 24b3625e..177f1514 100644
--- a/README.rst
+++ b/README.rst
@@ -31,9 +31,9 @@ http://softlayer.github.io/softlayer-python/.
 Additional API documentation can be found on the SoftLayer Development Network:
 
 * `SoftLayer API reference
-  <https://sldn.softlayer.com/reference/softlayerapi>`_
+  <http://developer.softlayer.com/reference/softlayerapi>`_
 * `Object mask information and examples
-  <https://sldn.softlayer.com/article/object-masks>`_
+  <http://developer.softlayer.com/article/Object-Masks>`_
 * `Code Examples
   <https://softlayer.github.io/python/>`_
 
diff --git a/SoftLayer/CLI/hardware/bandwidth.py b/SoftLayer/CLI/hardware/bandwidth.py
new file mode 100644
index 00000000..efe821c2
--- /dev/null
+++ b/SoftLayer/CLI/hardware/bandwidth.py
@@ -0,0 +1,45 @@
+"""Get details for a hardware device."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import helpers
+from SoftLayer.CLI.virt.bandwidth import create_bandwidth_table
+
+
+@click.command()
+@click.argument('identifier')
+@click.option('--start_date', '-s', type=click.STRING, required=True,
+              help="Start Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss,")
+@click.option('--end_date', '-e', type=click.STRING, required=True,
+              help="End Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss")
+@click.option('--summary_period', '-p', type=click.INT, default=3600, show_default=True,
+              help="300, 600, 1800, 3600, 43200 or 86400 seconds")
+@click.option('--quite_summary', '-q', is_flag=True, default=False, show_default=True,
+              help="Only show the summary table")
+@environment.pass_env
+def cli(env, identifier, start_date, end_date, summary_period, quite_summary):
+    """Bandwidth data over date range. Bandwidth is listed in GB
+
+    Using just a date might get you times off by 1 hour, use T00:01 to get just the specific days data
+    Timezones can also be included with the YYYY-MM-DDTHH:mm:ss.00000-HH:mm format.
+
+    Due to some rounding and date alignment details, results here might be slightly different than
+    results in the control portal.
+
+    Example::
+
+        slcli hw bandwidth 1234 -s 2019-05-01T00:01 -e 2019-05-02T00:00:01.00000-12:00
+    """
+    hardware = SoftLayer.HardwareManager(env.client)
+    hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware')
+    data = hardware.get_bandwidth_data(hardware_id, start_date, end_date, None, summary_period)
+
+    title = "Bandwidth Report: %s - %s" % (start_date, end_date)
+    table, sum_table = create_bandwidth_table(data, summary_period, title)
+
+    env.fout(sum_table)
+    if not quite_summary:
+        env.fout(table)
diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py
index 4382fc36..e254ad33 100644
--- a/SoftLayer/CLI/hardware/detail.py
+++ b/SoftLayer/CLI/hardware/detail.py
@@ -57,6 +57,10 @@ def cli(env, identifier, passwords, price):
 
     table.add_row(['vlans', vlan_table])
 
+    bandwidth = hardware.get_bandwidth_allocation(hardware_id)
+    bw_table = _bw_table(bandwidth)
+    table.add_row(['Bandwidth', bw_table])
+
     if result.get('notes'):
         table.add_row(['notes', result['notes']])
 
@@ -85,3 +89,17 @@ def cli(env, identifier, passwords, price):
     table.add_row(['tags', formatting.tags(result['tagReferences'])])
 
     env.fout(table)
+
+
+def _bw_table(bw_data):
+    """Generates a bandwidth useage table"""
+    table = formatting.Table(['Type', 'In GB', 'Out GB', 'Allotment'])
+    for bw_point in bw_data.get('useage'):
+        bw_type = 'Private'
+        allotment = 'N/A'
+        if bw_point['type']['alias'] == 'PUBLIC_SERVER_BW':
+            bw_type = 'Public'
+            allotment = bw_data['allotment'].get('amount', '-')
+
+        table.add_row([bw_type, bw_point['amountIn'], bw_point['amountOut'], allotment])
+    return table
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 6736b233..62c1baa4 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -19,6 +19,7 @@
     ('account:summary', 'SoftLayer.CLI.account.summary:cli'),
 
     ('virtual', 'SoftLayer.CLI.virt'),
+    ('virtual:bandwidth', 'SoftLayer.CLI.virt.bandwidth:cli'),
     ('virtual:cancel', 'SoftLayer.CLI.virt.cancel:cli'),
     ('virtual:capture', 'SoftLayer.CLI.virt.capture:cli'),
     ('virtual:create', 'SoftLayer.CLI.virt.create:cli'),
@@ -213,6 +214,7 @@
     ('rwhois:show', 'SoftLayer.CLI.rwhois.show:cli'),
 
     ('hardware', 'SoftLayer.CLI.hardware'),
+    ('hardware:bandwidth', 'SoftLayer.CLI.hardware.bandwidth:cli'),
     ('hardware:cancel', 'SoftLayer.CLI.hardware.cancel:cli'),
     ('hardware:cancel-reasons', 'SoftLayer.CLI.hardware.cancel_reasons:cli'),
     ('hardware:create', 'SoftLayer.CLI.hardware.create:cli'),
diff --git a/SoftLayer/CLI/virt/bandwidth.py b/SoftLayer/CLI/virt/bandwidth.py
new file mode 100644
index 00000000..2f29cc7f
--- /dev/null
+++ b/SoftLayer/CLI/virt/bandwidth.py
@@ -0,0 +1,103 @@
+"""Get details for a hardware device."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+from SoftLayer.CLI import helpers
+from SoftLayer import utils
+
+
+@click.command()
+@click.argument('identifier')
+@click.option('--start_date', '-s', type=click.STRING, required=True,
+              help="Start Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss,")
+@click.option('--end_date', '-e', type=click.STRING, required=True,
+              help="End Date YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss")
+@click.option('--summary_period', '-p', type=click.INT, default=3600, show_default=True,
+              help="300, 600, 1800, 3600, 43200 or 86400 seconds")
+@click.option('--quite_summary', '-q', is_flag=True, default=False, show_default=True,
+              help="Only show the summary table")
+@environment.pass_env
+def cli(env, identifier, start_date, end_date, summary_period, quite_summary):
+    """Bandwidth data over date range. Bandwidth is listed in GB
+
+    Using just a date might get you times off by 1 hour, use T00:01 to get just the specific days data
+    Timezones can also be included with the YYYY-MM-DDTHH:mm:ss.00000-HH:mm format.
+
+    Due to some rounding and date alignment details, results here might be slightly different than
+    results in the control portal.
+
+    Example::
+
+        slcli hw bandwidth 1234 -s 2019-05-01T00:01 -e 2019-05-02T00:00:01.00000-12:00
+    """
+    vsi = SoftLayer.VSManager(env.client)
+    vsi_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS')
+    data = vsi.get_bandwidth_data(vsi_id, start_date, end_date, None, summary_period)
+
+    title = "Bandwidth Report: %s - %s" % (start_date, end_date)
+    table, sum_table = create_bandwidth_table(data, summary_period, title)
+
+    env.fout(sum_table)
+    if not quite_summary:
+        env.fout(table)
+
+
+def create_bandwidth_table(data, summary_period, title="Bandwidth Report"):
+    """Create 2 tables, bandwidth and sumamry. Used here and in hw bandwidth command"""
+
+    formatted_data = {}
+    for point in data:
+        key = utils.clean_time(point['dateTime'])
+        data_type = point['type']
+        # conversion from byte to megabyte
+        value = round(float(point['counter']) / 2 ** 20, 4)
+        if formatted_data.get(key) is None:
+            formatted_data[key] = {}
+        formatted_data[key][data_type] = float(value)
+
+    table = formatting.Table(['Date', 'Pub In', 'Pub Out', 'Pri In', 'Pri Out'], title=title)
+
+    sum_table = formatting.Table(['Type', 'Sum GB', 'Average MBps', 'Max GB', 'Max Date'], title="Summary")
+
+    # Required to specify keyName because getBandwidthTotals returns other counter types for some reason.
+    bw_totals = [
+        {'keyName': 'publicIn_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pub In'},
+        {'keyName': 'publicOut_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pub Out'},
+        {'keyName': 'privateIn_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pri In'},
+        {'keyName': 'privateOut_net_octet', 'sum': 0.0, 'max': 0, 'name': 'Pri Out'},
+    ]
+
+    for point in formatted_data:
+        new_row = [point]
+        for bw_type in bw_totals:
+            counter = formatted_data[point].get(bw_type['keyName'], 0)
+            new_row.append(mb_to_gb(counter))
+            bw_type['sum'] = bw_type['sum'] + counter
+            if counter > bw_type['max']:
+                bw_type['max'] = counter
+                bw_type['maxDate'] = point
+        table.add_row(new_row)
+
+    for bw_type in bw_totals:
+        total = bw_type.get('sum', 0.0)
+        average = 0
+        if total > 0:
+            average = round(total / len(formatted_data) / summary_period, 4)
+        sum_table.add_row([
+            bw_type.get('name'),
+            mb_to_gb(total),
+            average,
+            mb_to_gb(bw_type.get('max')),
+            bw_type.get('maxDate')
+        ])
+
+    return table, sum_table
+
+
+def mb_to_gb(mbytes):
+    """Converts a MegaByte int to GigaByte. mbytes/2^10"""
+    return round(mbytes / 2 ** 10, 4)
diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py
index 7a418dad..feb03cf8 100644
--- a/SoftLayer/CLI/virt/detail.py
+++ b/SoftLayer/CLI/virt/detail.py
@@ -39,65 +39,42 @@ def cli(env, identifier, passwords=False, price=False):
     table.add_row(['domain', result['domain']])
     table.add_row(['fqdn', result['fullyQualifiedDomainName']])
     table.add_row(['status', formatting.FormattedItem(
-        result['status']['keyName'] or formatting.blank(),
-        result['status']['name'] or formatting.blank()
+        result['status']['keyName'],
+        result['status']['name']
     )])
     table.add_row(['state', formatting.FormattedItem(
         utils.lookup(result, 'powerState', 'keyName'),
         utils.lookup(result, 'powerState', 'name'),
     )])
     table.add_row(['active_transaction', formatting.active_txn(result)])
-    table.add_row(['datacenter',
-                   result['datacenter']['name'] or formatting.blank()])
+    table.add_row(['datacenter', result['datacenter']['name'] or formatting.blank()])
     _cli_helper_dedicated_host(env, result, table)
     operating_system = utils.lookup(result,
                                     'operatingSystem',
                                     'softwareLicense',
                                     'softwareDescription') or {}
-    table.add_row(['os', operating_system.get('name') or formatting.blank()])
-    table.add_row(['os_version',
-                   operating_system.get('version') or formatting.blank()])
+    table.add_row(['os', operating_system.get('name', '-')])
+    table.add_row(['os_version', operating_system.get('version', '-')])
     table.add_row(['cores', result['maxCpu']])
     table.add_row(['memory', formatting.mb_to_gb(result['maxMemory'])])
-    table.add_row(['public_ip',
-                   result['primaryIpAddress'] or formatting.blank()])
-    table.add_row(['private_ip',
-                   result['primaryBackendIpAddress'] or formatting.blank()])
+    table.add_row(['public_ip', result.get('primaryIpAddress', '-')])
+    table.add_row(['private_ip', result.get('primaryBackendIpAddress', '-')])
     table.add_row(['private_only', result['privateNetworkOnlyFlag']])
     table.add_row(['private_cpu', result['dedicatedAccountHostOnlyFlag']])
     table.add_row(['created', result['createDate']])
     table.add_row(['modified', result['modifyDate']])
-    if utils.lookup(result, 'billingItem') != []:
-        table.add_row(['owner', formatting.FormattedItem(
-            utils.lookup(result, 'billingItem', 'orderItem',
-                         'order', 'userRecord',
-                         'username') or formatting.blank(),
-        )])
-    else:
-        table.add_row(['owner', formatting.blank()])
 
-    vlan_table = formatting.Table(['type', 'number', 'id'])
-    for vlan in result['networkVlans']:
-        vlan_table.add_row([
-            vlan['networkSpace'], vlan['vlanNumber'], vlan['id']])
-    table.add_row(['vlans', vlan_table])
+    table.add_row(_get_owner_row(result))
+    table.add_row(_get_vlan_table(result))
 
-    if result.get('networkComponents'):
-        secgroup_table = formatting.Table(['interface', 'id', 'name'])
-        has_secgroups = False
-        for comp in result.get('networkComponents'):
-            interface = 'PRIVATE' if comp['port'] == 0 else 'PUBLIC'
-            for binding in comp['securityGroupBindings']:
-                has_secgroups = True
-                secgroup = binding['securityGroup']
-                secgroup_table.add_row([
-                    interface, secgroup['id'],
-                    secgroup.get('name') or formatting.blank()])
-        if has_secgroups:
-            table.add_row(['security_groups', secgroup_table])
+    bandwidth = vsi.get_bandwidth_allocation(vs_id)
+    table.add_row(['Bandwidth', _bw_table(bandwidth)])
+
+    security_table = _get_security_table(result)
+    if security_table is not None:
+        table.add_row(['security_groups', security_table])
 
-    if result.get('notes'):
-        table.add_row(['notes', result['notes']])
+    table.add_row(['notes', result.get('notes', '-')])
 
     if price:
         total_price = utils.lookup(result,
@@ -145,6 +122,20 @@ def cli(env, identifier, passwords=False, price=False):
     env.fout(table)
 
 
+def _bw_table(bw_data):
+    """Generates a bandwidth useage table"""
+    table = formatting.Table(['Type', 'In GB', 'Out GB', 'Allotment'])
+    for bw_point in bw_data.get('useage'):
+        bw_type = 'Private'
+        allotment = 'N/A'
+        if bw_point['type']['alias'] == 'PUBLIC_SERVER_BW':
+            bw_type = 'Public'
+            allotment = bw_data['allotment'].get('amount', '-')
+
+        table.add_row([bw_type, bw_point['amountIn'], bw_point['amountOut'], allotment])
+    return table
+
+
 def _cli_helper_dedicated_host(env, result, table):
     """Get details on dedicated host for a virtual server."""
 
@@ -160,3 +151,40 @@ def _cli_helper_dedicated_host(env, result, table):
             dedicated_host = {}
         table.add_row(['dedicated_host',
                        dedicated_host.get('name') or formatting.blank()])
+
+
+def _get_owner_row(result):
+    """Formats and resturns the Owner row"""
+
+    if utils.lookup(result, 'billingItem') != []:
+        owner = utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username')
+    else:
+        owner = formatting.blank()
+    return(['owner', owner])
+
+
+def _get_vlan_table(result):
+    """Formats and resturns a vlan table"""
+
+    vlan_table = formatting.Table(['type', 'number', 'id'])
+    for vlan in result['networkVlans']:
+        vlan_table.add_row([
+            vlan['networkSpace'], vlan['vlanNumber'], vlan['id']])
+    return ['vlans', vlan_table]
+
+
+def _get_security_table(result):
+    secgroup_table = formatting.Table(['interface', 'id', 'name'])
+    has_secgroups = False
+
+    if result.get('networkComponents'):
+        for comp in result.get('networkComponents'):
+            interface = 'PRIVATE' if comp['port'] == 0 else 'PUBLIC'
+            for binding in comp['securityGroupBindings']:
+                has_secgroups = True
+                secgroup = binding['securityGroup']
+                secgroup_table.add_row([interface, secgroup['id'], secgroup.get('name', '-')])
+    if has_secgroups:
+        return secgroup_table
+    else:
+        return None
diff --git a/SoftLayer/CLI/virt/usage.py b/SoftLayer/CLI/virt/usage.py
index ec970221..9936d941 100644
--- a/SoftLayer/CLI/virt/usage.py
+++ b/SoftLayer/CLI/virt/usage.py
@@ -17,7 +17,7 @@
 @click.option('--end_date', '-e', type=click.STRING, required=True, help="End Date e.g. 2019-4-2 (yyyy-MM-dd)")
 @click.option('--valid_type', '-t', type=click.STRING, required=True,
               help="Metric_Data_Type keyName e.g. CPU0, CPU1, MEMORY_USAGE, etc.")
-@click.option('--summary_period', '-p', type=click.INT, default=1800,
+@click.option('--summary_period', '-p', type=click.INT, default=3600,
               help="300, 600, 1800, 3600, 43200 or 86400 seconds")
 @environment.pass_env
 def cli(env, identifier, start_date, end_date, valid_type, summary_period):
diff --git a/SoftLayer/decoration.py b/SoftLayer/decoration.py
index 66ce62cc..a5e35d74 100644
--- a/SoftLayer/decoration.py
+++ b/SoftLayer/decoration.py
@@ -15,7 +15,6 @@
     exceptions.ServerError,
     exceptions.ApplicationError,
     exceptions.RemoteSystemError,
-    exceptions.TransportError
 )
 
 
diff --git a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
index 72838692..47e9a1bc 100644
--- a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
+++ b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py
@@ -118,3 +118,34 @@
         }
     }
 ]
+
+getBandwidthAllotmentDetail = {
+    'allocationId': 25465663,
+    'bandwidthAllotmentId': 138442,
+    'effectiveDate': '2019-04-03T23:00:00-06:00',
+    'endEffectiveDate': None,
+    'id': 25888247,
+    'serviceProviderId': 1,
+    'allocation': {
+        'amount': '250'
+    }
+}
+
+getBillingCycleBandwidthUsage = [
+    {
+        'amountIn': '.448',
+        'amountOut': '.52157',
+        'type': {
+            'alias': 'PUBLIC_SERVER_BW'
+        }
+    },
+    {
+        'amountIn': '.03842',
+        'amountOut': '.01822',
+        'type': {
+            'alias': 'PRIVATE_SERVER_BW'
+        }
+    }
+]
+
+getMetricTrackingObjectId = 1000
diff --git a/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py b/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py
index 6a0a031a..50cfb197 100644
--- a/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py
+++ b/SoftLayer/fixtures/SoftLayer_Metric_Tracking_Object.py
@@ -10,3 +10,48 @@
         "type": "cpu0"
     },
 ]
+
+
+# Using counter > 32bit int causes unit tests to fail.
+getBandwidthData = [
+    {
+        'counter': 37.21,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'cpu0'
+    },
+    {
+        'counter': 76.12,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'cpu1'
+    },
+    {
+        'counter': 257623973,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'memory'
+    },
+    {
+        'counter': 137118503,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'memory_usage'
+    },
+    {
+        'counter': 125888818,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'privateIn_net_octet'
+    },
+    {
+        'counter': 961037,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'privateOut_net_octet'
+    },
+    {
+        'counter': 1449885176,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'publicIn_net_octet'
+    },
+    {
+        'counter': 91803794,
+        'dateTime': '2019-05-20T23:00:00-06:00',
+        'type': 'publicOut_net_octet'
+    }
+]
diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
index 49433b01..aaae79b7 100644
--- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
+++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
@@ -628,3 +628,33 @@
 ]
 
 getMetricTrackingObjectId = 1000
+
+
+getBandwidthAllotmentDetail = {
+    'allocationId': 25465663,
+    'bandwidthAllotmentId': 138442,
+    'effectiveDate': '2019-04-03T23:00:00-06:00',
+    'endEffectiveDate': None,
+    'id': 25888247,
+    'serviceProviderId': 1,
+    'allocation': {
+        'amount': '250'
+    }
+}
+
+getBillingCycleBandwidthUsage = [
+    {
+        'amountIn': '.448',
+        'amountOut': '.52157',
+        'type': {
+            'alias': 'PUBLIC_SERVER_BW'
+        }
+    },
+    {
+        'amountIn': '.03842',
+        'amountOut': '.01822',
+        'type': {
+            'alias': 'PRIVATE_SERVER_BW'
+        }
+    }
+]
diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py
index 77c4e604..4a52a523 100644
--- a/SoftLayer/managers/hardware.py
+++ b/SoftLayer/managers/hardware.py
@@ -664,6 +664,40 @@ def wait_for_ready(self, instance_id, limit=14400, delay=10, pending=False):
         LOGGER.info("Waiting for %d expired.", instance_id)
         return False
 
+    def get_tracking_id(self, instance_id):
+        """Returns the Metric Tracking Object Id for a hardware server
+
+        :param int instance_id: Id of the hardware server
+        """
+        return self.hardware.getMetricTrackingObjectId(id=instance_id)
+
+    def get_bandwidth_data(self, instance_id, start_date=None, end_date=None, direction=None, rollup=3600):
+        """Gets bandwidth data for a server
+
+        Will get averaged bandwidth data for a given time period. If you use a rollup over 3600 be aware
+        that the API will bump your start/end date to align with how data is stored. For example if you
+        have a rollup of 86400 your start_date will be bumped to 00:00. If you are not using a time in the
+        start/end date fields, this won't really matter.
+
+        :param int instance_id: Hardware Id to get data for
+        :param date start_date: Date to start pulling data for.
+        :param date end_date: Date to finish pulling data for
+        :param string direction: Can be either 'public', 'private', or None for both.
+        :param int rollup: 300, 600, 1800, 3600, 43200 or 86400 seconds to average data over.
+        """
+        tracking_id = self.get_tracking_id(instance_id)
+        data = self.client.call('Metric_Tracking_Object', 'getBandwidthData', start_date, end_date, direction,
+                                rollup, id=tracking_id, iter=True)
+        return data
+
+    def get_bandwidth_allocation(self, instance_id):
+        """Combines getBandwidthAllotmentDetail() and getBillingCycleBandwidthUsage() """
+        a_mask = "mask[allocation[amount]]"
+        allotment = self.client.call('Hardware_Server', 'getBandwidthAllotmentDetail', id=instance_id, mask=a_mask)
+        u_mask = "mask[amountIn,amountOut,type]"
+        useage = self.client.call('Hardware_Server', 'getBillingCycleBandwidthUsage', id=instance_id, mask=u_mask)
+        return {'allotment': allotment['allocation'], 'useage': useage}
+
 
 def _get_extra_price_id(items, key_name, hourly, location):
     """Returns a price id attached to item with the given key_name."""
diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py
index 3fa2ac1d..f0d75dc3 100644
--- a/SoftLayer/managers/ssl.py
+++ b/SoftLayer/managers/ssl.py
@@ -61,7 +61,7 @@ def add_certificate(self, certificate):
 
         :param dict certificate: A dictionary representing the parts of the
                                  certificate.
-                                 See sldn.softlayer.com for more info.
+                                 See developer.softlayer.com for more info.
 
         Example::
 
diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py
index 073b8aea..03ac6d03 100644
--- a/SoftLayer/managers/vs.py
+++ b/SoftLayer/managers/vs.py
@@ -1018,11 +1018,45 @@ def get_summary_data_usage(self, instance_id, start_date=None, end_date=None, va
             }
         ]
 
-        metric_tracking_id = self.guest.getMetricTrackingObjectId(id=instance_id)
+        metric_tracking_id = self.get_tracking_id(instance_id)
 
         return self.client.call('Metric_Tracking_Object', 'getSummaryData', start_date, end_date, valid_types,
                                 summary_period, id=metric_tracking_id, iter=True)
 
+    def get_tracking_id(self, instance_id):
+        """Returns the Metric Tracking Object Id for a hardware server
+
+        :param int instance_id: Id of the hardware server
+        """
+        return self.guest.getMetricTrackingObjectId(id=instance_id)
+
+    def get_bandwidth_data(self, instance_id, start_date=None, end_date=None, direction=None, rollup=3600):
+        """Gets bandwidth data for a server
+
+        Will get averaged bandwidth data for a given time period. If you use a rollup over 3600 be aware
+        that the API will bump your start/end date to align with how data is stored. For example if you
+        have a rollup of 86400 your start_date will be bumped to 00:00. If you are not using a time in the
+        start/end date fields, this won't really matter.
+
+        :param int instance_id: Hardware Id to get data for
+        :param date start_date: Date to start pulling data for.
+        :param date end_date: Date to finish pulling data for
+        :param string direction: Can be either 'public', 'private', or None for both.
+        :param int rollup: 300, 600, 1800, 3600, 43200 or 86400 seconds to average data over.
+        """
+        tracking_id = self.get_tracking_id(instance_id)
+        data = self.client.call('Metric_Tracking_Object', 'getBandwidthData', start_date, end_date, direction,
+                                rollup, id=tracking_id, iter=True)
+        return data
+
+    def get_bandwidth_allocation(self, instance_id):
+        """Combines getBandwidthAllotmentDetail() and getBillingCycleBandwidthUsage() """
+        a_mask = "mask[allocation[amount]]"
+        allotment = self.client.call('Virtual_Guest', 'getBandwidthAllotmentDetail', id=instance_id, mask=a_mask)
+        u_mask = "mask[amountIn,amountOut,type]"
+        useage = self.client.call('Virtual_Guest', 'getBillingCycleBandwidthUsage', id=instance_id, mask=u_mask)
+        return {'allotment': allotment['allocation'], 'useage': useage}
+
     # pylint: disable=inconsistent-return-statements
     def _get_price_id_for_upgrade(self, package_items, option, value, public=True):
         """Find the price id for the option and value to upgrade.
diff --git a/docs/api/client.rst b/docs/api/client.rst
index c798ac71..9fc13c1e 100644
--- a/docs/api/client.rst
+++ b/docs/api/client.rst
@@ -8,7 +8,7 @@ and executing XML-RPC calls against the SoftLayer API. Below are some links
 that will help to use the SoftLayer API.
 
 
-* `SoftLayer API Documentation <https://softlayer.github.io/reference/softlayerapi/>`_
+* `SoftLayer API Documentation <http://developer.softlayer.com/reference/softlayerapi>`_
 * `Source on GitHub <https://github.com/softlayer/softlayer-python>`_
 
 ::
diff --git a/docs/cli.rst b/docs/cli.rst
index ebd62741..307592fb 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -176,3 +176,74 @@ Most commands will take in additional options/arguments. To see all available ac
 	                                  separated tags
 	  --help                          Show this message and exit.
 
+
+
+Debugging
+=========
+To see exactly what API call is being made by the SLCLI, you can use the verbose option. 
+
+A single `-v` will show a simple version of the API call, along with some statistics
+
+::
+
+    slcli -v vs detail 74397127
+    Calling: SoftLayer_Virtual_Guest::getObject(id=74397127, mask='id,globalIdentifier,fullyQualifiedDomainName,hostname,domain', filter='None', args=(), limit=None, offset=None))
+    Calling: SoftLayer_Virtual_Guest::getReverseDomainRecords(id=77460683, mask='', filter='None', args=(), limit=None, offset=None))
+    :..................:..............................................................:
+    :       name       :                            value                             :
+    :..................:..............................................................:
+    :  execution_time  :                          2.020334s                           :
+    :    api_calls     :        SoftLayer_Virtual_Guest::getObject (1.515583s)        :
+    :                  : SoftLayer_Virtual_Guest::getReverseDomainRecords (0.494480s) :
+    :     version      :                   softlayer-python/v5.7.2                    :
+    :  python_version  :           3.7.3 (default, Mar 27 2019, 09:23:15)             :
+    :                  :              [Clang 10.0.1 (clang-1001.0.46.3)]              :
+    : library_location : /Users/chris/Code/py3/lib/python3.7/site-packages/SoftLayer  :
+    :..................:..............................................................:
+
+
+Using `-vv` will print out some API call details in the summary as well.
+
+::
+
+    slcli -vv account summary
+    Calling: SoftLayer_Account::getObject(id=None, mask='mask[ nextInvoiceTotalAmount, pendingInvoice[invoiceTotalAmount], blockDeviceTemplateGroupCount, dedicatedHostCount, domainCount, hardwareCount, networkStorageCount, openTicketCount, networkVlanCount, subnetCount, userCount, virtualGuestCount ]', filter='None', args=(), limit=None, offset=None))
+    :..................:.............................................................:
+    :       name       :                            value                            :
+    :..................:.............................................................:
+    :  execution_time  :                          0.921271s                          :
+    :    api_calls     :           SoftLayer_Account::getObject (0.911208s)          :
+    :     version      :                   softlayer-python/v5.7.2                   :
+    :  python_version  :           3.7.3 (default, Mar 27 2019, 09:23:15)            :
+    :                  :              [Clang 10.0.1 (clang-1001.0.46.3)]             :
+    : library_location : /Users/chris/Code/py3/lib/python3.7/site-packages/SoftLayer :
+    :..................:.............................................................:
+    :........:.................................................:
+    :        :           SoftLayer_Account::getObject          :
+    :........:.................................................:
+    :   id   :                       None                      :
+    :  mask  :                      mask[                      :
+    :        :                   nextInvoiceTotalAmount,       :
+    :        :             pendingInvoice[invoiceTotalAmount], :
+    :        :                blockDeviceTemplateGroupCount,   :
+    :        :                     dedicatedHostCount,         :
+    :        :                         domainCount,            :
+    :        :                        hardwareCount,           :
+    :        :                     networkStorageCount,        :
+    :        :                       openTicketCount,          :
+    :        :                      networkVlanCount,          :
+    :        :                         subnetCount,            :
+    :        :                          userCount,             :
+    :        :                      virtualGuestCount          :
+    :        :                              ]                  :
+    : filter :                       None                      :
+    : limit  :                       None                      :
+    : offset :                       None                      :
+    :........:.................................................:
+
+Using `-vvv` will print out the exact API that can be used without the softlayer-python framework, A simple python code snippet for XML-RPC, a curl call for REST API calls. This is dependant on the endpoint you are using in the config file.
+
+::
+
+    slcli -vvv account summary
+    curl -u $SL_USER:$SL_APIKEY -X GET -H "Accept: */*" -H "Accept-Encoding: gzip, deflate, compress"  'https://api.softlayer.com/rest/v3.1/SoftLayer_Account/getObject.json?objectMask=mask%5B%0A++++++++++++nextInvoiceTotalAmount%2C%0A++++++++++++pendingInvoice%5BinvoiceTotalAmount%5D%2C%0A++++++++++++blockDeviceTemplateGroupCount%2C%0A++++++++++++dedicatedHostCount%2C%0A++++++++++++domainCount%2C%0A++++++++++++hardwareCount%2C%0A++++++++++++networkStorageCount%2C%0A++++++++++++openTicketCount%2C%0A++++++++++++networkVlanCount%2C%0A++++++++++++subnetCount%2C%0A++++++++++++userCount%2C%0A++++++++++++virtualGuestCount%0A++++++++++++%5D'
diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst
index f6bb7e48..3e7eeaf4 100644
--- a/docs/cli/hardware.rst
+++ b/docs/cli/hardware.rst
@@ -4,6 +4,10 @@ Interacting with Hardware
 ==============================
 
 
+.. click:: SoftLayer.CLI.hardware.bandwidth:cli
+   :prog: hw bandwidth
+   :show-nested:
+
 .. click:: SoftLayer.CLI.hardware.cancel_reasons:cli
    :prog: hw cancel-reasons
    :show-nested:
diff --git a/docs/cli/reports.rst b/docs/cli/reports.rst
new file mode 100644
index 00000000..f62de588
--- /dev/null
+++ b/docs/cli/reports.rst
@@ -0,0 +1,17 @@
+.. _cli_reports:
+
+Reports
+=======
+
+There are a few report type commands in the SLCLI.
+
+.. click:: SoftLayer.CLI.summary:cli
+   :prog: summary
+   :show-nested:
+
+A list of datacenters, and how many servers, VSI, vlans, subnets and public_ips are in each.
+
+
+.. click:: SoftLayer.CLI.report.bandwidth:cli
+   :prog: report bandwidth
+   :show-nested:
\ No newline at end of file
diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst
index 2276bd7e..f855238d 100644
--- a/docs/cli/vs.rst
+++ b/docs/cli/vs.rst
@@ -170,39 +170,81 @@ username is 'root' and password is 'ABCDEFGH'.
 	:..............:...........................:
 
 
-There are many other commands to help manage virtual servers. To see them all,
-use `slcli help vs`.
 
-::
+.. click:: SoftLayer.CLI.virt.bandwidth:cli
+   :prog: vs bandwidth
+   :show-nested:
+
+If no timezone is specified, IMS local time (CST) will be assumed, which might not match your user's selected timezone.
+
+
+.. click:: SoftLayer.CLI.virt.cancel:cli
+   :prog: vs cancel
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.capture:cli
+   :prog: vs capture
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.create:cli
+   :prog: vs create
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.create_options:cli
+   :prog: vs create-options
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.dns:cli
+   :prog: vs dns-sync
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.edit:cli
+   :prog: vs edit
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.list:cli
+   :prog: vs list
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.power:pause
+   :prog: vs pause
+   :show-nested:
+
+
+.. click:: SoftLayer.CLI.virt.power:power_on
+   :prog: vs power-on
+   :show-nested:
+
+
+.. click:: SoftLayer.CLI.virt.power:power_off
+   :prog: vs power-off
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.power:resume
+   :prog: vs resume
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.power:rescue
+   :prog: vs rescue
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.power:reboot
+   :prog: vs reboot
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.ready:cli
+   :prog: vs ready
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.upgrade:cli
+   :prog: vs upgrade
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.usage:cli
+   :prog: vs usage
+   :show-nested:
+
 
-	$ slcli vs
-	Usage: slcli vs [OPTIONS] COMMAND [ARGS]...
-
-	  Virtual Servers.
-
-	Options:
-	  --help  Show this message and exit.
-
-	Commands:
-	  cancel          Cancel virtual servers.
-	  capture         Capture SoftLayer image.
-	  create          Order/create virtual servers.
-	  create-options  Virtual server order options.
-	  credentials     List virtual server credentials.
-	  detail          Get details for a virtual server.
-	  dns-sync        Sync DNS records.
-	  edit            Edit a virtual server's details.
-	  list            List virtual servers.
-	  network         Manage network settings.
-	  pause           Pauses an active virtual server.
-	  power_off       Power off an active virtual server.
-	  power_on        Power on a virtual server.
-	  ready           Check if a virtual server is ready.
-	  reboot          Reboot an active virtual server.
-	  reload          Reload operating system on a virtual server.
-	  rescue          Reboot into a rescue image.
-	  resume          Resumes a paused virtual server.
-	  upgrade         Upgrade a virtual server.
 
 
 Reserved Capacity
diff --git a/tox.ini b/tox.ini
index e5c7c2f6..ff08bac1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py35,py36,py37,pypy,analysis,coverage
+envlist = py35,py36,py37,pypy,analysis,coverage
 
 
 [flake8]


Test patch:
diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py
index 75e2cd78..f14118bc 100644
--- a/tests/CLI/modules/server_tests.py
+++ b/tests/CLI/modules/server_tests.py
@@ -96,37 +96,17 @@ def test_server_credentials_exception_password_not_found(self):
         )
 
     def test_server_details(self):
-        result = self.run_command(['server', 'detail', '1234',
-                                   '--passwords', '--price'])
-        expected = {
-            'cores': 2,
-            'created': '2013-08-01 15:23:45',
-            'datacenter': 'TEST00',
-            'guid': '1a2b3c-1701',
-            'domain': 'test.sftlyr.ws',
-            'hostname': 'hardware-test1',
-            'fqdn': 'hardware-test1.test.sftlyr.ws',
-            'id': 1000,
-            'ipmi_ip': '10.1.0.3',
-            'memory': 2048,
-            'notes': 'These are test notes.',
-            'os': 'Ubuntu',
-            'os_version': 'Ubuntu 12.04 LTS',
-            'owner': 'chechu',
-            'prices': [{'Item': 'Total', 'Recurring Price': 16.08},
-                       {'Item': 'test', 'Recurring Price': 1}],
-            'private_ip': '10.1.0.2',
-            'public_ip': '172.16.1.100',
-            'remote users': [{'password': 'abc123', 'ipmi_username': 'root'}],
-            'status': 'ACTIVE',
-            'tags': ['test_tag'],
-            'users': [{'password': 'abc123', 'username': 'root'}],
-            'vlans': [{'id': 9653, 'number': 1800, 'type': 'PRIVATE'},
-                      {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}]
-        }
+        result = self.run_command(['server', 'detail', '1234', '--passwords', '--price'])
 
         self.assert_no_fail(result)
-        self.assertEqual(expected, json.loads(result.output))
+        output = json.loads(result.output)
+        self.assertEqual(output['notes'], 'These are test notes.')
+        self.assertEqual(output['prices'][0]['Recurring Price'], 16.08)
+        self.assertEqual(output['remote users'][0]['password'], 'abc123')
+        self.assertEqual(output['users'][0]['username'], 'root')
+        self.assertEqual(output['vlans'][0]['number'], 1800)
+        self.assertEqual(output['owner'], 'chechu')
+        self.assertEqual(output['Bandwidth'][0]['Allotment'], '250')
 
     def test_detail_vs_empty_tag(self):
         mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
@@ -603,3 +583,47 @@ def test_toggle_ipmi_off(self):
         result = self.run_command(['server', 'toggle-ipmi', '--disable', '12345'])
         self.assert_no_fail(result)
         self.assertEqual(result.output, 'True\n')
+
+    def test_bandwidth_hw(self):
+        if sys.version_info < (3, 6):
+            self.skipTest("Test requires python 3.6+")
+        result = self.run_command(['server', 'bandwidth', '100', '--start_date=2019-01-01', '--end_date=2019-02-01'])
+        self.assert_no_fail(result)
+
+        date = '2019-05-20 23:00'
+        # number of characters from the end of output to break so json can parse properly
+        pivot = 157
+        # only pyhon 3.7 supports the timezone format slapi uses
+        if sys.version_info < (3, 7):
+            date = '2019-05-20T23:00:00-06:00'
+            pivot = 166
+        # Since this is 2 tables, it gets returned as invalid json like "[{}][{}]"" instead of "[[{}],[{}]]"
+        # so we just do some hacky string substitution to pull out the respective arrays that can be jsonifyied
+
+        output_summary = json.loads(result.output[0:-pivot])
+        output_list = json.loads(result.output[-pivot:])
+
+        self.assertEqual(output_summary[0]['Average MBps'], 0.3841)
+        self.assertEqual(output_summary[1]['Max Date'], date)
+        self.assertEqual(output_summary[2]['Max GB'], 0.1172)
+        self.assertEqual(output_summary[3]['Sum GB'], 0.0009)
+
+        self.assertEqual(output_list[0]['Date'], date)
+        self.assertEqual(output_list[0]['Pub In'], 1.3503)
+
+    def test_bandwidth_hw_quite(self):
+        result = self.run_command(['server', 'bandwidth', '100', '--start_date=2019-01-01',
+                                   '--end_date=2019-02-01', '-q'])
+        self.assert_no_fail(result)
+        date = '2019-05-20 23:00'
+
+        # only pyhon 3.7 supports the timezone format slapi uses
+        if sys.version_info < (3, 7):
+            date = '2019-05-20T23:00:00-06:00'
+
+        output_summary = json.loads(result.output)
+
+        self.assertEqual(output_summary[0]['Average MBps'], 0.3841)
+        self.assertEqual(output_summary[1]['Max Date'], date)
+        self.assertEqual(output_summary[2]['Max GB'], 0.1172)
+        self.assertEqual(output_summary[3]['Sum GB'], 0.0009)
diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py
index 7b03bb08..16016d45 100644
--- a/tests/CLI/modules/vs/vs_tests.py
+++ b/tests/CLI/modules/vs/vs_tests.py
@@ -5,10 +5,12 @@
     :license: MIT, see LICENSE for more details.
 """
 import json
+import sys
 
 import mock
 
 from SoftLayer.CLI import exceptions
+from SoftLayer.fixtures import SoftLayer_Virtual_Guest as SoftLayer_Virtual_Guest
 from SoftLayer import SoftLayerAPIError
 from SoftLayer import testing
 
@@ -168,78 +170,20 @@ def mock_lookup_func(dic, key, *keys):
         result = self.run_command(['vs', 'detail', '100', '--passwords', '--price'])
 
         self.assert_no_fail(result)
-        self.assertEqual(json.loads(result.output),
-                         {'active_transaction': None,
-                          'cores': 2,
-                          'created': '2013-08-01 15:23:45',
-                          'datacenter': 'TEST00',
-                          'dedicated_host': 'test-dedicated',
-                          'dedicated_host_id': 37401,
-                          'hostname': 'vs-test1',
-                          'domain': 'test.sftlyr.ws',
-                          'fqdn': 'vs-test1.test.sftlyr.ws',
-                          'id': 100,
-                          'guid': '1a2b3c-1701',
-                          'memory': 1024,
-                          'modified': {},
-                          'os': 'Ubuntu',
-                          'os_version': '12.04-64 Minimal for VSI',
-                          'notes': 'notes',
-                          'price_rate': 0,
-                          'tags': ['production'],
-                          'private_cpu': {},
-                          'private_ip': '10.45.19.37',
-                          'private_only': {},
-                          'ptr': 'test.softlayer.com.',
-                          'public_ip': '172.16.240.2',
-                          'state': 'RUNNING',
-                          'status': 'ACTIVE',
-                          'users': [{'software': 'Ubuntu',
-                                     'password': 'pass',
-                                     'username': 'user'}],
-                          'vlans': [{'type': 'PUBLIC',
-                                     'number': 23,
-                                     'id': 1}],
-                          'owner': None})
+        output = json.loads(result.output)
+        self.assertEqual(output['owner'], None)
 
     def test_detail_vs(self):
-        result = self.run_command(['vs', 'detail', '100',
-                                   '--passwords', '--price'])
+        result = self.run_command(['vs', 'detail', '100', '--passwords', '--price'])
 
         self.assert_no_fail(result)
-        self.assertEqual(json.loads(result.output),
-                         {'active_transaction': None,
-                          'cores': 2,
-                          'created': '2013-08-01 15:23:45',
-                          'datacenter': 'TEST00',
-                          'dedicated_host': 'test-dedicated',
-                          'dedicated_host_id': 37401,
-                          'hostname': 'vs-test1',
-                          'domain': 'test.sftlyr.ws',
-                          'fqdn': 'vs-test1.test.sftlyr.ws',
-                          'id': 100,
-                          'guid': '1a2b3c-1701',
-                          'memory': 1024,
-                          'modified': {},
-                          'os': 'Ubuntu',
-                          'os_version': '12.04-64 Minimal for VSI',
-                          'notes': 'notes',
-                          'price_rate': 6.54,
-                          'tags': ['production'],
-                          'private_cpu': {},
-                          'private_ip': '10.45.19.37',
-                          'private_only': {},
-                          'ptr': 'test.softlayer.com.',
-                          'public_ip': '172.16.240.2',
-                          'state': 'RUNNING',
-                          'status': 'ACTIVE',
-                          'users': [{'software': 'Ubuntu',
-                                     'password': 'pass',
-                                     'username': 'user'}],
-                          'vlans': [{'type': 'PUBLIC',
-                                     'number': 23,
-                                     'id': 1}],
-                          'owner': 'chechu'})
+        output = json.loads(result.output)
+        self.assertEqual(output['notes'], 'notes')
+        self.assertEqual(output['price_rate'], 6.54)
+        self.assertEqual(output['users'][0]['username'], 'user')
+        self.assertEqual(output['vlans'][0]['number'], 23)
+        self.assertEqual(output['owner'], 'chechu')
+        self.assertEqual(output['Bandwidth'][0]['Allotment'], '250')
 
     def test_detail_vs_empty_tag(self):
         mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject')
@@ -278,6 +222,48 @@ def test_detail_vs_no_dedicated_host_hostname(self):
         self.assertEqual(json.loads(result.output)['dedicated_host_id'], 37401)
         self.assertIsNone(json.loads(result.output)['dedicated_host'])
 
+    def test_detail_vs_security_group(self):
+        vg_return = SoftLayer_Virtual_Guest.getObject
+        sec_group = [
+            {
+                'id': 35386715,
+                'name': 'eth',
+                'port': 0,
+                'speed': 100,
+                'status': 'ACTIVE',
+                'primaryIpAddress': '10.175.106.149',
+                'securityGroupBindings': [
+                    {
+                        'id': 1620971,
+                        'networkComponentId': 35386715,
+                        'securityGroupId': 128321,
+                        'securityGroup': {
+                            'id': 128321,
+                            'name': 'allow_all'
+                        }
+                    }
+                ]
+            }
+        ]
+
+        vg_return['networkComponents'] = sec_group
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getObject')
+        mock.return_value = vg_return
+        result = self.run_command(['vs', 'detail', '100'])
+        self.assert_no_fail(result)
+        output = json.loads(result.output)
+        self.assertEqual(output['security_groups'][0]['id'], 128321)
+        self.assertEqual(output['security_groups'][0]['name'], 'allow_all')
+        self.assertEqual(output['security_groups'][0]['interface'], 'PRIVATE')
+
+    def test_detail_vs_ptr_error(self):
+        mock = self.set_mock('SoftLayer_Virtual_Guest', 'getReverseDomainRecords')
+        mock.side_effect = SoftLayerAPIError("SoftLayer_Exception", "Not Found")
+        result = self.run_command(['vs', 'detail', '100'])
+        self.assert_no_fail(result)
+        output = json.loads(result.output)
+        self.assertEqual(output.get('ptr', None), None)
+
     def test_create_options(self):
         result = self.run_command(['vs', 'create-options'])
 
@@ -696,3 +682,48 @@ def test_usage_metric_data_empty(self):
              '--summary_period=300'])
         self.assertEqual(result.exit_code, 2)
         self.assertIsInstance(result.exception, exceptions.CLIAbort)
+
+    def test_bandwidth_vs(self):
+        if sys.version_info < (3, 6):
+            self.skipTest("Test requires python 3.6+")
+
+        result = self.run_command(['vs', 'bandwidth', '100', '--start_date=2019-01-01', '--end_date=2019-02-01'])
+        self.assert_no_fail(result)
+
+        date = '2019-05-20 23:00'
+        # number of characters from the end of output to break so json can parse properly
+        pivot = 157
+        # only pyhon 3.7 supports the timezone format slapi uses
+        if sys.version_info < (3, 7):
+            date = '2019-05-20T23:00:00-06:00'
+            pivot = 166
+        # Since this is 2 tables, it gets returned as invalid json like "[{}][{}]"" instead of "[[{}],[{}]]"
+        # so we just do some hacky string substitution to pull out the respective arrays that can be jsonifyied
+
+        output_summary = json.loads(result.output[0:-pivot])
+        output_list = json.loads(result.output[-pivot:])
+
+        self.assertEqual(output_summary[0]['Average MBps'], 0.3841)
+        self.assertEqual(output_summary[1]['Max Date'], date)
+        self.assertEqual(output_summary[2]['Max GB'], 0.1172)
+        self.assertEqual(output_summary[3]['Sum GB'], 0.0009)
+
+        self.assertEqual(output_list[0]['Date'], date)
+        self.assertEqual(output_list[0]['Pub In'], 1.3503)
+
+    def test_bandwidth_vs_quite(self):
+        result = self.run_command(['vs', 'bandwidth', '100', '--start_date=2019-01-01', '--end_date=2019-02-01', '-q'])
+        self.assert_no_fail(result)
+
+        date = '2019-05-20 23:00'
+
+        # only pyhon 3.7 supports the timezone format slapi uses
+        if sys.version_info < (3, 7):
+            date = '2019-05-20T23:00:00-06:00'
+
+        output_summary = json.loads(result.output)
+
+        self.assertEqual(output_summary[0]['Average MBps'], 0.3841)
+        self.assertEqual(output_summary[1]['Max Date'], date)
+        self.assertEqual(output_summary[2]['Max GB'], 0.1172)
+        self.assertEqual(output_summary[3]['Sum GB'], 0.0009)
diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py
index 030d808d..461094be 100644
--- a/tests/managers/hardware_tests.py
+++ b/tests/managers/hardware_tests.py
@@ -10,6 +10,7 @@
 
 
 import SoftLayer
+
 from SoftLayer import fixtures
 from SoftLayer import managers
 from SoftLayer import testing
@@ -320,6 +321,14 @@ def test_cancel_hardware_monthly_whenever(self):
         self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem',
                                 identifier=6327, args=(False, False, 'No longer needed', ''))
 
+    def test_cancel_running_transaction(self):
+        mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject')
+        mock.return_value = {'id': 987, 'billingItem': {'id': 6327},
+                             'activeTransaction': {'id': 4567}}
+        self.assertRaises(SoftLayer.SoftLayerError,
+                          self.hardware.cancel_hardware,
+                          12345)
+
     def test_change_port_speed_public(self):
         self.hardware.change_port_speed(2, True, 100)
 
@@ -410,14 +419,104 @@ def test_reflash_firmware_selective(self):
                                 'createFirmwareReflashTransaction',
                                 identifier=100, args=(1, 0, 0))
 
+    def test_get_tracking_id(self):
+        result = self.hardware.get_tracking_id(1234)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getMetricTrackingObjectId')
+        self.assertEqual(result, 1000)
+
+    def test_get_bandwidth_data(self):
+        result = self.hardware.get_bandwidth_data(1234, '2019-01-01', '2019-02-01', 'public', 1000)
+        self.assert_called_with('SoftLayer_Metric_Tracking_Object',
+                                'getBandwidthData',
+                                args=('2019-01-01', '2019-02-01', 'public', 1000),
+                                identifier=1000)
+        self.assertEqual(result[0]['type'], 'cpu0')
+
+    def test_get_bandwidth_allocation(self):
+        result = self.hardware.get_bandwidth_allocation(1234)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getBandwidthAllotmentDetail', identifier=1234)
+        self.assert_called_with('SoftLayer_Hardware_Server', 'getBillingCycleBandwidthUsage', identifier=1234)
+        self.assertEqual(result['allotment']['amount'], '250')
+        self.assertEqual(result['useage'][0]['amountIn'], '.448')
+
 
 class HardwareHelperTests(testing.TestCase):
     def test_get_extra_price_id_no_items(self):
         ex = self.assertRaises(SoftLayer.SoftLayerError,
                                managers.hardware._get_extra_price_id,
                                [], 'test', True, None)
-        self.assertEqual("Could not find valid price for extra option, 'test'",
-                         str(ex))
+        self.assertEqual("Could not find valid price for extra option, 'test'", str(ex))
+
+    def test_get_extra_price_mismatched(self):
+        items = [
+            {'keyName': 'TEST', 'prices': [{'id': 1, 'locationGroupId': None, 'recurringFee': 99}]},
+            {'keyName': 'TEST', 'prices': [{'id': 2, 'locationGroupId': 55, 'hourlyRecurringFee': 99}]},
+            {'keyName': 'TEST', 'prices': [{'id': 3, 'locationGroupId': None, 'hourlyRecurringFee': 99}]},
+        ]
+        location = {
+            'location': {
+                'location': {
+                    'priceGroups': [
+                        {'id': 50},
+                        {'id': 51}
+                    ]
+                }
+            }
+        }
+        result = managers.hardware._get_extra_price_id(items, 'TEST', True, location)
+        self.assertEqual(3, result)
+
+    def test_get_bandwidth_price_mismatched(self):
+        items = [
+            {'itemCategory': {'categoryCode': 'bandwidth'},
+             'capacity': 100,
+             'prices': [{'id': 1, 'locationGroupId': None, 'hourlyRecurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'bandwidth'},
+             'capacity': 100,
+             'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'bandwidth'},
+             'capacity': 100,
+             'prices': [{'id': 3, 'locationGroupId': None, 'recurringFee': 99}]
+             },
+        ]
+        location = {
+            'location': {
+                'location': {
+                    'priceGroups': [
+                        {'id': 50},
+                        {'id': 51}
+                    ]
+                }
+            }
+        }
+        result = managers.hardware._get_bandwidth_price_id(items, False, False, location)
+        self.assertEqual(3, result)
+
+    def test_get_os_price_mismatched(self):
+        items = [
+            {'itemCategory': {'categoryCode': 'os'},
+             'softwareDescription': {'referenceCode': 'TEST_OS'},
+             'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'os'},
+             'softwareDescription': {'referenceCode': 'TEST_OS'},
+             'prices': [{'id': 3, 'locationGroupId': None, 'recurringFee': 99}]
+             },
+        ]
+        location = {
+            'location': {
+                'location': {
+                    'priceGroups': [
+                        {'id': 50},
+                        {'id': 51}
+                    ]
+                }
+            }
+        }
+        result = managers.hardware._get_os_price_id(items, 'TEST_OS', location)
+        self.assertEqual(3, result)
 
     def test_get_default_price_id_item_not_first(self):
         items = [{
@@ -432,33 +531,84 @@ def test_get_default_price_id_item_not_first(self):
         ex = self.assertRaises(SoftLayer.SoftLayerError,
                                managers.hardware._get_default_price_id,
                                items, 'unknown', True, None)
-        self.assertEqual("Could not find valid price for 'unknown' option",
-                         str(ex))
+        self.assertEqual("Could not find valid price for 'unknown' option", str(ex))
 
     def test_get_default_price_id_no_items(self):
         ex = self.assertRaises(SoftLayer.SoftLayerError,
                                managers.hardware._get_default_price_id,
                                [], 'test', True, None)
-        self.assertEqual("Could not find valid price for 'test' option",
-                         str(ex))
+        self.assertEqual("Could not find valid price for 'test' option", str(ex))
 
     def test_get_bandwidth_price_id_no_items(self):
         ex = self.assertRaises(SoftLayer.SoftLayerError,
                                managers.hardware._get_bandwidth_price_id,
                                [], hourly=True, no_public=False)
-        self.assertEqual("Could not find valid price for bandwidth option",
-                         str(ex))
+        self.assertEqual("Could not find valid price for bandwidth option", str(ex))
 
     def test_get_os_price_id_no_items(self):
         ex = self.assertRaises(SoftLayer.SoftLayerError,
                                managers.hardware._get_os_price_id,
                                [], 'UBUNTU_14_64', None)
-        self.assertEqual("Could not find valid price for os: 'UBUNTU_14_64'",
-                         str(ex))
+        self.assertEqual("Could not find valid price for os: 'UBUNTU_14_64'", str(ex))
 
     def test_get_port_speed_price_id_no_items(self):
         ex = self.assertRaises(SoftLayer.SoftLayerError,
                                managers.hardware._get_port_speed_price_id,
                                [], 10, True, None)
-        self.assertEqual("Could not find valid price for port speed: '10'",
-                         str(ex))
+        self.assertEqual("Could not find valid price for port speed: '10'", str(ex))
+
+    def test_get_port_speed_price_id_mismatch(self):
+        items = [
+            {'itemCategory': {'categoryCode': 'port_speed'},
+             'capacity': 101,
+             'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}],
+             'prices': [{'id': 1, 'locationGroupId': None, 'recurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'port_speed'},
+             'capacity': 100,
+             'attributes': [{'attributeTypeKeyName': 'IS_NOT_PRIVATE_NETWORK_ONLY'}],
+             'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'port_speed'},
+             'capacity': 100,
+             'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}, {'attributeTypeKeyName': 'NON_LACP'}],
+             'prices': [{'id': 3, 'locationGroupId': 55, 'recurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'port_speed'},
+             'capacity': 100,
+             'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}],
+             'prices': [{'id': 4, 'locationGroupId': 12, 'recurringFee': 99}]
+             },
+            {'itemCategory': {'categoryCode': 'port_speed'},
+             'capacity': 100,
+             'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}],
+             'prices': [{'id': 5, 'locationGroupId': None, 'recurringFee': 99}]
+             },
+        ]
+        location = {
+            'location': {
+                'location': {
+                    'priceGroups': [
+                        {'id': 50},
+                        {'id': 51}
+                    ]
+                }
+            }
+        }
+        result = managers.hardware._get_port_speed_price_id(items, 100, True, location)
+        self.assertEqual(5, result)
+
+    def test_matches_location(self):
+        price = {'id': 1, 'locationGroupId': 51, 'recurringFee': 99}
+        location = {
+            'location': {
+                'location': {
+                    'priceGroups': [
+                        {'id': 50},
+                        {'id': 51}
+                    ]
+                }
+            }
+        }
+        result = managers.hardware._matches_location(price, location)
+        self.assertTrue(result)
diff --git a/tests/managers/vs/vs_order_tests.py b/tests/managers/vs/vs_order_tests.py
index 12750224..7b54f545 100644
--- a/tests/managers/vs/vs_order_tests.py
+++ b/tests/managers/vs/vs_order_tests.py
@@ -9,6 +9,7 @@
 import mock
 
 import SoftLayer
+from SoftLayer import exceptions
 from SoftLayer import fixtures
 from SoftLayer import testing
 
@@ -45,16 +46,11 @@ def test_upgrade_blank(self):
         result = self.vs.upgrade(1)
 
         self.assertEqual(result, False)
-        self.assertEqual(self.calls('SoftLayer_Product_Order', 'placeOrder'),
-                         [])
+        self.assertEqual(self.calls('SoftLayer_Product_Order', 'placeOrder'), [])
 
     def test_upgrade_full(self):
         # Testing all parameters Upgrade
-        result = self.vs.upgrade(1,
-                                 cpus=4,
-                                 memory=2,
-                                 nic_speed=1000,
-                                 public=True)
+        result = self.vs.upgrade(1, cpus=4, memory=2, nic_speed=1000, public=True)
 
         self.assertEqual(result, True)
         self.assert_called_with('SoftLayer_Product_Order', 'placeOrder')
@@ -67,10 +63,7 @@ def test_upgrade_full(self):
 
     def test_upgrade_with_flavor(self):
         # Testing Upgrade with parameter preset
-        result = self.vs.upgrade(1,
-                                 preset="M1_64X512X100",
-                                 nic_speed=1000,
-                                 public=True)
+        result = self.vs.upgrade(1, preset="M1_64X512X100", nic_speed=1000, public=True)
 
         self.assertEqual(result, True)
         self.assert_called_with('SoftLayer_Product_Order', 'placeOrder')
@@ -141,6 +134,42 @@ def test_get_price_id_for_upgrade_finds_memory_price(self):
                                                      value='1000')
         self.assertEqual(1122, price_id)
 
+    def test__get_price_id_for_upgrade_find_private_price(self):
+        package_items = self.vs._get_package_items()
+        price_id = self.vs._get_price_id_for_upgrade(package_items=package_items,
+                                                     option='cpus',
+                                                     value='4',
+                                                     public=False)
+        self.assertEqual(1007, price_id)
+
+    def test_upgrade_mem_and_preset_exception(self):
+        self.assertRaises(
+            ValueError,
+            self.vs.upgrade,
+            1234,
+            memory=10,
+            preset="M1_64X512X100"
+        )
+
+    def test_upgrade_cpu_and_preset_exception(self):
+        self.assertRaises(
+            ValueError,
+            self.vs.upgrade,
+            1234,
+            cpus=10,
+            preset="M1_64X512X100"
+        )
+
+    @mock.patch('SoftLayer.managers.vs.VSManager._get_price_id_for_upgrade_option')
+    def test_upgrade_no_price_exception(self, get_price):
+        get_price.return_value = None
+        self.assertRaises(
+            exceptions.SoftLayerError,
+            self.vs.upgrade,
+            1234,
+            memory=1,
+        )
+
     @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict')
     def test_order_guest(self, create_dict):
         create_dict.return_value = {'test': 1, 'verify': 1}
@@ -173,3 +202,42 @@ def test_order_guest_ipv6(self, create_dict):
         self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate')
         self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=200)
         self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder')
+
+    @mock.patch('SoftLayer.managers.vs.VSManager._generate_create_dict')
+    def test_order_guest_placement_group(self, create_dict):
+        create_dict.return_value = {'test': 1, 'verify': 1}
+        guest = {'test': 1, 'verify': 1, 'placement_id': 5}
+        result = self.vs.order_guest(guest, test=True)
+
+        call = self.calls('SoftLayer_Product_Order', 'verifyOrder')[0]
+        order_container = call.args[0]
+
+        self.assertEqual(1234, result['orderId'])
+        self.assertEqual(5, order_container['virtualGuests'][0]['placementGroupId'])
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate')
+        self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder')
+
+    def test_get_price_id_empty(self):
+        upgrade_prices = [
+            {'categories': None, 'item': None},
+            {'categories': [{'categoryCode': 'ram'}], 'item': None},
+            {'categories': None, 'item': {'capacity': 1}},
+        ]
+        result = self.vs._get_price_id_for_upgrade_option(upgrade_prices, 'memory', 1)
+        self.assertEqual(None, result)
+
+    def test_get_price_id_memory_capacity(self):
+        upgrade_prices = [
+            {'categories': [{'categoryCode': 'ram'}], 'item': {'capacity': 1}, 'id': 99}
+        ]
+        result = self.vs._get_price_id_for_upgrade_option(upgrade_prices, 'memory', 1)
+        self.assertEqual(99, result)
+
+    def test_get_price_id_mismatch_capacity(self):
+        upgrade_prices = [
+            {'categories': [{'categoryCode': 'ram1'}], 'item': {'capacity': 1}, 'id': 90},
+            {'categories': [{'categoryCode': 'ram'}], 'item': {'capacity': 2}, 'id': 91},
+            {'categories': [{'categoryCode': 'ram'}], 'item': {'capacity': 1}, 'id': 92},
+        ]
+        result = self.vs._get_price_id_for_upgrade_option(upgrade_prices, 'memory', 1)
+        self.assertEqual(92, result)
diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py
index 47674345..2192e642 100644
--- a/tests/managers/vs/vs_tests.py
+++ b/tests/managers/vs/vs_tests.py
@@ -477,6 +477,28 @@ def test_generate_private_vlan(self):
 
         self.assertEqual(data, assert_data)
 
+    def test_generate_sec_group(self):
+        data = self.vs._generate_create_dict(
+            cpus=1,
+            memory=1,
+            hostname='test',
+            domain='test.com',
+            os_code="OS",
+            public_security_groups=[1, 2, 3],
+            private_security_groups=[4, 5, 6]
+        )
+
+        pub_sec_binding = data['primaryNetworkComponent']['securityGroupBindings']
+        prv_sec_binding = data['primaryBackendNetworkComponent']['securityGroupBindings']
+        # Public
+        self.assertEqual(pub_sec_binding[0]['securityGroup']['id'], 1)
+        self.assertEqual(pub_sec_binding[1]['securityGroup']['id'], 2)
+        self.assertEqual(pub_sec_binding[2]['securityGroup']['id'], 3)
+        # Private
+        self.assertEqual(prv_sec_binding[0]['securityGroup']['id'], 4)
+        self.assertEqual(prv_sec_binding[1]['securityGroup']['id'], 5)
+        self.assertEqual(prv_sec_binding[2]['securityGroup']['id'], 6)
+
     def test_create_network_components_vlan_subnet_private_vlan_subnet_public(self):
         data = self.vs._create_network_components(
             private_vlan=1,
@@ -843,7 +865,8 @@ def test_usage_vs_cpu(self):
 
         args = ('2019-3-4', '2019-4-2', [{"keyName": "CPU0", "summaryType": "max"}], 300)
 
-        self.assert_called_with('SoftLayer_Metric_Tracking_Object', 'getSummaryData', args=args, identifier=1000)
+        self.assert_called_with('SoftLayer_Metric_Tracking_Object',
+                                'getSummaryData', args=args, identifier=1000)
 
     def test_usage_vs_memory(self):
         result = self.vs.get_summary_data_usage('100',
@@ -858,3 +881,23 @@ def test_usage_vs_memory(self):
         args = ('2019-3-4', '2019-4-2', [{"keyName": "MEMORY_USAGE", "summaryType": "max"}], 300)
 
         self.assert_called_with('SoftLayer_Metric_Tracking_Object', 'getSummaryData', args=args, identifier=1000)
+
+    def test_get_tracking_id(self):
+        result = self.vs.get_tracking_id(1234)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'getMetricTrackingObjectId')
+        self.assertEqual(result, 1000)
+
+    def test_get_bandwidth_data(self):
+        result = self.vs.get_bandwidth_data(1234, '2019-01-01', '2019-02-01', 'public', 1000)
+        self.assert_called_with('SoftLayer_Metric_Tracking_Object',
+                                'getBandwidthData',
+                                args=('2019-01-01', '2019-02-01', 'public', 1000),
+                                identifier=1000)
+        self.assertEqual(result[0]['type'], 'cpu0')
+
+    def test_get_bandwidth_allocation(self):
+        result = self.vs.get_bandwidth_allocation(1234)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'getBandwidthAllotmentDetail', identifier=1234)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'getBillingCycleBandwidthUsage', identifier=1234)
+        self.assertEqual(result['allotment']['amount'], '250')
+        self.assertEqual(result['useage'][0]['amountIn'], '.448')
diff --git a/tests/managers/vs/vs_waiting_for_ready_tests.py b/tests/managers/vs/vs_waiting_for_ready_tests.py
index 802b945f..4308bd55 100644
--- a/tests/managers/vs/vs_waiting_for_ready_tests.py
+++ b/tests/managers/vs/vs_waiting_for_ready_tests.py
@@ -151,7 +151,7 @@ def test_exception_from_api(self, _sleep, _time, _vs, _dsleep):
         _dsleep.return_value = False
 
         self.guestObject.side_effect = [
-            exceptions.TransportError(104, "Its broken"),
+            exceptions.ServerError(504, "Its broken"),
             {'activeTransaction': {'id': 1}},
             {'provisionDate': 'aaa'}
         ]