Repository: softlayer/softlayer-python
Pull request #735 (2016-06-27 01:55:11)

Enhance the client to support uploading an attachment to a ticket
Please review, comment and merge

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

Coverage decreased (-0.1%) to 79.654% when pulling **e30bbb589811a534619b27e8e421b4da8bcf1972 on joshuakwan:master** into **62515bd07eb51982ed4271b574ecb96a167fe82f on softlayer:master**.


Patch:
diff --git a/SoftLayer/API.py b/SoftLayer/API.py
index dc887d41..9b9fd518 100644
--- a/SoftLayer/API.py
+++ b/SoftLayer/API.py
@@ -34,6 +34,7 @@
     'raw_headers',
     'limit',
     'offset',
+    'verify',
 ))
 
 
@@ -174,15 +175,22 @@ def __getitem__(self, name):
         return Service(self, name)
 
     def call(self, service, method, *args, **kwargs):
-        """Make a SoftLayer API call
+        """Make a SoftLayer API call.
 
-        :param service: the name of the SoftLayer API service
         :param method: the method to call on the service
-        :param \\*args: same optional arguments that ``Service.call`` takes
-        :param \\*\\*kwargs: same optional keyword arguments that
-                           ``Service.call`` takes
-
-        :param service: the name of the SoftLayer API service
+        :param \\*args: (optional) arguments for the remote call
+        :param id: (optional) id for the resource
+        :param mask: (optional) object mask
+        :param dict filter: (optional) filter dict
+        :param dict headers: (optional) optional XML-RPC headers
+        :param boolean compress: (optional) Enable/Disable HTTP compression
+        :param dict raw_headers: (optional) HTTP transport headers
+        :param int limit: (optional) return at most this many results
+        :param int offset: (optional) offset results by this many
+        :param boolean iter: (optional) if True, returns a generator with the
+                             results
+        :param bool verify: verify SSL cert
+        :param cert: client certificate path
 
         Usage:
             >>> import SoftLayer
@@ -222,6 +230,7 @@ def call(self, service, method, *args, **kwargs):
         request.filter = kwargs.get('filter')
         request.limit = kwargs.get('limit')
         request.offset = kwargs.get('offset')
+        request.verify = kwargs.get('verify')
 
         if self.auth:
             extra_headers = self.auth.get_headers()
@@ -314,22 +323,15 @@ def __init__(self, client, name):
         self.name = name
 
     def call(self, name, *args, **kwargs):
-        """Make a SoftLayer API call.
+        """Make a SoftLayer API call
 
+        :param service: the name of the SoftLayer API service
         :param method: the method to call on the service
-        :param \\*args: (optional) arguments for the remote call
-        :param id: (optional) id for the resource
-        :param mask: (optional) object mask
-        :param dict filter: (optional) filter dict
-        :param dict headers: (optional) optional XML-RPC headers
-        :param boolean compress: (optional) Enable/Disable HTTP compression
-        :param dict raw_headers: (optional) HTTP transport headers
-        :param int limit: (optional) return at most this many results
-        :param int offset: (optional) offset results by this many
-        :param boolean iter: (optional) if True, returns a generator with the
-                             results
-        :param bool verify: verify SSL cert
-        :param cert: client certificate path
+        :param \\*args: same optional arguments that ``BaseClient.call`` takes
+        :param \\*\\*kwargs: same optional keyword arguments that
+                           ``BaseClient.call`` takes
+
+        :param service: the name of the SoftLayer API service
 
         Usage:
             >>> import SoftLayer
diff --git a/SoftLayer/CLI/block/access/__init__.py b/SoftLayer/CLI/block/access/__init__.py
new file mode 100644
index 00000000..050b3938
--- /dev/null
+++ b/SoftLayer/CLI/block/access/__init__.py
@@ -0,0 +1,1 @@
+"""Block Storage Access Control."""
diff --git a/SoftLayer/CLI/block/access/authorize.py b/SoftLayer/CLI/block/access/authorize.py
new file mode 100644
index 00000000..4e9dc20f
--- /dev/null
+++ b/SoftLayer/CLI/block/access/authorize.py
@@ -0,0 +1,42 @@
+"""Create a block storage snapshot."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+
+
+@click.command()
+@click.argument('volume_id')
+@click.option('--hardware-id', '-h', multiple=True,
+              help='The id of one SoftLayer_Hardware to authorize')
+@click.option('--virtual-id', '-v', multiple=True,
+              help='The id of one SoftLayer_Virtual_Guest to authorize')
+@click.option('--ip-address-id', '-i', multiple=True,
+              help='The id of one SoftLayer_Network_Subnet_IpAddress'
+              ' to authorize')
+@click.option('--ip-address', multiple=True,
+              help='An IP address to authorize')
+@environment.pass_env
+def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, ip_address):
+    """Authorizes hosts to access a given volume"""
+    block_manager = SoftLayer.BlockStorageManager(env.client)
+    ip_address_id_list = list(ip_address_id)
+
+    # Convert actual IP Addresses to their SoftLayer ids
+    if ip_address is not None:
+        network_manager = SoftLayer.NetworkManager(env.client)
+        for ip_address_value in ip_address:
+            ip_address_object = network_manager.ip_lookup(ip_address_value)
+            if ip_address_object == "":
+                click.echo("IP Address not found on your account.  " +
+                           "Please confirm IP and try again.")
+                raise exceptions.ArgumentError('Incorrect IP Address')
+            else:
+                ip_address_id_list.append(ip_address_object['id'])
+
+    block_manager.authorize_host_to_volume(volume_id,
+                                           hardware_id,
+                                           virtual_id,
+                                           ip_address_id_list)
diff --git a/SoftLayer/CLI/block/access/revoke.py b/SoftLayer/CLI/block/access/revoke.py
new file mode 100644
index 00000000..1b0c93a4
--- /dev/null
+++ b/SoftLayer/CLI/block/access/revoke.py
@@ -0,0 +1,38 @@
+"""Create a block storage snapshot."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+import SoftLayer
+from SoftLayer.CLI import environment
+
+
+@click.command()
+@click.argument('volume_id')
+@click.option('--hardware-id', '-h', multiple=True,
+              help='The id of one SoftLayer_Hardware'
+              ' to revoke authorization')
+@click.option('--virtual-id', '-v', multiple=True,
+              help='The id of one SoftLayer_Virtual_Guest'
+              ' to revoke authorization')
+@click.option('--ip_address-id', '-i', multiple=True,
+              help='The id of one SoftLayer_Network_Subnet_IpAddress'
+              ' to revoke authorization')
+@click.option('--ip-address', multiple=True,
+              help='An IP address to revoke authorization')
+@environment.pass_env
+def cli(env, volume_id, hardware_id, virtual_id, ip_address_id, ip_address):
+    """Revokes authorization for hosts accessing a given volume"""
+    block_manager = SoftLayer.BlockStorageManager(env.client)
+    ip_address_id_list = list(ip_address_id)
+
+    # Convert actual IP Addresses to their SoftLayer ids
+    if ip_address is not None:
+        network_manager = SoftLayer.NetworkManager(env.client)
+        for ip_address_value in ip_address:
+            ip_address_object = network_manager.ip_lookup(ip_address_value)
+            ip_address_id_list.append(ip_address_object['id'])
+
+    block_manager.deauthorize_host_to_volume(volume_id,
+                                             hardware_id,
+                                             virtual_id,
+                                             ip_address_id_list)
diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py
index 592052a2..cb914fef 100644
--- a/SoftLayer/CLI/environment.py
+++ b/SoftLayer/CLI/environment.py
@@ -69,7 +69,7 @@ def list_commands(self, *path):
         path_str = ':'.join(path)
 
         commands = []
-        for command in self.commands.keys():
+        for command in self.commands:
 
             # Filter based on prefix and the segment length
             if all([command.startswith(path_str),
diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py
index 15b97199..9ba66bb1 100644
--- a/SoftLayer/CLI/formatting.py
+++ b/SoftLayer/CLI/formatting.py
@@ -323,9 +323,14 @@ def __str__(self):
         # If the original value is None, represent this as 'NULL'
         if self.original is None:
             return 'NULL'
-        return str(self.original)
 
-    __repr__ = __str__
+        try:
+            return str(self.original)
+        except UnicodeError:
+            return 'invalid'
+
+    def __repr__(self):
+        return 'FormattedItem(%r, %r)' % (self.original, self.formatted)
 
     # Implement sorting methods.
     # NOTE(kmcdonald): functools.total_ordering could be used once support for
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index a9aec3c9..f7ff388f 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -61,9 +61,13 @@
     ('block:volume-detail', 'SoftLayer.CLI.block.detail:cli'),
     ('block:volume-cancel', 'SoftLayer.CLI.block.cancel:cli'),
     ('block:volume-order', 'SoftLayer.CLI.block.order:cli'),
+
     ('block:snapshot-list', 'SoftLayer.CLI.block.snapshot_list:cli'),
     ('block:snapshot-delete', 'SoftLayer.CLI.block.snapshot_delete:cli'),
+
     ('block:access-list', 'SoftLayer.CLI.block.access_list:cli'),
+    ('block:access-authorize', 'SoftLayer.CLI.block.access.authorize:cli'),
+    ('block:access-revoke', 'SoftLayer.CLI.block.access.revoke:cli'),
 
     ('firewall', 'SoftLayer.CLI.firewall'),
     ('firewall:add', 'SoftLayer.CLI.firewall.add:cli'),
@@ -195,6 +199,7 @@
     ('ticket:detail', 'SoftLayer.CLI.ticket.detail:cli'),
     ('ticket:list', 'SoftLayer.CLI.ticket.list:cli'),
     ('ticket:update', 'SoftLayer.CLI.ticket.update:cli'),
+    ('ticket:upload', 'SoftLayer.CLI.ticket.upload:cli'),
     ('ticket:subjects', 'SoftLayer.CLI.ticket.subjects:cli'),
     ('ticket:summary', 'SoftLayer.CLI.ticket.summary:cli'),
     ('ticket:attach', 'SoftLayer.CLI.ticket.attach:cli'),
diff --git a/SoftLayer/CLI/subnet/detail.py b/SoftLayer/CLI/subnet/detail.py
index cfc0dadb..9c089fcc 100644
--- a/SoftLayer/CLI/subnet/detail.py
+++ b/SoftLayer/CLI/subnet/detail.py
@@ -7,6 +7,7 @@
 from SoftLayer.CLI import environment
 from SoftLayer.CLI import formatting
 from SoftLayer.CLI import helpers
+from SoftLayer import utils
 
 
 @click.command()
@@ -35,6 +36,8 @@ def cli(env, identifier, no_vs, no_hardware):
                    '%s/%s' % (subnet['networkIdentifier'],
                               str(subnet['cidr']))])
     table.add_row(['subnet type', subnet['subnetType']])
+    table.add_row(['network space',
+                   utils.lookup(subnet, 'networkVlan', 'networkSpace')])
     table.add_row(['gateway', subnet.get('gateway', formatting.blank())])
     table.add_row(['broadcast',
                    subnet.get('broadcastAddress', formatting.blank())])
diff --git a/SoftLayer/CLI/subnet/list.py b/SoftLayer/CLI/subnet/list.py
index 5db042a7..e5d0d83a 100644
--- a/SoftLayer/CLI/subnet/list.py
+++ b/SoftLayer/CLI/subnet/list.py
@@ -15,7 +15,7 @@
               type=click.Choice(['id',
                                  'identifier',
                                  'type',
-                                 'address_space',
+                                 'network_space',
                                  'datacenter',
                                  'vlan_id',
                                  'IPs',
@@ -25,18 +25,18 @@
               help="Filter by datacenter shortname (sng01, dal05, ...)")
 @click.option('--identifier', help="Filter by network identifier")
 @click.option('--subnet-type', '-t', help="Filter by subnet type")
-@click.option('--address-space', help="Filter by address space")
+@click.option('--network-space', help="Filter by network space")
 @click.option('--v4', '--ipv4', is_flag=True, help="Display only IPv4 subnets")
 @click.option('--v6', '--ipv6', is_flag=True, help="Display only IPv6 subnets")
 @environment.pass_env
-def cli(env, sortby, datacenter, identifier, subnet_type, address_space,
+def cli(env, sortby, datacenter, identifier, subnet_type, network_space,
         ipv4, ipv6):
     """List subnets."""
 
     mgr = SoftLayer.NetworkManager(env.client)
 
     table = formatting.Table([
-        'id', 'identifier', 'type', 'address_space', 'datacenter', 'vlan_id',
+        'id', 'identifier', 'type', 'network_space', 'datacenter', 'vlan_id',
         'IPs', 'hardware', 'vs',
     ])
     table.sortby = sortby
@@ -52,7 +52,7 @@ def cli(env, sortby, datacenter, identifier, subnet_type, address_space,
         version=version,
         identifier=identifier,
         subnet_type=subnet_type,
-        address_space=address_space,
+        network_space=network_space,
     )
 
     for subnet in subnets:
@@ -60,7 +60,9 @@ def cli(env, sortby, datacenter, identifier, subnet_type, address_space,
             subnet['id'],
             '%s/%s' % (subnet['networkIdentifier'], str(subnet['cidr'])),
             subnet.get('subnetType', formatting.blank()),
-            subnet.get('addressSpace', formatting.blank()),
+            utils.lookup(subnet,
+                         'networkVlan',
+                         'networkSpace') or formatting.blank(),
             utils.lookup(subnet, 'datacenter', 'name',) or formatting.blank(),
             subnet['networkVlanId'],
             subnet['ipAddressCount'],
diff --git a/SoftLayer/CLI/ticket/upload.py b/SoftLayer/CLI/ticket/upload.py
new file mode 100644
index 00000000..a26ca880
--- /dev/null
+++ b/SoftLayer/CLI/ticket/upload.py
@@ -0,0 +1,37 @@
+"""Adds an attachment to an existing ticket."""
+# :license: MIT, see LICENSE for more details.
+
+import os
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier')
+@click.option('--path', help="The path of the attachment to be uploaded")
+@click.option('--name', help="The name of the attachment shown in the ticket")
+@environment.pass_env
+def cli(env, identifier, path, name):
+    """Adds an attachment to an existing ticket."""
+    mgr = SoftLayer.TicketManager(env.client)
+
+    ticket_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'ticket')
+
+    if path is None:
+        raise exceptions.ArgumentError("Missing argument --path")
+
+    if not os.path.exists(path):
+        raise exceptions.ArgumentError("%s not exist" % path)
+
+    if name is None:
+        name = os.path.basename(path)
+
+    attached_file = mgr.upload_attachment(ticket_id=ticket_id,
+                                          file_path=path,
+                                          file_name=name)
+    env.fout("File attached: \n%s" % attached_file)
diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py
index 41b7a164..2aad9220 100644
--- a/SoftLayer/CLI/virt/create_options.py
+++ b/SoftLayer/CLI/virt/create_options.py
@@ -90,7 +90,7 @@ def add_block_rows(disks, name):
 
             simple[bid].append(str(block['diskImage']['capacity']))
 
-        for label in sorted(simple.keys()):
+        for label in sorted(simple):
             table.add_row(['%s disk(%s)' % (name, label),
                            formatting.listing(simple[label],
                                               separator=',')])
diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py
index b03c26d0..509945b3 100644
--- a/SoftLayer/fixtures/SoftLayer_Account.py
+++ b/SoftLayer/fixtures/SoftLayer_Account.py
@@ -1,3 +1,5 @@
+# -*- coding: UTF-8 -*-
+
 getPrivateBlockDeviceTemplateGroups = [{
     'accountId': 1234,
     'blockDevices': [],
@@ -5,15 +7,18 @@
     'globalIdentifier': 'E6DBD73B-1651-4B28-BCBA-A11DF7C9D79E',
     'id': 200,
     'name': 'test_image',
-    'parentId': ''
+    'parentId': '',
+    'publicFlag': False,
 }, {
     'accountId': 1234,
     'blockDevices': [],
     'createDate': '2013-12-05T21:53:03-06:00',
     'globalIdentifier': 'F9329795-4220-4B0A-B970-C86B950667FA',
     'id': 201,
-    'name': 'private_image2',
-    'parentId': ''
+    # 'name': 'private_image2',
+    'name': u'a¬ሴ€耀',
+    'parentId': '',
+    'publicFlag': False,
 }]
 
 getVirtualGuests = [{
diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py
index 0257b167..5a45f14f 100644
--- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py
+++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py
@@ -63,3 +63,5 @@
 }
 getSnapshots = []
 deleteObject = True
+allowAccessFromHostList = True
+removeAccessFromHostList = True
diff --git a/SoftLayer/fixtures/SoftLayer_Ticket.py b/SoftLayer/fixtures/SoftLayer_Ticket.py
index c85deda8..ea230f98 100644
--- a/SoftLayer/fixtures/SoftLayer_Ticket.py
+++ b/SoftLayer/fixtures/SoftLayer_Ticket.py
@@ -48,5 +48,17 @@
     "ticketId": 100
 }
 
+addAttachedFile = {
+    "id": 123,
+    "fileName": "a_file_name",
+    "fileSize": "100",
+    "ticketId": 100,
+    "updateId": 200,
+    "createDate": "2013-08-01T14:14:04-07:00",
+    "modifyDate": "2013-08-01T14:14:04-07:00",
+    "uploaderType": "USER",
+    "uploaderId": "300"
+}
+
 removeAttachedHardware = True
 removeAttachedVirtualGuest = True
diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py
index 3030c009..ee58ad76 100644
--- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py
+++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py
@@ -5,7 +5,8 @@
     'globalIdentifier': '0B5DEAF4-643D-46CA-A695-CECBE8832C9D',
     'id': 100,
     'name': 'test_image',
-    'parentId': ''
+    'parentId': '',
+    'publicFlag': True,
 }, {
     'accountId': 1234,
     'blockDevices': [],
@@ -13,7 +14,8 @@
     'globalIdentifier': 'EB38414C-2AB3-47F3-BBBD-56A5F689620B',
     'id': 101,
     'name': 'test_image2',
-    'parentId': ''
+    'parentId': '',
+    'publicFlag': True,
 }]
 
 getObject = IMAGES[0]
diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py
index 906496ee..26040321 100644
--- a/SoftLayer/managers/block.py
+++ b/SoftLayer/managers/block.py
@@ -135,6 +135,80 @@ def get_block_volume_snapshot_list(self, volume_id, **kwargs):
         return self.client.call('Network_Storage', 'getSnapshots',
                                 id=volume_id, **kwargs)
 
+    def authorize_host_to_volume(self, volume_id,
+                                 hardware_ids=None,
+                                 virtual_guest_ids=None,
+                                 ip_address_ids=None):
+        """Authorizes hosts to Block Storage Volumes
+
+        :param volume_id: The Volume to authorize hosts to
+        :param hardware_ids: A List of SoftLayer_Hardware ids
+        :param virtual_guest_ids: A List of SoftLayer_Virtual_Guest ids
+        :param ip_address_ids: A List of SoftLayer_Network_Subnet_IpAddress ids
+        """
+        host_templates = []
+
+        if hardware_ids is not None:
+            for hardware_id in hardware_ids:
+                host_templates.append({
+                    'objectType': 'SoftLayer_Hardware',
+                    'id': hardware_id
+                })
+
+        if virtual_guest_ids is not None:
+            for virtual_guest_id in virtual_guest_ids:
+                host_templates.append({
+                    'objectType': 'SoftLayer_Virtual_Guest',
+                    'id': virtual_guest_id
+                })
+
+        if ip_address_ids is not None:
+            for ip_address_id in ip_address_ids:
+                host_templates.append({
+                    'objectType': 'SoftLayer_Network_Subnet_IpAddress',
+                    'id': ip_address_id
+                })
+
+        return self.client.call('Network_Storage', 'allowAccessFromHostList',
+                                host_templates, id=volume_id)
+
+    def deauthorize_host_to_volume(self, volume_id,
+                                   hardware_ids=None,
+                                   virtual_guest_ids=None,
+                                   ip_address_ids=None):
+        """Revokes authorization of hosts to Block Storage Volumes
+
+        :param volume_id: The Volume to deauthorize hosts to
+        :param hardware_ids: A List of SoftLayer_Hardware ids
+        :param virtual_guest_ids: A List of SoftLayer_Virtual_Guest ids
+        :param ip_address_ids: A List of SoftLayer_Network_Subnet_IpAddress ids
+        """
+        host_templates = []
+
+        if hardware_ids is not None:
+            for hardware_id in hardware_ids:
+                host_templates.append({
+                    'objectType': 'SoftLayer_Hardware',
+                    'id': hardware_id
+                })
+
+        if virtual_guest_ids is not None:
+            for virtual_guest_id in virtual_guest_ids:
+                host_templates.append({
+                    'objectType': 'SoftLayer_Virtual_Guest',
+                    'id': virtual_guest_id
+                })
+
+        if ip_address_ids is not None:
+            for ip_address_id in ip_address_ids:
+                host_templates.append({
+                    'objectType': 'SoftLayer_Network_Subnet_IpAddress',
+                    'id': ip_address_id
+                })
+
+        return self.client.call('Network_Storage', 'removeAccessFromHostList',
+                                host_templates, id=volume_id)
+
     def delete_snapshot(self, snapshot_id):
         """Deletes the specified snapshot object.
 
diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py
index 1cf3b315..904b91f9 100644
--- a/SoftLayer/managers/network.py
+++ b/SoftLayer/managers/network.py
@@ -14,7 +14,7 @@
                                 'datacenter',
                                 'ipAddressCount',
                                 'virtualGuests',
-                                'addressSpace'])
+                                'networkVlan[id,networkSpace]'])
 DEFAULT_VLAN_MASK = ','.join([
     'firewallInterfaces',
     'hardwareCount',
@@ -245,7 +245,7 @@ def list_global_ips(self, version=None, identifier=None, **kwargs):
         return self.account.getGlobalIpRecords(**kwargs)
 
     def list_subnets(self, identifier=None, datacenter=None, version=0,
-                     subnet_type=None, address_space=None, **kwargs):
+                     subnet_type=None, network_space=None, **kwargs):
         """Display a list of all subnets on the account.
 
         This provides a quick overview of all subnets including information
@@ -258,7 +258,7 @@ def list_subnets(self, identifier=None, datacenter=None, version=0,
         :param int version: Only returns subnets of this version (4 or 6).
         :param string subnet_type: If specified, it will only returns subnets
                                      of this type.
-        :param string address_space: If specified, it will only returns subnets
+        :param string network_space: If specified, it will only returns subnets
                                        with the given address space label.
         :param dict \\*\\*kwargs: response-level options (mask, limit, etc.)
         """
@@ -280,9 +280,9 @@ def list_subnets(self, identifier=None, datacenter=None, version=0,
         else:
             # This filters out global IPs from the subnet listing.
             _filter['subnets']['subnetType'] = {'operation': '!= GLOBAL_IP'}
-        if address_space:
-            _filter['subnets']['addressSpace'] = utils.query_filter(
-                address_space)
+        if network_space:
+            _filter['subnets']['networkVlan']['networkSpace'] = (
+                utils.query_filter(network_space))
 
         kwargs['filter'] = _filter.to_dict()
 
diff --git a/SoftLayer/managers/ticket.py b/SoftLayer/managers/ticket.py
index eb5a12a6..d95f5c83 100644
--- a/SoftLayer/managers/ticket.py
+++ b/SoftLayer/managers/ticket.py
@@ -80,6 +80,30 @@ def update_ticket(self, ticket_id=None, body=None):
         """
         return self.ticket.addUpdate({'entry': body}, id=ticket_id)
 
+    def upload_attachment(self, ticket_id=None, file_path=None,
+                          file_name=None):
+        """Upload an attachment to a ticket.
+
+        :param integer ticket_id: the id of the ticket to
+                                  upload the attachment to
+        :param string file_path:
+                                  The path of the attachment to be uploaded
+        :param string file_name:
+                                  The name of the attachment shown
+                                  in the ticket
+        :returns The uploaded attachment
+        """
+        file_content = None
+        with open(file_path, 'rb') as attached_file:
+            file_content = attached_file.read()
+
+        file_object = {
+            "filename": file_name,
+            "data": file_content
+        }
+
+        return self.ticket.addAttachedFile(file_object, id=ticket_id)
+
     def attach_hardware(self, ticket_id=None, hardware_id=None):
         """Attach hardware to a ticket.
 


Test patch:
diff --git a/SoftLayer/testing/xmlrpc.py b/SoftLayer/testing/xmlrpc.py
index d13753b3..b4b81067 100644
--- a/SoftLayer/testing/xmlrpc.py
+++ b/SoftLayer/testing/xmlrpc.py
@@ -59,9 +59,12 @@ def do_POST(self):
                                                       methodresponse=True)
 
             self.send_response(200)
-            self.send_header("Content-type", "application/xml")
+            self.send_header("Content-type", "application/xml; charset=UTF-8")
             self.end_headers()
-            self.wfile.write(response_body.encode('utf-8'))
+            try:
+                self.wfile.write(response_body.encode('utf-8'))
+            except UnicodeDecodeError:
+                self.wfile.write(response_body)
 
         except SoftLayer.SoftLayerAPIError as ex:
             self.send_response(200)
diff --git a/tests/CLI/helper_tests.py b/tests/CLI/helper_tests.py
index 59b41d92..68f5dc6a 100644
--- a/tests/CLI/helper_tests.py
+++ b/tests/CLI/helper_tests.py
@@ -11,6 +11,7 @@
 
 import click
 import mock
+import six
 
 from SoftLayer.CLI import core
 from SoftLayer.CLI import exceptions
@@ -95,6 +96,13 @@ def test_init(self):
         self.assertEqual('test', item.formatted)
         self.assertEqual('test', str(item))
 
+    def test_unicode(self):
+        if six.PY2:
+            item = formatting.FormattedItem(u'\u32423', u'\u32423')
+            self.assertEqual(u'\u32423', item.original)
+            self.assertEqual(u'\u32423', item.formatted)
+            self.assertEqual('invalid', str(item))
+
     def test_mb_to_gb(self):
         item = formatting.mb_to_gb(1024)
         self.assertEqual(1024, item.original)
diff --git a/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py
index 69674c63..c9d27ba3 100644
--- a/tests/CLI/modules/ticket_tests.py
+++ b/tests/CLI/modules/ticket_tests.py
@@ -167,3 +167,40 @@ def test_ticket_detach_virtual_server(self):
                                 'removeAttachedVirtualGuest',
                                 args=(100,),
                                 identifier=1)
+
+    def test_ticket_upload_no_path(self):
+        result = self.run_command(['ticket', 'upload', '1'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_ticket_upload_invalid_path(self):
+        result = self.run_command(['ticket', 'upload', '1',
+                                   '--path=tests/resources/nonexistent_file',
+                                   '--name=a_file_name'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_ticket_upload_no_name(self):
+        result = self.run_command(['ticket', 'upload', '1',
+                                   '--path=tests/resources/attachment_upload'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Ticket',
+                                'addAttachedFile',
+                                args=({"filename": "attachment_upload",
+                                       "data": b"ticket attached data"},),
+                                identifier=1)
+
+    def test_ticket_upload(self):
+        result = self.run_command(['ticket', 'upload', '1',
+                                   '--path=tests/resources/attachment_upload',
+                                   '--name=a_file_name'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Ticket',
+                                'addAttachedFile',
+                                args=({"filename": "a_file_name",
+                                       "data": b"ticket attached data"},),
+                                identifier=1)
diff --git a/tests/api_tests.py b/tests/api_tests.py
index 34df1517..8e89d27c 100644
--- a/tests/api_tests.py
+++ b/tests/api_tests.py
@@ -80,34 +80,15 @@ def test_simple_call(self):
                                 offset=None,
                                 )
 
-    def test_complex(self):
+    def test_verify(self):
+        client = SoftLayer.BaseClient(transport=self.mocks)
         mock = self.set_mock('SoftLayer_SERVICE', 'METHOD')
         mock.return_value = {"test": "result"}
-        _filter = {'TYPE': {'attribute': {'operation': '^= prefix'}}}
-
-        resp = self.client['SERVICE'].METHOD(
-            1234,
-            id=5678,
-            mask={'object': {'attribute': ''}},
-            headers={'header': 'value'},
-            raw_headers={'RAW': 'HEADER'},
-            filter=_filter,
-            limit=9,
-            offset=10)
+
+        resp = client.call('SERVICE', 'METHOD', verify=False)
 
         self.assertEqual(resp, {"test": "result"})
-        self.assert_called_with('SoftLayer_SERVICE', 'METHOD',
-                                mask={'object': {'attribute': ''}},
-                                filter=_filter,
-                                identifier=5678,
-                                args=(1234,),
-                                limit=9,
-                                offset=10,
-                                )
-        calls = self.calls('SoftLayer_SERVICE', 'METHOD')
-        self.assertEqual(len(calls), 1)
-        self.assertIn('header', calls[0].headers)
-        self.assertEqual(calls[0].headers['header'], 'value')
+        self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=False)
 
     @mock.patch('SoftLayer.API.BaseClient.iter_call')
     def test_iterate(self, _iter_call):
diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py
index a6b6f866..e278cf32 100644
--- a/tests/managers/block_tests.py
+++ b/tests/managers/block_tests.py
@@ -210,3 +210,19 @@ def test_order_block_volume_endurance(self):
                     'setupFee': '1'}],
                 },
             )
+
+    def test_authorize_host_to_volume(self):
+        result = self.block.authorize_host_to_volume(50,
+                                                     hardware_ids=[100],
+                                                     virtual_guest_ids=[200],
+                                                     ip_address_ids=[300])
+        self.assertEqual(fixtures.SoftLayer_Network_Storage.
+                         allowAccessFromHostList, result)
+
+    def test_deauthorize_host_to_volume(self):
+        result = self.block.authorize_host_to_volume(50,
+                                                     hardware_ids=[100],
+                                                     virtual_guest_ids=[200],
+                                                     ip_address_ids=[300])
+        self.assertEqual(fixtures.SoftLayer_Network_Storage.
+                         removeAccessFromHostList, result)
diff --git a/tests/managers/network_tests.py b/tests/managers/network_tests.py
index 446bc7b3..b62a656a 100644
--- a/tests/managers/network_tests.py
+++ b/tests/managers/network_tests.py
@@ -193,7 +193,7 @@ def test_list_subnets_with_filters(self):
             datacenter='dal00',
             version=4,
             subnet_type='PRIMARY',
-            address_space='PUBLIC',
+            network_space='PUBLIC',
         )
 
         self.assertEqual(result, fixtures.SoftLayer_Account.getSubnets)
@@ -205,9 +205,10 @@ def test_list_subnets_with_filters(self):
                 },
                 'version': {'operation': 4},
                 'subnetType': {'operation': '_= PRIMARY'},
-                'addressSpace': {'operation': '_= PUBLIC'},
+                'networkVlan': {'networkSpace': {'operation': '_= PUBLIC'}},
             }
         }
+
         self.assert_called_with('SoftLayer_Account', 'getSubnets',
                                 mask='mask[%s]' % network.DEFAULT_SUBNET_MASK,
                                 filter=_filter)
diff --git a/tests/resources/attachment_upload b/tests/resources/attachment_upload
new file mode 100644
index 00000000..54ac0f19
--- /dev/null
+++ b/tests/resources/attachment_upload
@@ -0,0 +1,1 @@
+ticket attached data
\ No newline at end of file