Repository: softlayer/softlayer-python
Pull request #703 (2016-03-20 19:18:23)

Add Python 3.5 and later this year Python 3.6.


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index 506bdf14..39c1ee47 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,11 @@
+5.0.1
+
+    * Adds missing depdendency that was previously pulled in by prompt_toolkit
+
+    * Fix a bug by updating the CDN manager to use the new purge method
+
+    * Fixes bug that occured when iscsi listings with resources have no datacenter
+
 5.0.0
 
     * Adds a shell (accessable with `slcli shell`) which provides autocomplete for slcli commands and options
diff --git a/README.rst b/README.rst
index d8949f2f..31da9b61 100644
--- a/README.rst
+++ b/README.rst
@@ -57,7 +57,7 @@ This library relies on the `requests <http://docs.python-requests.org/>`_ librar
 
 System Requirements
 -------------------
-* Python 2.7, 3.3, or 3.4.
+* Python 2.7, 3.3 or higher.
 * A valid SoftLayer API username and key.
 * A connection to SoftLayer's private network is required to use
   our private network API endpoints.
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 0d93f2ec..26378fb4 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -188,6 +188,8 @@
     ('ticket:update', 'SoftLayer.CLI.ticket.update:cli'),
     ('ticket:subjects', 'SoftLayer.CLI.ticket.subjects:cli'),
     ('ticket:summary', 'SoftLayer.CLI.ticket.summary:cli'),
+    ('ticket:attach', 'SoftLayer.CLI.ticket.attach:cli'),
+    ('ticket:detach', 'SoftLayer.CLI.ticket.detach:cli'),
 
     ('vlan', 'SoftLayer.CLI.vlan'),
     ('vlan:detail', 'SoftLayer.CLI.vlan.detail:cli'),
diff --git a/SoftLayer/CLI/ticket/attach.py b/SoftLayer/CLI/ticket/attach.py
new file mode 100644
index 00000000..98adaa65
--- /dev/null
+++ b/SoftLayer/CLI/ticket/attach.py
@@ -0,0 +1,42 @@
+"""Attach devices to a ticket."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier', type=int)
+@click.option('--hardware',
+              'hardware_identifier',
+              help="The identifier for hardware to attach")
+@click.option('--virtual',
+              'virtual_identifier',
+              help="The identifier for a virtual server to attach")
+@environment.pass_env
+def cli(env, identifier, hardware_identifier, virtual_identifier):
+    """Attach devices to a ticket."""
+    ticket_mgr = SoftLayer.TicketManager(env.client)
+
+    if hardware_identifier and virtual_identifier:
+        raise exceptions.ArgumentError(
+            "Cannot attach hardware and a virtual server at the same time")
+    elif hardware_identifier:
+        hardware_mgr = SoftLayer.HardwareManager(env.client)
+        hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids,
+                                         hardware_identifier,
+                                         'hardware')
+        ticket_mgr.attach_hardware(identifier, hardware_id)
+    elif virtual_identifier:
+        vs_mgr = SoftLayer.VSManager(env.client)
+        vs_id = helpers.resolve_id(vs_mgr.resolve_ids,
+                                   virtual_identifier,
+                                   'VS')
+        ticket_mgr.attach_virtual_server(identifier, vs_id)
+    else:
+        raise exceptions.ArgumentError(
+            "Must have a hardware or virtual server identifier to attach")
diff --git a/SoftLayer/CLI/ticket/create.py b/SoftLayer/CLI/ticket/create.py
index 82f81ee8..71ebd26e 100644
--- a/SoftLayer/CLI/ticket/create.py
+++ b/SoftLayer/CLI/ticket/create.py
@@ -5,25 +5,49 @@
 
 import SoftLayer
 from SoftLayer.CLI import environment
+from SoftLayer.CLI import helpers
 from SoftLayer.CLI import ticket
 
 
 @click.command()
 @click.option('--title', required=True, help="The title of the ticket")
 @click.option('--subject-id',
+              type=int,
               required=True,
               help="""The subject id to use for the ticket,
  issue 'slcli ticket subjects' to get the list""")
 @click.option('--body', help="The ticket body")
+@click.option('--hardware',
+              'hardware_identifier',
+              help="The identifier for hardware to attach")
+@click.option('--virtual',
+              'virtual_identifier',
+              help="The identifier for a virtual server to attach")
 @environment.pass_env
-def cli(env, title, subject_id, body):
+def cli(env, title, subject_id, body, hardware_identifier, virtual_identifier):
     """Create a support ticket."""
-    mgr = SoftLayer.TicketManager(env.client)
+    ticket_mgr = SoftLayer.TicketManager(env.client)
 
     if body is None:
         body = click.edit('\n\n' + ticket.TEMPLATE_MSG)
 
-    created_ticket = mgr.create_ticket(title=title,
-                                       body=body,
-                                       subject=subject_id)
-    env.fout(ticket.get_ticket_results(mgr, created_ticket['id']))
+    created_ticket = ticket_mgr.create_ticket(
+        title=title,
+        body=body,
+        subject=subject_id)
+
+    if hardware_identifier:
+        hardware_mgr = SoftLayer.HardwareManager(env.client)
+        hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids,
+                                         hardware_identifier,
+                                         'hardware')
+        ticket_mgr.attach_hardware(created_ticket['id'], hardware_id)
+
+    if virtual_identifier:
+        vs_mgr = SoftLayer.VSManager(env.client)
+        vs_id = helpers.resolve_id(vs_mgr.resolve_ids,
+                                   virtual_identifier,
+                                   'VS')
+        ticket_mgr.attach_virtual_server(created_ticket['id'], vs_id)
+
+    env.fout(ticket.get_ticket_results(ticket_mgr, created_ticket['id']))
diff --git a/SoftLayer/CLI/ticket/detach.py b/SoftLayer/CLI/ticket/detach.py
new file mode 100644
index 00000000..8c8cae05
--- /dev/null
+++ b/SoftLayer/CLI/ticket/detach.py
@@ -0,0 +1,42 @@
+"""Detach devices from a ticket."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import helpers
+
+
+@click.command()
+@click.argument('identifier', type=int)
+@click.option('--hardware',
+              'hardware_identifier',
+              help="The identifier for hardware to detach")
+@click.option('--virtual',
+              'virtual_identifier',
+              help="The identifier for a virtual server to detach")
+@environment.pass_env
+def cli(env, identifier, hardware_identifier, virtual_identifier):
+    """Detach devices from a ticket."""
+    ticket_mgr = SoftLayer.TicketManager(env.client)
+
+    if hardware_identifier and virtual_identifier:
+        raise exceptions.ArgumentError(
+            "Cannot detach hardware and a virtual server at the same time")
+    elif hardware_identifier:
+        hardware_mgr = SoftLayer.HardwareManager(env.client)
+        hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids,
+                                         hardware_identifier,
+                                         'hardware')
+        ticket_mgr.detach_hardware(identifier, hardware_id)
+    elif virtual_identifier:
+        vs_mgr = SoftLayer.VSManager(env.client)
+        vs_id = helpers.resolve_id(vs_mgr.resolve_ids,
+                                   virtual_identifier,
+                                   'VS')
+        ticket_mgr.detach_virtual_server(identifier, vs_id)
+    else:
+        raise exceptions.ArgumentError(
+            "Must have a hardware or virtual server identifier to detach")
diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py
index 189eb53f..d764cfb0 100644
--- a/SoftLayer/CLI/virt/create.py
+++ b/SoftLayer/CLI/virt/create.py
@@ -81,7 +81,13 @@ def _parse_create_args(client, args):
         data['os_code'] = args['os']
 
     if args.get('image'):
-        data['image_id'] = args['image']
+        if args.get('image').isdigit():
+            image_mgr = SoftLayer.ImageManager(client)
+            image_details = image_mgr.get_image(args.get('image'),
+                                                mask="id,globalIdentifier")
+            data['image_id'] = image_details['globalIdentifier']
+        else:
+            data['image_id'] = args['image']
 
     if args.get('datacenter'):
         data['datacenter'] = args['datacenter']
diff --git a/SoftLayer/CLI/vlan/detail.py b/SoftLayer/CLI/vlan/detail.py
index f97928a2..59a08655 100644
--- a/SoftLayer/CLI/vlan/detail.py
+++ b/SoftLayer/CLI/vlan/detail.py
@@ -39,7 +39,7 @@ def cli(env, identifier, no_vs, no_hardware):
     table.add_row(['firewall',
                    'Yes' if vlan['firewallInterfaces'] else 'No'])
     subnets = []
-    for subnet in vlan['subnets']:
+    for subnet in vlan.get('subnets', []):
         subnet_table = formatting.KeyValueTable(['name', 'value'])
         subnet_table.align['name'] = 'r'
         subnet_table.align['value'] = 'l'
@@ -57,7 +57,7 @@ def cli(env, identifier, no_vs, no_hardware):
     server_columns = ['hostname', 'domain', 'public_ip', 'private_ip']
 
     if not no_vs:
-        if vlan['virtualGuests']:
+        if vlan.get('virtualGuests'):
             vs_table = formatting.KeyValueTable(server_columns)
             for vsi in vlan['virtualGuests']:
                 vs_table.add_row([vsi['hostname'],
@@ -69,7 +69,7 @@ def cli(env, identifier, no_vs, no_hardware):
             table.add_row(['vs', 'none'])
 
     if not no_hardware:
-        if vlan['hardware']:
+        if vlan.get('hardware'):
             hw_table = formatting.Table(server_columns)
             for hardware in vlan['hardware']:
                 hw_table.add_row([hardware['hostname'],
diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py
index 9b03ce82..abcd90c7 100644
--- a/SoftLayer/consts.py
+++ b/SoftLayer/consts.py
@@ -5,7 +5,7 @@
 
     :license: MIT, see LICENSE for more details.
 """
-VERSION = 'v5.0.0'
+VERSION = 'v5.0.1'
 API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/'
 API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/'
 API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/'
diff --git a/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py b/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py
index 9581d5b7..28e043bc 100644
--- a/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py
+++ b/SoftLayer/fixtures/SoftLayer_Network_ContentDelivery_Account.py
@@ -33,3 +33,5 @@
 loadContent = True
 
 purgeContent = True
+
+purgeCache = True
diff --git a/SoftLayer/fixtures/SoftLayer_Ticket.py b/SoftLayer/fixtures/SoftLayer_Ticket.py
index 4933cc62..c85deda8 100644
--- a/SoftLayer/fixtures/SoftLayer_Ticket.py
+++ b/SoftLayer/fixtures/SoftLayer_Ticket.py
@@ -33,3 +33,20 @@
 }
 edit = True
 addUpdate = {}
+
+addAttachedHardware = {
+    "id": 123,
+    "createDate": "2013-08-01T14:14:04-07:00",
+    "hardwareId": 1,
+    "ticketId": 100
+}
+
+addAttachedVirtualGuest = {
+    "id": 123,
+    "createDate": "2013-08-01T14:14:04-07:00",
+    "virtualGuestId": 1,
+    "ticketId": 100
+}
+
+removeAttachedHardware = True
+removeAttachedVirtualGuest = True
diff --git a/SoftLayer/managers/cdn.py b/SoftLayer/managers/cdn.py
index a805ed39..d93f148a 100644
--- a/SoftLayer/managers/cdn.py
+++ b/SoftLayer/managers/cdn.py
@@ -131,8 +131,8 @@ def purge_content(self, account_id, urls):
             urls = [urls]
 
         for i in range(0, len(urls), MAX_URLS_PER_PURGE):
-            result = self.account.purgeContent(urls[i:i + MAX_URLS_PER_PURGE],
-                                               id=account_id)
+            result = self.account.purgeCache(urls[i:i + MAX_URLS_PER_PURGE],
+                                             id=account_id)
             if not result:
                 return result
 
diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py
index 0b4fc381..50f1121b 100644
--- a/SoftLayer/managers/network.py
+++ b/SoftLayer/managers/network.py
@@ -23,6 +23,15 @@
     'virtualGuestCount',
     'networkSpace',
 ])
+DEFAULT_GET_VLAN_MASK = ','.join([
+    'firewallInterfaces',
+    'primaryRouter[id, fullyQualifiedDomainName, datacenter]',
+    'totalPrimaryIpAddressCount',
+    'networkSpace',
+    'hardware',
+    'subnets',
+    'virtualGuests',
+])
 
 
 class NetworkManager(object):
@@ -207,7 +216,7 @@ def get_vlan(self, vlan_id):
                   the specified VLAN.
 
         """
-        return self.vlan.getObject(id=vlan_id, mask=DEFAULT_VLAN_MASK)
+        return self.vlan.getObject(id=vlan_id, mask=DEFAULT_GET_VLAN_MASK)
 
     def list_global_ips(self, version=None, identifier=None, **kwargs):
         """Returns a list of all global IP address records on the account.
diff --git a/SoftLayer/managers/ticket.py b/SoftLayer/managers/ticket.py
index dd1be176..eb5a12a6 100644
--- a/SoftLayer/managers/ticket.py
+++ b/SoftLayer/managers/ticket.py
@@ -39,13 +39,13 @@ def list_tickets(self, open_status=True, closed_status=True):
         return self.client.call('Account', call, mask=mask)
 
     def list_subjects(self):
-        """List all tickets."""
+        """List all ticket subjects."""
         return self.client['Ticket_Subject'].getAllObjects()
 
     def get_ticket(self, ticket_id):
         """Get details about a ticket.
 
-        :param integer id: the ticket ID
+        :param integer ticket_id: the ticket ID
         :returns: A dictionary containing a large amount of information about
                   the specified ticket.
 
@@ -79,3 +79,39 @@ def update_ticket(self, ticket_id=None, body=None):
         :param string body: entry to update in the ticket
         """
         return self.ticket.addUpdate({'entry': body}, id=ticket_id)
+
+    def attach_hardware(self, ticket_id=None, hardware_id=None):
+        """Attach hardware to a ticket.
+
+        :param integer ticket_id: the id of the ticket to attach to
+        :param integer hardware_id: the id of the hardware to attach
+        :returns The new ticket attachment
+        """
+        return self.ticket.addAttachedHardware(hardware_id, id=ticket_id)
+
+    def attach_virtual_server(self, ticket_id=None, virtual_id=None):
+        """Attach a virtual server to a ticket.
+
+        :param integer ticket_id: the id of the ticket to attach to
+        :param integer virtual_id: the id of the virtual server to attach
+        :returns The new ticket attachment
+        """
+        return self.ticket.addAttachedVirtualGuest(virtual_id, id=ticket_id)
+
+    def detach_hardware(self, ticket_id=None, hardware_id=None):
+        """Detach hardware from a ticket.
+
+        :param ticket_id: the id of the ticket to detach from
+        :param hardware_id: the id of the hardware to detach
+        :return: Whether the detachment was successful
+        """
+        return self.ticket.removeAttachedHardware(hardware_id, id=ticket_id)
+
+    def detach_virtual_server(self, ticket_id=None, virtual_id=None):
+        """Detach a virtual server from a ticket.
+
+        :param ticket_id: the id of the ticket to detach from
+        :param virtual_id: the id of the virtual server to detach
+        :return: Whether the detachment was successful
+        """
+        return self.ticket.removeAttachedVirtualGuest(virtual_id, id=ticket_id)
diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py
index 03546178..f39ffc0f 100644
--- a/SoftLayer/utils.py
+++ b/SoftLayer/utils.py
@@ -59,13 +59,8 @@ def to_dict(self):
 
         This is needed for places where strict type checking is done.
         """
-        new_dict = {}
-        for key, val in self.items():
-            if isinstance(val, NestedDict):
-                new_dict[key] = val.to_dict()
-            else:
-                new_dict[key] = val
-        return new_dict
+        return {key: val.to_dict() if isinstance(val, NestedDict) else val
+                for key, val in self.items()}
 
 
 def query_filter(query):
diff --git a/docs/conf.py b/docs/conf.py
index df161220..cb63c4da 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -55,9 +55,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = '5.0.0'
+version = '5.0.1'
 # The full version, including alpha/beta/rc tags.
-release = '5.0.0'
+release = '5.0.1'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/fabfile.py b/fabfile.py
index 9badba0a..c864c537 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -36,14 +36,14 @@ def release(version, force=False):
 
     clean()
 
-    puts(" * Tagging Version %s" % version_str)
-    force_option = 'f' if force else ''
-    local("git tag -%sam \"%s\" %s" % (force_option, version_str, version_str))
-
     local("pip install wheel")
 
     puts(" * Uploading to PyPI")
     upload()
 
+    puts(" * Tagging Version %s" % version_str)
+    force_option = 'f' if force else ''
+    local("git tag -%sam \"%s\" %s" % (force_option, version_str, version_str))
+
     puts(" * Pushing Tag to upstream")
     local("git push upstream %s" % version_str)
diff --git a/setup.py b/setup.py
index ea0e4111..cce102aa 100644
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,7 @@
 
 setup(
     name='SoftLayer',
-    version='5.0.0',
+    version='5.0.1',
     description=DESCRIPTION,
     long_description=LONG_DESCRIPTION,
     author='SoftLayer Technologies, Inc.',
@@ -36,6 +36,7 @@
         'click >= 5',
         'requests >= 2.7.0',
         'prompt_toolkit >= 0.53',
+        'pygments >= 2.0.0',
     ],
     keywords=['softlayer', 'cloud'],
     classifiers=[
@@ -49,8 +50,8 @@
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
     ],


Test patch:
diff --git a/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py
index 5a579c64..6f54e5cc 100644
--- a/tests/CLI/modules/ticket_tests.py
+++ b/tests/CLI/modules/ticket_tests.py
@@ -4,9 +4,11 @@
 
     :license: MIT, see LICENSE for more details.
 """
-from SoftLayer import testing
-
 import json
+import mock
+
+from SoftLayer.CLI import exceptions
+from SoftLayer import testing
 
 
 class TicketTests(testing.TestCase):
@@ -38,3 +40,130 @@ def test_detail(self):
         }
         self.assertEqual(result.exit_code, 0)
         self.assertEqual(json.loads(result.output), expected)
+
+    def test_create(self):
+        result = self.run_command(['ticket', 'create', '--title=Test',
+                                   '--subject-id=1000',
+                                   '--body=ticket body'])
+
+        self.assertEqual(result.exit_code, 0)
+
+        args = ({'subjectId': 1000,
+                 'contents': 'ticket body',
+                 'assignedUserId': 12345,
+                 'title': 'Test'}, 'ticket body')
+
+        self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket',
+                                args=args)
+
+    def test_create_and_attach(self):
+        result = self.run_command(['ticket', 'create', '--title=Test',
+                                   '--subject-id=1000',
+                                   '--body=ticket body',
+                                   '--hardware=234',
+                                   '--virtual=567'])
+
+        self.assertEqual(result.exit_code, 0)
+
+        args = ({'subjectId': 1000,
+                 'contents': 'ticket body',
+                 'assignedUserId': 12345,
+                 'title': 'Test'}, 'ticket body')
+
+        self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket',
+                                args=args)
+        self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware',
+                                args=(234,),
+                                identifier=100)
+        self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest',
+                                args=(567,),
+                                identifier=100)
+
+    @mock.patch('click.edit')
+    def test_create_no_body(self, edit_mock):
+        edit_mock.return_value = 'ticket body'
+        result = self.run_command(['ticket', 'create', '--title=Test',
+                                   '--subject-id=1000'])
+        self.assertEqual(result.exit_code, 0)
+
+        args = ({'subjectId': 1000,
+                 'contents': 'ticket body',
+                 'assignedUserId': 12345,
+                 'title': 'Test'}, 'ticket body')
+
+        self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket',
+                                args=args)
+
+    def test_subjects(self):
+        list_expected_ids = [1001, 1002, 1003, 1004, 1005]
+        result = self.run_command(['ticket', 'subjects'])
+
+        self.assertEqual(result.exit_code, 0)
+        results = json.loads(result.output)
+        for result in results:
+            self.assertIn(result['id'], list_expected_ids)
+
+    def test_attach_no_identifier(self):
+        result = self.run_command(['ticket', 'attach', '1'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_attach_two_identifiers(self):
+        result = self.run_command(['ticket',
+                                   'attach',
+                                   '1',
+                                   '--hardware=100',
+                                   '--virtual=100'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_ticket_attach_hardware(self):
+        result = self.run_command(['ticket', 'attach', '1', '--hardware=100'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware',
+                                args=(100,),
+                                identifier=1)
+
+    def test_ticket_attach_virtual_server(self):
+        result = self.run_command(['ticket', 'attach', '1', '--virtual=100'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest',
+                                args=(100,),
+                                identifier=1)
+
+    def test_detach_no_identifier(self):
+        result = self.run_command(['ticket', 'detach', '1'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_detach_two_identifiers(self):
+        result = self.run_command(['ticket',
+                                   'detach',
+                                   '1',
+                                   '--hardware=100',
+                                   '--virtual=100'])
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_ticket_detach_hardware(self):
+        result = self.run_command(['ticket', 'detach', '1', '--hardware=100'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assert_called_with('SoftLayer_Ticket',
+                                'removeAttachedHardware',
+                                args=(100,),
+                                identifier=1)
+
+    def test_ticket_detach_virtual_server(self):
+        result = self.run_command(['ticket', 'detach', '1', '--virtual=100'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assert_called_with('SoftLayer_Ticket',
+                                'removeAttachedVirtualGuest',
+                                args=(100,),
+                                identifier=1)
diff --git a/tests/CLI/modules/vs_tests.py b/tests/CLI/modules/vs_tests.py
index 5b5ff810..573de5fe 100644
--- a/tests/CLI/modules/vs_tests.py
+++ b/tests/CLI/modules/vs_tests.py
@@ -134,6 +134,41 @@ def test_create(self, confirm_mock):
         self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject',
                                 args=args)
 
+    @mock.patch('SoftLayer.CLI.formatting.confirm')
+    def test_create_with_integer_image_id(self, confirm_mock):
+        confirm_mock.return_value = True
+        result = self.run_command(['vs', 'create',
+                                   '--cpu=2',
+                                   '--domain=example.com',
+                                   '--hostname=host',
+                                   '--image=12345',
+                                   '--memory=1',
+                                   '--network=100',
+                                   '--billing=hourly',
+                                   '--datacenter=dal05'])
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(json.loads(result.output),
+                         {'guid': '1a2b3c-1701',
+                          'id': 100,
+                          'created': '2013-08-01 15:23:45'})
+
+        args = ({
+            'datacenter': {'name': 'dal05'},
+            'domain': 'example.com',
+            'hourlyBillingFlag': True,
+            'localDiskFlag': True,
+            'maxMemory': 1024,
+            'hostname': 'host',
+            'startCpus': 2,
+            'blockDeviceTemplateGroup': {
+                'globalIdentifier': '0B5DEAF4-643D-46CA-A695-CECBE8832C9D',
+            },
+            'networkComponents': [{'maxSpeed': '100'}]
+        },)
+        self.assert_called_with('SoftLayer_Virtual_Guest', 'createObject',
+                                args=args)
+
     @mock.patch('SoftLayer.CLI.formatting.confirm')
     def test_dns_sync_both(self, confirm_mock):
         confirm_mock.return_value = True
diff --git a/tests/managers/cdn_tests.py b/tests/managers/cdn_tests.py
index e4c20f52..6f438776 100644
--- a/tests/managers/cdn_tests.py
+++ b/tests/managers/cdn_tests.py
@@ -105,7 +105,7 @@ def test_purge_content(self):
 
         self.cdn_client.purge_content(12345, urls)
         calls = self.calls('SoftLayer_Network_ContentDelivery_Account',
-                           'purgeContent')
+                           'purgeCache')
         self.assertEqual(len(calls),
                          math.ceil(len(urls) / float(cdn.MAX_URLS_PER_PURGE)))
 
@@ -115,12 +115,12 @@ def test_purge_content_failure(self):
                 'http://x/img/0x001.png']
 
         mock = self.set_mock('SoftLayer_Network_ContentDelivery_Account',
-                             'purgeContent')
+                             'purgeCache')
         mock.return_value = False
 
         self.cdn_client.purge_content(12345, urls)
         calls = self.calls('SoftLayer_Network_ContentDelivery_Account',
-                           'purgeContent')
+                           'purgeCache')
         self.assertEqual(len(calls),
                          math.ceil(len(urls) / float(cdn.MAX_URLS_PER_PURGE)))
 
@@ -129,6 +129,6 @@ def test_purge_content_single(self):
 
         self.cdn_client.purge_content(12345, url)
         self.assert_called_with('SoftLayer_Network_ContentDelivery_Account',
-                                'purgeContent',
+                                'purgeCache',
                                 args=([url],),
                                 identifier=12345)
diff --git a/tests/managers/ticket_tests.py b/tests/managers/ticket_tests.py
index aa75c982..2ebbfced 100644
--- a/tests/managers/ticket_tests.py
+++ b/tests/managers/ticket_tests.py
@@ -77,3 +77,28 @@ def test_update_ticket(self):
         self.assert_called_with('SoftLayer_Ticket', 'addUpdate',
                                 args=({'entry': 'Update1'},),
                                 identifier=100)
+
+    def test_attach_hardware(self):
+        self.ticket.attach_hardware(100, 123)
+        self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware',
+                                args=(123,),
+                                identifier=100)
+
+    def test_attach_virtual_server(self):
+        self.ticket.attach_virtual_server(100, 123)
+        self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest',
+                                args=(123,),
+                                identifier=100)
+
+    def test_detach_hardware(self):
+        self.ticket.detach_hardware(100, 123)
+        self.assert_called_with('SoftLayer_Ticket', 'removeAttachedHardware',
+                                args=(123,),
+                                identifier=100)
+
+    def test_detach_virtual_server(self):
+        self.ticket.detach_virtual_server(100, 123)
+        self.assert_called_with('SoftLayer_Ticket',
+                                'removeAttachedVirtualGuest',
+                                args=(123,),
+                                identifier=100)