Repository: softlayer/softlayer-python
Pull request #850 (2017-07-28 15:34:34)

Security groups
This change adds security groups CLIs. It also adds helpers for manipulating security groups to the network manager. Security groups can be added to a VSI at order time, so public and private security groups were added to the ordering helper.


Patch:
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index fe2bb12f..d729bf02 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -209,6 +209,23 @@
     ('hardware:credentials', 'SoftLayer.CLI.hardware.credentials:cli'),
     ('hardware:update-firmware', 'SoftLayer.CLI.hardware.update_firmware:cli'),
 
+    ('securitygroup', 'SoftLayer.CLI.securitygroup'),
+    ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'),
+    ('securitygroup:detail', 'SoftLayer.CLI.securitygroup.detail:cli'),
+    ('securitygroup:create', 'SoftLayer.CLI.securitygroup.create:cli'),
+    ('securitygroup:edit', 'SoftLayer.CLI.securitygroup.edit:cli'),
+    ('securitygroup:delete', 'SoftLayer.CLI.securitygroup.delete:cli'),
+    ('securitygroup:rule-list', 'SoftLayer.CLI.securitygroup.rule:rule_list'),
+    ('securitygroup:rule-add', 'SoftLayer.CLI.securitygroup.rule:add'),
+    ('securitygroup:rule-edit', 'SoftLayer.CLI.securitygroup.rule:edit'),
+    ('securitygroup:rule-remove', 'SoftLayer.CLI.securitygroup.rule:remove'),
+    ('securitygroup:interface-list',
+     'SoftLayer.CLI.securitygroup.interface:interface_list'),
+    ('securitygroup:interface-add',
+     'SoftLayer.CLI.securitygroup.interface:add'),
+    ('securitygroup:interface-remove',
+     'SoftLayer.CLI.securitygroup.interface:remove'),
+
     ('sshkey', 'SoftLayer.CLI.sshkey'),
     ('sshkey:add', 'SoftLayer.CLI.sshkey.add:cli'),
     ('sshkey:remove', 'SoftLayer.CLI.sshkey.remove:cli'),
@@ -256,6 +273,7 @@
     'lb': 'loadbal',
     'meta': 'metadata',
     'my': 'metadata',
+    'sg': 'securitygroup',
     'server': 'hardware',
     'vm': 'virtual',
     'vs': 'virtual',
diff --git a/SoftLayer/CLI/securitygroup/__init__.py b/SoftLayer/CLI/securitygroup/__init__.py
new file mode 100644
index 00000000..936e276c
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/__init__.py
@@ -0,0 +1,1 @@
+"""Network security groups."""
diff --git a/SoftLayer/CLI/securitygroup/create.py b/SoftLayer/CLI/securitygroup/create.py
new file mode 100644
index 00000000..5fc59947
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/create.py
@@ -0,0 +1,32 @@
+"""Create security groups."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+
+
+@click.command()
+@click.option('--name', '-n',
+              help="The name of the security group")
+@click.option('--description', '-d',
+              help="The description of the security group")
+@environment.pass_env
+def cli(env, name, description):
+    """Create a security group."""
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    result = mgr.create_securitygroup(name, description)
+    table = formatting.KeyValueTable(['name', 'value'])
+    table.align['name'] = 'r'
+    table.align['value'] = 'l'
+    table.add_row(['id', result['id']])
+    table.add_row(['name',
+                   result.get('name') or formatting.blank()])
+    table.add_row(['description',
+                   result.get('description') or formatting.blank()])
+    table.add_row(['created', result['createDate']])
+
+    env.fout(table)
diff --git a/SoftLayer/CLI/securitygroup/delete.py b/SoftLayer/CLI/securitygroup/delete.py
new file mode 100644
index 00000000..4cbca3e7
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/delete.py
@@ -0,0 +1,17 @@
+"""Delete a security group."""
+# :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('securitygroup_id')
+@environment.pass_env
+def cli(env, securitygroup_id):
+    """Deletes the given security group"""
+    mgr = SoftLayer.NetworkManager(env.client)
+    if not mgr.delete_securitygroup(securitygroup_id):
+        raise exceptions.CLIAbort("Failed to delete security group")
diff --git a/SoftLayer/CLI/securitygroup/detail.py b/SoftLayer/CLI/securitygroup/detail.py
new file mode 100644
index 00000000..4a3381ff
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/detail.py
@@ -0,0 +1,73 @@
+"""Get details about a security group."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+
+
+@click.command()
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Get details about a security group."""
+
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    secgroup = mgr.get_securitygroup(identifier)
+
+    table = formatting.KeyValueTable(['name', 'value'])
+    table.align['name'] = 'r'
+    table.align['value'] = 'l'
+
+    table.add_row(['id', secgroup['id']])
+    table.add_row(['name', secgroup.get('name') or formatting.blank()])
+    table.add_row(['description',
+                   secgroup.get('description') or formatting.blank()])
+
+    rule_table = formatting.Table(['id', 'remoteIp', 'remoteGroupId',
+                                   'direction', 'ethertype', 'portRangeMin',
+                                   'portRangeMax', 'protocol'])
+    for rule in secgroup.get('rules', []):
+        rg_id = rule.get('remoteGroup', {}).get('id') or formatting.blank()
+        port_min = rule.get('portRangeMin')
+        port_max = rule.get('portRangeMax')
+        if port_min is None:
+            port_min = formatting.blank()
+        if port_max is None:
+            port_max = formatting.blank()
+        rule_table.add_row([rule['id'],
+                            rule.get('remoteIp') or formatting.blank(),
+                            rule.get('remoteGroupId', rg_id),
+                            rule['direction'],
+                            rule.get('ethertype') or formatting.blank(),
+                            port_min,
+                            port_max,
+                            rule.get('protocol') or formatting.blank()])
+
+    table.add_row(['rules', rule_table])
+
+    vsi_table = formatting.Table(['id', 'hostname', 'interface', 'ipAddress'])
+
+    for binding in secgroup.get('networkComponentBindings', []):
+        try:
+            vsi = binding['networkComponent']['guest']
+            vsi_id = vsi['id']
+            hostname = vsi['hostname']
+            interface = ('PRIVATE' if binding['networkComponent']['port'] == 0
+                         else 'PUBLIC')
+            ip_address = (vsi['primaryBackendIpAddress']
+                          if binding['networkComponent']['port'] == 0
+                          else vsi['primaryIpAddress'])
+        except KeyError:
+            vsi_id = "N/A"
+            hostname = "Not enough permission to view"
+            interface = "N/A"
+            ip_address = "N/A"
+        vsi_table.add_row([vsi_id, hostname, interface, ip_address])
+
+    table.add_row(['servers', vsi_table])
+
+    env.fout(table)
diff --git a/SoftLayer/CLI/securitygroup/edit.py b/SoftLayer/CLI/securitygroup/edit.py
new file mode 100644
index 00000000..a8a49c67
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/edit.py
@@ -0,0 +1,28 @@
+"""Edit details of a security group."""
+# :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('group_id')
+@click.option('--name', '-n',
+              help="The name of the security group")
+@click.option('--description', '-d',
+              help="The description of the security group")
+@environment.pass_env
+def cli(env, group_id, name, description):
+    """Edit details of a security group."""
+    mgr = SoftLayer.NetworkManager(env.client)
+    data = {}
+    if name:
+        data['name'] = name
+    if description:
+        data['description'] = description
+
+    if not mgr.edit_securitygroup(group_id, **data):
+        raise exceptions.CLIAbort("Failed to edit security group")
diff --git a/SoftLayer/CLI/securitygroup/interface.py b/SoftLayer/CLI/securitygroup/interface.py
new file mode 100644
index 00000000..a2f33ee8
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/interface.py
@@ -0,0 +1,150 @@
+"""Security group interface operations."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import formatting
+
+COLUMNS = ['networkComponentId',
+           'virtualServerId',
+           'hostname',
+           'interface',
+           'ipAddress', ]
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.option('--sortby',
+              help='Column to sort by',
+              type=click.Choice(COLUMNS))
+@environment.pass_env
+def interface_list(env, securitygroup_id, sortby):
+    """List interfaces associated with security groups."""
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    table = formatting.Table(COLUMNS)
+    table.sortby = sortby
+
+    mask = (
+        '''networkComponentBindings[
+            networkComponentId,
+            networkComponent[
+                id,
+                port,
+                guest[
+                    id,
+                    hostname,
+                    primaryBackendIpAddress,
+                    primaryIpAddress
+                ]
+            ]
+        ]'''
+    )
+
+    secgroup = mgr.get_securitygroup(securitygroup_id, mask=mask)
+    for binding in secgroup.get('networkComponentBindings', []):
+        interface_id = binding['networkComponentId']
+        try:
+            interface = binding['networkComponent']
+            vsi = interface['guest']
+            vsi_id = vsi['id']
+            hostname = vsi['hostname']
+            priv_pub = 'PRIVATE' if interface['port'] == 0 else 'PUBLIC'
+            ip_address = (vsi['primaryBackendIpAddress']
+                          if interface['port'] == 0
+                          else vsi['primaryIpAddress'])
+        except KeyError:
+            vsi_id = "N/A"
+            hostname = "Not enough permission to view"
+            priv_pub = "N/A"
+            ip_address = "N/A"
+
+        table.add_row([
+            interface_id,
+            vsi_id,
+            hostname,
+            priv_pub,
+            ip_address
+        ])
+
+    env.fout(table)
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.option('--network-component', '-n',
+              help=('The network component to associate '
+                    'with the security group'))
+@click.option('--server', '-s',
+              help='The server ID to associate with the security group')
+@click.option('--interface', '-i',
+              help='The interface of the server to associate (public/private)')
+@environment.pass_env
+def add(env, securitygroup_id, network_component, server, interface):
+    """Attach an interface to a security group."""
+    _validate_args(network_component, server, interface)
+
+    mgr = SoftLayer.NetworkManager(env.client)
+    component_id = _get_component_id(env, network_component, server, interface)
+
+    success = mgr.attach_securitygroup_component(securitygroup_id,
+                                                 component_id)
+    if not success:
+        raise exceptions.CLIAbort("Could not attach network component")
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.option('--network-component', '-n',
+              help=('The network component to remove from '
+                    'with the security group'))
+@click.option('--server', '-s',
+              help='The server ID to remove from the security group')
+@click.option('--interface', '-i',
+              help='The interface of the server to remove (public/private)')
+@environment.pass_env
+def remove(env, securitygroup_id, network_component, server, interface):
+    """Detach an interface from a security group."""
+    _validate_args(network_component, server, interface)
+
+    mgr = SoftLayer.NetworkManager(env.client)
+    component_id = _get_component_id(env, network_component, server, interface)
+
+    success = mgr.detach_securitygroup_component(securitygroup_id,
+                                                 component_id)
+    if not success:
+        raise exceptions.CLIAbort("Could not detach network component")
+
+
+def _validate_args(network_component, server, interface):
+    use_server = bool(server and interface and not network_component)
+    use_component = bool(network_component and not bool(server or interface))
+
+    if not use_server and not use_component:
+        raise exceptions.CLIAbort("Must set either --network-component "
+                                  "or both --server and --interface")
+    if use_server and interface.lower() not in ['public', 'private']:
+        raise exceptions.CLIAbort(
+            "Interface must be either 'public' or 'private'")
+
+
+def _get_component_id(env, network_component, server, interface):
+    use_server = bool(server and interface and not network_component)
+    vs_mgr = SoftLayer.VSManager(env.client)
+
+    if use_server:
+        vs_mask = 'networkComponents[id, port]'
+        vsi = vs_mgr.get_instance(server, mask=vs_mask)
+        port = 0 if interface.lower() == 'private' else 1
+        component = [c for c in vsi['networkComponents'] if c['port'] == port]
+        if len(component) != 1:
+            raise exceptions.CLIAbort("Instance %s has no %s interface"
+                                      % (server, interface))
+        component_id = component[0]['id']
+    else:
+        component_id = network_component
+
+    return component_id
diff --git a/SoftLayer/CLI/securitygroup/list.py b/SoftLayer/CLI/securitygroup/list.py
new file mode 100644
index 00000000..0ba9ba1d
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/list.py
@@ -0,0 +1,36 @@
+"""List securitygroups."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import formatting
+
+COLUMNS = ['id',
+           'name',
+           'description', ]
+
+
+@click.command()
+@click.option('--sortby',
+              help='Column to sort by',
+              type=click.Choice(COLUMNS))
+@environment.pass_env
+def cli(env, sortby):
+    """List security groups."""
+
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    table = formatting.Table(COLUMNS)
+    table.sortby = sortby
+
+    sgs = mgr.list_securitygroups()
+    for secgroup in sgs:
+        table.add_row([
+            secgroup['id'],
+            secgroup.get('name') or formatting.blank(),
+            secgroup.get('description') or formatting.blank(),
+        ])
+
+    env.fout(table)
diff --git a/SoftLayer/CLI/securitygroup/rule.py b/SoftLayer/CLI/securitygroup/rule.py
new file mode 100644
index 00000000..dfbc93ed
--- /dev/null
+++ b/SoftLayer/CLI/securitygroup/rule.py
@@ -0,0 +1,140 @@
+"""Manage security group rules."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+from SoftLayer.CLI import exceptions
+from SoftLayer.CLI import formatting
+
+COLUMNS = ['id',
+           'remoteIp',
+           'remoteGroupId',
+           'direction',
+           'ethertype',
+           'portRangeMin',
+           'portRangeMax',
+           'protocol']
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.option('--sortby',
+              help='Column to sort by',
+              type=click.Choice(COLUMNS))
+@environment.pass_env
+def rule_list(env, securitygroup_id, sortby):
+    """List security group rules."""
+
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    table = formatting.Table(COLUMNS)
+    table.sortby = sortby
+
+    rules = mgr.list_securitygroup_rules(securitygroup_id)
+    for rule in rules:
+        port_min = rule.get('portRangeMin')
+        port_max = rule.get('portRangeMax')
+        if port_min is None:
+            port_min = formatting.blank()
+        if port_max is None:
+            port_max = formatting.blank()
+
+        table.add_row([
+            rule['id'],
+            rule.get('remoteIp') or formatting.blank(),
+            rule.get('remoteGroupId') or formatting.blank(),
+            rule['direction'],
+            rule.get('ethertype') or formatting.blank(),
+            port_min,
+            port_max,
+            rule.get('protocol') or formatting.blank()
+        ])
+
+    env.fout(table)
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.option('--remote-ip', '-r',
+              help='The remote IP/CIDR to enforce')
+@click.option('--remote-group', '-s', type=click.INT,
+              help='The ID of the remote security group to enforce')
+@click.option('--direction', '-d',
+              help=('The direction of traffic to enforce '
+                    '(ingress, egress)'))
+@click.option('--ethertype', '-e',
+              help='The ethertype (IPv4 or IPv6) to enforce')
+@click.option('--port-max', '-M', type=click.INT,
+              help='The upper port bound to enforce')
+@click.option('--port-min', '-m', type=click.INT,
+              help='The lower port bound to enforce')
+@click.option('--protocol', '-p',
+              help='The protocol (icmp, tcp, udp) to enforce')
+@environment.pass_env
+def add(env, securitygroup_id, remote_ip, remote_group,
+        direction, ethertype, port_max, port_min, protocol):
+    """Add a security group rule to a security group."""
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    ret = mgr.add_securitygroup_rule(securitygroup_id, remote_ip, remote_group,
+                                     direction, ethertype, port_max,
+                                     port_min, protocol)
+
+    if not ret:
+        raise exceptions.CLIAbort("Failed to add security group rule")
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.argument('rule_id')
+@click.option('--remote-ip', '-r',
+              help='The remote IP/CIDR to enforce')
+@click.option('--remote-group', '-s',
+              help='The ID of the remote security group to enforce')
+@click.option('--direction', '-d',
+              help='The direction of traffic to enforce')
+@click.option('--ethertype', '-e',
+              help='The ethertype (IPv4 or IPv6) to enforce')
+@click.option('--port-max', '-M',
+              help='The upper port bound to enforce')
+@click.option('--port-min', '-m',
+              help='The lower port bound to enforce')
+@click.option('--protocol', '-p',
+              help='The protocol (icmp, tcp, udp) to enforce')
+@environment.pass_env
+def edit(env, securitygroup_id, rule_id, remote_ip, remote_group,
+         direction, ethertype, port_max, port_min, protocol):
+    """Edit a security group rule in a security group."""
+    mgr = SoftLayer.NetworkManager(env.client)
+
+    data = {}
+    if remote_ip:
+        data['remote_ip'] = remote_ip
+    if remote_group:
+        data['remote_group'] = remote_group
+    if direction:
+        data['direction'] = direction
+    if ethertype:
+        data['ethertype'] = ethertype
+    if port_max is not None:
+        data['port_max'] = port_max
+    if port_min is not None:
+        data['port_min'] = port_min
+    if protocol:
+        data['protocol'] = protocol
+
+    if not mgr.edit_securitygroup_rule(securitygroup_id, rule_id, **data):
+        raise exceptions.CLIAbort("Failed to edit security group rule")
+
+
+@click.command()
+@click.argument('securitygroup_id')
+@click.argument('rule_id')
+@environment.pass_env
+def remove(env, securitygroup_id, rule_id):
+    """Remove a rule from a security group."""
+    mgr = SoftLayer.NetworkManager(env.client)
+    if not mgr.remove_securitygroup_rule(securitygroup_id, rule_id):
+        raise exceptions.CLIAbort("Failed to remove security group rule")
diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py
index d764cfb0..a3fa28b2 100644
--- a/SoftLayer/CLI/virt/create.py
+++ b/SoftLayer/CLI/virt/create.py
@@ -119,6 +119,14 @@ def _parse_create_args(client, args):
     if args.get('vlan_private'):
         data['private_vlan'] = args['vlan_private']
 
+    if args.get('public_security_group'):
+        pub_groups = args.get('public_security_group')
+        data['public_security_groups'] = [group for group in pub_groups]
+
+    if args.get('private_security_group'):
+        priv_groups = args.get('private_security_group')
+        data['private_security_groups'] = [group for group in priv_groups]
+
     if args.get('tag'):
         data['tags'] = ','.join(args['tag'])
 
@@ -201,6 +209,14 @@ def _parse_create_args(client, args):
               help="The ID of the private VLAN on which you want the virtual "
                    "server placed",
               type=click.INT)
+@helpers.multi_option('--public-security-group',
+                      '-S',
+                      help=('Security group ID to associate with '
+                            'the public interface'))
+@helpers.multi_option('--private-security-group',
+                      '-s',
+                      help=('Security group ID to associate with '
+                            'the private interface'))
 @click.option('--wait',
               type=click.INT,
               help="Wait until VS is finished provisioning for up to X "
diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py
index 9403b1ab..baa2b855 100644
--- a/SoftLayer/CLI/virt/detail.py
+++ b/SoftLayer/CLI/virt/detail.py
@@ -77,6 +77,20 @@ def cli(env, identifier, passwords=False, price=False):
             vlan['networkSpace'], vlan['vlanNumber'], vlan['id']])
     table.add_row(['vlans', vlan_table])
 
+    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])
+
     if result.get('notes'):
         table.add_row(['notes', result['notes']])
 
diff --git a/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py b/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py
new file mode 100644
index 00000000..7e79560f
--- /dev/null
+++ b/SoftLayer/fixtures/SoftLayer_Network_SecurityGroup.py
@@ -0,0 +1,46 @@
+getAllObjects = [
+    {'id': 100,
+     'name': 'secgroup1',
+     'description': 'Securitygroup1'},
+    {'id': 104,
+     'name': 'secgroup2'},
+    {'id': 110}
+]
+
+getRules = [
+    {'id': 100,
+     'direction': 'egress',
+     'ethertype': 'IPv4'}
+]
+
+guest_dict = {'id': 5000,
+              'hostname': 'test',
+              'primaryBackendIpAddress': '10.3.4.5',
+              'primaryIpAddress': '169.23.123.43'}
+
+getObject = {
+    'id': 100,
+    'name': 'secgroup1',
+    'description': 'Securitygroup1',
+    'networkComponentBindings': [{'networkComponentId': 1000,
+                                  'networkComponent': {'id': 1000,
+                                                       'port': 0,
+                                                       'guest': guest_dict}},
+                                 {'networkComponentId': 1001,
+                                  'networkComponent': {'id': 1001,
+                                                       'port': 1,
+                                                       'guest': guest_dict}}],
+    'rules': getRules
+}
+
+createObjects = [{'id': 100,
+                  'name': 'secgroup1',
+                  'description': 'Securitygroup1',
+                  'createDate': '2017-05-05T12:44:43-06:00'}]
+editObjects = True
+deleteObjects = True
+addRules = True
+editRules = True
+removeRules = True
+attachNetworkComponents = True
+detachNetworkComponents = True
diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py
index 690da99b..0e44d7b3 100644
--- a/SoftLayer/managers/network.py
+++ b/SoftLayer/managers/network.py
@@ -49,6 +49,7 @@ def __init__(self, client):
         self.vlan = client['Network_Vlan']
         self.subnet = client['Network_Subnet']
         self.network_storage = self.client['Network_Storage']
+        self.security_group = self.client['Network_SecurityGroup']
 
     def add_global_ip(self, version=4, test_order=False):
         """Adds a global IP address to the account.
@@ -62,6 +63,47 @@ def add_global_ip(self, version=4, test_order=False):
         return self.add_subnet('global', version=version,
                                test_order=test_order)
 
+    def add_securitygroup_rule(self, group_id, remote_ip=None,
+                               remote_group=None, direction=None,
+                               ethertype=None, port_max=None,
+                               port_min=None, protocol=None):
+        """Add a rule to a security group
+
+        :param int group_id: The ID of the security group to add this rule to
+        :param str remote_ip: The remote IP or CIDR to enforce the rule on
+        :param int remote_group: The remote security group ID to enforce
+                                 the rule on
+        :param str direction: The direction to enforce (egress or ingress)
+        :param str ethertype: The ethertype to enforce (IPv4 or IPv6)
+        :param int port_max: The upper port bound to enforce
+        :param int port_min: The lower port bound to enforce
+        :param str protocol: The protocol to enforce (icmp, udp, tcp)
+        """
+        rule = {'direction': direction}
+        if ethertype is not None:
+            rule['ethertype'] = ethertype
+        if port_max is not None:
+            rule['portRangeMax'] = port_max
+        if port_min is not None:
+            rule['portRangeMin'] = port_min
+        if protocol is not None:
+            rule['protocol'] = protocol
+        if remote_ip is not None:
+            rule['remoteIp'] = remote_ip
+        if remote_group is not None:
+            rule['remoteGroupId'] = remote_group
+        return self.add_securitygroup_rules(group_id, [rule])
+
+    def add_securitygroup_rules(self, group_id, rules):
+        """Add rules to a security group
+
+        :param int group_id: The ID of the security group to add the rules to
+        :param list rules: The list of rule dictionaries to add
+        """
+        if not isinstance(rules, list):
+            raise TypeError("The rules provided must be a list of dictionaries")
+        return self.security_group.addRules(rules, id=group_id)
+
     def add_subnet(self, subnet_type, quantity=None, vlan_id=None, version=4,
                    test_order=False):
         """Orders a new subnet
@@ -135,6 +177,24 @@ def assign_global_ip(self, global_ip_id, target):
         return self.client['Network_Subnet_IpAddress_Global'].route(
             target, id=global_ip_id)
 
+    def attach_securitygroup_component(self, group_id, component_id):
+        """Attaches a network component to a security group.
+
+        :param int group_id: The ID of the security group
+        :param int component_id: The ID of the network component to attach
+        """
+        return self.attach_securitygroup_components(group_id,
+                                                    [component_id])
+
+    def attach_securitygroup_components(self, group_id, component_ids):
+        """Attaches network components to a security group.
+
+        :param int group_id: The ID of the security group
+        :param list component_ids: The IDs of the network components to attach
+        """
+        return self.security_group.attachNetworkComponents(component_ids,
+                                                           id=group_id)
+
     def cancel_global_ip(self, global_ip_id):
         """Cancels the specified global IP address.
 
@@ -158,6 +218,41 @@ def cancel_subnet(self, subnet_id):
         billing_id = subnet['billingItem']['id']
         return self.client['Billing_Item'].cancelService(id=billing_id)
 
+    def create_securitygroup(self, name=None, description=None):
+        """Creates a security group.
+
+        :param string name: The name of the security group
+        :param string description: The description of the security group
+        """
+
+        create_dict = {'name': name, 'description': description}
+        return self.security_group.createObjects([create_dict])[0]
+
+    def delete_securitygroup(self, group_id):
+        """Deletes the specified security group.
+
+        :param int group_id: The ID of the security group
+        """
+        delete_dict = {'id': group_id}
+        return self.security_group.deleteObjects([delete_dict])
+
+    def detach_securitygroup_component(self, group_id, component_id):
+        """Detaches a network component from a security group.
+
+        :param int group_id: The ID of the security group
+        :param int component_id: The ID of the component to detach
+        """
+        return self.detach_securitygroup_components(group_id, [component_id])
+
+    def detach_securitygroup_components(self, group_id, component_ids):
+        """Detaches network components from a security group.
+
+        :param int group_id: The ID of the security group
+        :param list component_ids: The IDs of the network components to detach
+        """
+        return self.security_group.detachNetworkComponents(component_ids,
+                                                           id=group_id)
+
     def edit_rwhois(self, abuse_email=None, address1=None, address2=None,
                     city=None, company_name=None, country=None,
                     first_name=None, last_name=None, postal_code=None,
@@ -186,6 +281,66 @@ def edit_rwhois(self, abuse_email=None, address1=None, address2=None,
 
         return True
 
+    def edit_securitygroup(self, group_id, name=None, description=None):
+        """Edit security group details.
+
+        :param int group_id: The ID of the security group
+        :param string name: The name of the security group
+        :param string description: The description of the security group
+        """
+        successful = False
+        obj = {}
+        if name:
+            obj['name'] = name
+        if description:
+            obj['description'] = description
+
+        if obj:
+            obj['id'] = group_id
+            successful = self.security_group.editObjects([obj])
+
+        return successful
+
+    def edit_securitygroup_rule(self, group_id, rule_id, remote_ip=None,
+                                remote_group=None, direction=None,
+                                ethertype=None, port_max=None,
+                                port_min=None, protocol=None):
+        """Edit a security group rule.
+
+        :param int group_id: The ID of the security group the rule belongs to
+        :param int rule_id: The ID of the rule to edit
+        :param str remote_ip: The remote IP or CIDR to enforce the rule on
+        :param int remote_group: The remote security group ID to enforce
+                                          the rule on
+        :param str direction: The direction to enforce (egress or ingress)
+        :param str ethertype: The ethertype to enforce (IPv4 or IPv6)
+        :param str port_max: The upper port bound to enforce
+        :param str port_min: The lower port bound to enforce
+        :param str protocol: The protocol to enforce (icmp, udp, tcp)
+        """
+        successful = False
+        obj = {}
+        if remote_ip:
+            obj['remoteIp'] = remote_ip
+        if remote_group:
+            obj['remoteGroupId'] = remote_group
+        if direction:
+            obj['direction'] = direction
+        if ethertype:
+            obj['ethertype'] = ethertype
+        if port_max:
+            obj['portRangeMax'] = port_max
+        if port_min:
+            obj['portRangeMin'] = port_min
+        if protocol:
+            obj['protocol'] = protocol
+
+        if obj:
+            obj['id'] = rule_id
+            successful = self.security_group.editRules([obj], id=group_id)
+
+        return successful
+
     def ip_lookup(self, ip_address):
         """Looks up an IP address and returns network information about it.
 
@@ -203,6 +358,36 @@ def get_rwhois(self):
         """
         return self.account.getRwhoisData()
 
+    def get_securitygroup(self, group_id, **kwargs):
+        """Returns the information about the given security group.
+
+        :param string id: The ID for the security group
+        :returns: A diction of information about the security group
+        """
+        if 'mask' not in kwargs:
+            kwargs['mask'] = (
+                'id,'
+                'name,'
+                'description,'
+                '''rules[id, remoteIp, remoteGroupId,
+                         direction, ethertype, portRangeMin,
+                         portRangeMax, protocol],'''
+                '''networkComponentBindings[
+                    networkComponent[
+                        id,
+                        port,
+                        guest[
+                            id,
+                            hostname,
+                            primaryBackendIpAddress,
+                            primaryIpAddress
+                        ]
+                    ]
+                ]'''
+            )
+
+        return self.security_group.getObject(id=group_id, **kwargs)
+
     def get_subnet(self, subnet_id, **kwargs):
         """Returns information about a single subnet.
 
@@ -330,6 +515,33 @@ def list_vlans(self, datacenter=None, vlan_number=None, name=None,
 
         return self.account.getNetworkVlans(**kwargs)
 
+    def list_securitygroups(self, **kwargs):
+        """List security groups."""
+        return self.security_group.getAllObjects(**kwargs)
+
+    def list_securitygroup_rules(self, group_id):
+        """List security group rules associated with a security group.
+
+        :param int group_id: The security group to list rules for
+        """
+        return self.security_group.getRules(id=group_id)
+
+    def remove_securitygroup_rule(self, group_id, rule_id):
+        """Remove a rule from a security group.
+
+        :param int group_id: The ID of the security group
+        :param int rule_id: The ID of the rule to remove
+        """
+        return self.remove_securitygroup_rules(group_id, [rule_id])
+
+    def remove_securitygroup_rules(self, group_id, rules):
+        """Remove rules from a security group.
+
+        :param int group_id: The ID of the security group
+        :param list rules: The list of IDs to remove
+        """
+        return self.security_group.removeRules(rules, id=group_id)
+
     def resolve_global_ip_ids(self, identifier):
         """Resolve global ip ids."""
         return utils.resolve_ids(identifier,
diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py
index 6bcb4b20..52804360 100644
--- a/SoftLayer/managers/vs.py
+++ b/SoftLayer/managers/vs.py
@@ -192,7 +192,9 @@ def get_instance(self, instance_id, **kwargs):
                 'primaryIpAddress,'
                 '''networkComponents[id, status, speed, maxSpeed, name,
                                      macAddress, primaryIpAddress, port,
-                                     primarySubnet],'''
+                                     primarySubnet,
+                                     securityGroupBindings[
+                                        securityGroup[id, name]]],'''
                 'lastKnownPowerState.name,'
                 'powerState,'
                 'status,'
@@ -296,7 +298,8 @@ def _generate_create_dict(
             datacenter=None, os_code=None, image_id=None,
             dedicated=False, public_vlan=None, private_vlan=None,
             userdata=None, nic_speed=None, disks=None, post_uri=None,
-            private=False, ssh_keys=None):
+            private=False, ssh_keys=None, public_security_groups=None,
+            private_security_groups=None):
         """Returns a dict appropriate to pass into Virtual_Guest::createObject
 
             See :func:`create_instance` for a list of available options.
@@ -348,6 +351,20 @@ def _generate_create_dict(
                 "primaryBackendNetworkComponent": {
                     "networkVlan": {"id": int(private_vlan)}}})
 
+        if public_security_groups:
+            secgroups = [{'securityGroup': {'id': int(sg)}}
+                         for sg in public_security_groups]
+            pnc = data.get('primaryNetworkComponent', {})
+            pnc['securityGroupBindings'] = secgroups
+            data.update({'primaryNetworkComponent': pnc})
+
+        if private_security_groups:
+            secgroups = [{'securityGroup': {'id': int(sg)}}
+                         for sg in private_security_groups]
+            pbnc = data.get('primaryBackendNetworkComponent', {})
+            pbnc['securityGroupBindings'] = secgroups
+            data.update({'primaryBackendNetworkComponent': pbnc})
+
         if userdata:
             data['userData'] = [{'value': userdata}]
 
@@ -504,7 +521,8 @@ def create_instance(self, **kwargs):
                 'disks': ('100','25'),
                 'local_disk': True,
                 'memory': 1024,
-                'tags': 'test, pleaseCancel'
+                'tags': 'test, pleaseCancel',
+                'public_security_groups': [12, 15]
             }
 
             vsi = mgr.create_instance(**new_vsi)
@@ -530,6 +548,10 @@ def create_instance(self, **kwargs):
                                incur a fee on your account.
         :param int public_vlan: The ID of the public VLAN on which you want
                                 this VS placed.
+        :param list public_security_groups: The list of security group IDs
+                                            to apply to the public interface
+        :param list private_security_groups: The list of security group IDs
+                                             to apply to the private interface
         :param int private_vlan: The ID of the private VLAN on which you want
                                  this VS placed.
         :param list disks: A list of disk capacities for this server.
@@ -573,7 +595,8 @@ def create_instances(self, config_list):
                 'disks': ('100','25'),
                 'local_disk': True,
                 'memory': 1024,
-                'tags': 'test, pleaseCancel'
+                'tags': 'test, pleaseCancel',
+                'public_security_groups': [12, 15]
             }
 
             # using .copy() so we can make changes to individual nodes
diff --git a/tox.ini b/tox.ini
index 8262ba83..62f1fe67 100644
--- a/tox.ini
+++ b/tox.ini
@@ -35,10 +35,11 @@ commands =
            -d locally-disabled \
            -d no-else-return \ 
            -d len-as-condition \
-           --max-args=20 \
+           --max-args=25 \
            --max-branches=20 \
-           --max-statements=60 \
+           --max-statements=65 \
            --min-public-methods=0 \
+           --max-public-methods=35 \
            --min-similarity-lines=30 
 
     # invalid-name - Fixtures don't follow proper naming conventions


Test patch:
diff --git a/tests/CLI/modules/securitygroup_tests.py b/tests/CLI/modules/securitygroup_tests.py
new file mode 100644
index 00000000..b234941d
--- /dev/null
+++ b/tests/CLI/modules/securitygroup_tests.py
@@ -0,0 +1,233 @@
+"""
+    SoftLayer.tests.CLI.modules.securitygroup_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    :license: MIT, see LICENSE for more details.
+"""
+import json
+
+from SoftLayer import testing
+
+
+class SecurityGroupTests(testing.TestCase):
+    def test_list_securitygroup(self):
+        result = self.run_command(['sg', 'list'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'getAllObjects')
+        self.assertEqual([{'id': 100,
+                           'name': 'secgroup1',
+                           'description': 'Securitygroup1'},
+                          {'id': 104,
+                           'name': 'secgroup2',
+                           'description': None},
+                          {'id': 110,
+                           'name': None,
+                           'description': None}],
+                         json.loads(result.output))
+
+    def test_securitygroup_detail(self):
+        result = self.run_command(['sg', 'detail', '100'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'getObject', identifier='100')
+
+        priv_server_dict = {'id': 5000, 'hostname': 'test',
+                            'interface': 'PRIVATE',
+                            'ipAddress': '10.3.4.5'}
+        pub_server_dict = {'id': 5000, 'hostname': 'test',
+                           'interface': 'PUBLIC',
+                           'ipAddress': '169.23.123.43'}
+        self.assertEqual({'id': 100,
+                          'name': 'secgroup1',
+                          'description': 'Securitygroup1',
+                          'rules': [{'id': 100,
+                                     'direction': 'egress',
+                                     'ethertype': 'IPv4',
+                                     'remoteIp': None,
+                                     'remoteGroupId': None,
+                                     'protocol': None,
+                                     'portRangeMin': None,
+                                     'portRangeMax': None}],
+                          'servers': [priv_server_dict,
+                                      pub_server_dict]},
+                         json.loads(result.output))
+
+    def test_securitygroup_create(self):
+        result = self.run_command(['sg', 'create', '--name=secgroup1',
+                                   '--description=Securitygroup1'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'createObjects',
+                                args=([{'name': 'secgroup1',
+                                       'description': 'Securitygroup1'}],))
+        self.assertEqual({'id': 100,
+                          'name': 'secgroup1',
+                          'description': 'Securitygroup1',
+                          'created': '2017-05-05T12:44:43-06:00'},
+                         json.loads(result.output))
+
+    def test_securitygroup_edit(self):
+        result = self.run_command(['sg', 'edit', '104', '--name=foo'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'editObjects',
+                                args=([{'id': '104', 'name': 'foo'}],))
+
+    def test_securitygroup_edit_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup',
+                                'editObjects')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'edit', '100',
+                                   '--name=foo'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_securitygroup_delete(self):
+        result = self.run_command(['sg', 'delete', '104'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'deleteObjects',
+                                args=([{'id': '104'}],))
+
+    def test_securitygroup_delete_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup',
+                                'deleteObjects')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'delete', '100'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_securitygroup_rule_list(self):
+        result = self.run_command(['sg', 'rule-list', '100'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'getRules', identifier='100')
+        self.assertEqual([{'id': 100,
+                           'direction': 'egress',
+                           'ethertype': 'IPv4',
+                           'remoteIp': None,
+                           'remoteGroupId': None,
+                           'protocol': None,
+                           'portRangeMin': None,
+                           'portRangeMax': None}],
+                         json.loads(result.output))
+
+    def test_securitygroup_rule_add(self):
+        result = self.run_command(['sg', 'rule-add', '100',
+                                   '--direction=ingress'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup', 'addRules',
+                                identifier='100',
+                                args=([{'direction': 'ingress'}],))
+
+    def test_securitygroup_rule_add_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'addRules')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'rule-add', '100',
+                                   '--direction=ingress'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_securitygroup_rule_edit(self):
+        result = self.run_command(['sg', 'rule-edit', '100',
+                                   '520', '--direction=ingress'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'editRules', identifier='100',
+                                args=([{'id': '520',
+                                        'direction': 'ingress'}],))
+
+    def test_securitygroup_rule_edit_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup', 'editRules')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'rule-edit', '100',
+                                   '520', '--direction=ingress'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_securitygroup_rule_remove(self):
+        result = self.run_command(['sg', 'rule-remove', '100', '520'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'removeRules', identifier='100',
+                                args=(['520'],))
+
+    def test_securitygroup_rule_remove_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup',
+                                'removeRules')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'rule-remove', '100', '520'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_securitygroup_interface_list(self):
+        result = self.run_command(['sg', 'interface-list', '100'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'getObject', identifier='100')
+        self.assertEqual([{'hostname': 'test',
+                           'interface': 'PRIVATE',
+                           'ipAddress': '10.3.4.5',
+                           'networkComponentId': 1000,
+                           'virtualServerId': 5000},
+                          {'hostname': 'test',
+                           'interface': 'PUBLIC',
+                           'ipAddress': '169.23.123.43',
+                           'networkComponentId': 1001,
+                           'virtualServerId': 5000}],
+                         json.loads(result.output))
+
+    def test_securitygroup_interface_add(self):
+        result = self.run_command(['sg', 'interface-add', '100',
+                                   '--network-component=1000'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'attachNetworkComponents',
+                                identifier='100',
+                                args=(['1000'],))
+
+    def test_securitygroup_interface_add_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup',
+                                'attachNetworkComponents')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'interface-add', '100',
+                                   '--network-component=500'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_securitygroup_interface_remove(self):
+        result = self.run_command(['sg', 'interface-remove', '100',
+                                   '--network-component=500'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'detachNetworkComponents',
+                                identifier='100',
+                                args=(['500'],))
+
+    def test_securitygroup_interface_remove_fail(self):
+        fixture = self.set_mock('SoftLayer_Network_SecurityGroup',
+                                'detachNetworkComponents')
+        fixture.return_value = False
+
+        result = self.run_command(['sg', 'interface-remove', '100',
+                                   '--network-component=500'])
+
+        self.assertEqual(result.exit_code, 2)
diff --git a/tests/managers/network_tests.py b/tests/managers/network_tests.py
index b62a656a..47c76948 100644
--- a/tests/managers/network_tests.py
+++ b/tests/managers/network_tests.py
@@ -33,6 +33,49 @@ def test_add_global_ip(self):
 
         self.assertEqual(fixtures.SoftLayer_Product_Order.verifyOrder, result)
 
+    def test_add_securitygroup_rule(self):
+        result = self.network.add_securitygroup_rule(100,
+                                                     remote_ip='10.0.0.0/24',
+                                                     direction='ingress',
+                                                     ethertype='IPv4',
+                                                     port_min=95,
+                                                     port_max=100,
+                                                     protocol='tcp')
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'addRules', identifier=100,
+                                args=([{'remoteIp': '10.0.0.0/24',
+                                        'direction': 'ingress',
+                                        'ethertype': 'IPv4',
+                                        'portRangeMin': 95,
+                                        'portRangeMax': 100,
+                                        'protocol': 'tcp'}],))
+
+    def test_add_securitygroup_rules(self):
+        rule1 = {'remoteIp': '10.0.0.0/24',
+                 'direction': 'ingress',
+                 'ethertype': 'IPv4',
+                 'portRangeMin': 95,
+                 'portRangeMax': 100,
+                 'protocol': 'tcp'}
+        rule2 = {'remoteGroupId': 102,
+                 'direction': 'egress',
+                 'ethertype': 'IPv4'}
+
+        result = self.network.add_securitygroup_rules(100, [rule1, rule2])
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'addRules', identifier=100,
+                                args=([rule1, rule2],))
+
+    def test_add_securitygroup_rules_with_dict_error(self):
+        rule = {'remoteIp': '10.0.0.0/24',
+                'direction': 'ingress'}
+        self.assertRaises(TypeError, self.network.add_securitygroup_rules,
+                          rule)
+
     def test_add_subnet_for_ipv4(self):
         # Test a four public address IPv4 order
         result = self.network.add_subnet('public',
@@ -82,6 +125,24 @@ def test_assign_global_ip(self):
                                 identifier=9876,
                                 args=('172.16.24.76',))
 
+    def test_attach_securitygroup_component(self):
+        result = self.network.attach_securitygroup_component(100, 500)
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'attachNetworkComponents',
+                                identifier=100,
+                                args=([500],))
+
+    def test_attach_securitygroup_components(self):
+        result = self.network.attach_securitygroup_components(100, [500, 600])
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'attachNetworkComponents',
+                                identifier=100,
+                                args=([500, 600],))
+
     def test_cancel_global_ip(self):
         result = self.network.cancel_global_ip(1234)
 
@@ -96,6 +157,38 @@ def test_cancel_subnet(self):
         self.assert_called_with('SoftLayer_Billing_Item', 'cancelService',
                                 identifier=1056)
 
+    def test_create_securitygroup(self):
+        result = self.network.create_securitygroup(name='foo',
+                                                   description='bar')
+
+        sg_fixture = fixtures.SoftLayer_Network_SecurityGroup
+        self.assertEqual(sg_fixture.createObjects, [result])
+
+    def test_delete_securitygroup(self):
+        result = self.network.delete_securitygroup(100)
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'deleteObjects',
+                                args=([{'id': 100}],))
+
+    def test_detach_securitygroup_component(self):
+        result = self.network.detach_securitygroup_component(100, 500)
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'detachNetworkComponents',
+                                identifier=100, args=([500],))
+
+    def test_detach_securitygroup_components(self):
+        result = self.network.detach_securitygroup_components(100,
+                                                              [500, 600])
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'detachNetworkComponents',
+                                identifier=100, args=([500, 600],))
+
     def test_edit_rwhois(self):
         result = self.network.edit_rwhois(
             abuse_email='abuse@test.foo',
@@ -129,12 +222,37 @@ def test_edit_rwhois(self):
                                 identifier='id',
                                 args=(expected,))
 
+    def test_edit_securitygroup(self):
+        result = self.network.edit_securitygroup(100, name='foobar')
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'editObjects',
+                                args=([{'id': 100,
+                                        'name': 'foobar'}],))
+
+    def test_edit_securitygroup_rule(self):
+        result = self.network.edit_securitygroup_rule(100, 500,
+                                                      direction='ingress')
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'editRules', identifier=100,
+                                args=([{'id': 500,
+                                        'direction': 'ingress'}],))
+
     def test_get_rwhois(self):
         result = self.network.get_rwhois()
 
         self.assertEqual(result, fixtures.SoftLayer_Account.getRwhoisData)
         self.assert_called_with('SoftLayer_Account', 'getRwhoisData')
 
+    def test_get_securitygroup(self):
+        result = self.network.get_securitygroup(100)
+
+        sg_fixture = fixtures.SoftLayer_Network_SecurityGroup
+        self.assertEqual(sg_fixture.getObject, result)
+
     def test_get_subnet(self):
         result = self.network.get_subnet(9876)
 
@@ -240,6 +358,36 @@ def test_list_vlans_with_filters(self):
         self.assert_called_with('SoftLayer_Account', 'getNetworkVlans',
                                 filter=_filter)
 
+    def test_list_securitygroups(self):
+        result = self.network.list_securitygroups()
+
+        sg_fixture = fixtures.SoftLayer_Network_SecurityGroup
+        self.assertEqual(sg_fixture.getAllObjects, result)
+
+    def test_list_securitygroup_rules(self):
+        result = self.network.list_securitygroup_rules(100)
+
+        sg_fixture = fixtures.SoftLayer_Network_SecurityGroup
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'getRules', identifier=100)
+        self.assertEqual(sg_fixture.getRules, result)
+
+    def test_remove_securitygroup_rule(self):
+        result = self.network.remove_securitygroup_rule(100, 500)
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'removeRules', identifier=100,
+                                args=([500],))
+
+    def test_remove_securitygroup_rules(self):
+        result = self.network.remove_securitygroup_rules(100, [500, 600])
+
+        self.assertTrue(result)
+        self.assert_called_with('SoftLayer_Network_SecurityGroup',
+                                'removeRules', identifier=100,
+                                args=([500, 600],))
+
     def test_summary_by_datacenter(self):
         result = self.network.summary_by_datacenter()