diff --git a/lib/ansible/modules/network/slxos/slxos_linkagg.py b/lib/ansible/modules/network/slxos/slxos_linkagg.py
new file mode 100644
index 00000000000..a37cf438b96
--- /dev/null
+++ b/lib/ansible/modules/network/slxos/slxos_linkagg.py
@@ -0,0 +1,326 @@
+#!/usr/bin/python
+#
+# (c) 2018 Extreme Networks Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+#
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = """
+---
+module: slxos_linkagg
+version_added: "2.6"
+author: "Matthew Stone (@bigmstone)"
+short_description: Manage link aggregation groups on Extreme Networks SLXOS network devices
+description:
+ - This module provides declarative management of link aggregation groups
+ on Extreme Networks SLXOS network devices.
+notes:
+ - Tested against SLX-OS 17s.1.02
+options:
+ group:
+ description:
+ - Channel-group number for the port-channel
+ Link aggregation group. Range 1-1024.
+ mode:
+ description:
+ - Mode of the link aggregation group.
+ choices: ['active', 'on', 'passive']
+ members:
+ description:
+ - List of members of the link aggregation group.
+ aggregate:
+ description: List of link aggregation definitions.
+ state:
+ description:
+ - State of the link aggregation group.
+ default: present
+ choices: ['present', 'absent']
+ purge:
+ description:
+ - Purge links not defined in the I(aggregate) parameter.
+ type: bool
+"""
+
+EXAMPLES = """
+- name: create link aggregation group
+ slxos_linkagg:
+ group: 10
+ state: present
+
+- name: delete link aggregation group
+ slxos_linkagg:
+ group: 10
+ state: absent
+
+- name: set link aggregation group to members
+ slxos_linkagg:
+ group: 200
+ mode: active
+ members:
+ - Ethernet 0/1
+ - Ethernet 0/2
+
+- name: remove link aggregation group from Ethernet 0/1
+ slxos_linkagg:
+ group: 200
+ mode: active
+ members:
+ - Ethernet 0/1
+
+- name: Create aggregate of linkagg definitions
+ slxos_linkagg:
+ aggregate:
+ - { group: 3, mode: on, members: [Ethernet 0/1] }
+ - { group: 100, mode: passive, members: [Ethernet 0/2] }
+"""
+
+RETURN = """
+commands:
+ description: The list of configuration mode commands to send to the device
+ returned: always, except for the platforms that use Netconf transport to manage the device.
+ type: list
+ sample:
+ - interface port-channel 30
+ - interface Ethernet 0/3
+ - channel-group 30 mode on
+ - no interface port-channel 30
+"""
+
+import re
+from copy import deepcopy
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.network.common.config import CustomNetworkConfig
+from ansible.module_utils.network.common.utils import remove_default_spec
+from ansible.module_utils.network.slxos.slxos import get_config, load_config
+
+
+def search_obj_in_list(group, lst):
+ for o in lst:
+ if o['group'] == group:
+ return o
+
+
+def map_obj_to_commands(updates, module):
+ commands = list()
+ want, have = updates
+ purge = module.params['purge']
+
+ for w in want:
+ group = w['group']
+ mode = w['mode']
+ members = w.get('members') or []
+ state = w['state']
+ del w['state']
+
+ obj_in_have = search_obj_in_list(group, have)
+
+ if state == 'absent':
+ if obj_in_have:
+ commands.append('no interface port-channel {0}'.format(group))
+
+ elif state == 'present':
+ cmd = ['interface port-channel {0}'.format(group),
+ 'exit']
+ if not obj_in_have:
+ if not group:
+ module.fail_json(msg='group is a required option')
+ commands.extend(cmd)
+
+ if members:
+ for m in members:
+ commands.append('interface {0}'.format(m))
+ commands.append('channel-group {0} mode {1}'.format(group, mode))
+
+ else:
+ if members:
+ if 'members' not in obj_in_have.keys():
+ for m in members:
+ commands.extend(cmd)
+ commands.append('interface {0}'.format(m))
+ commands.append('channel-group {0} mode {1}'.format(group, mode))
+
+ elif set(members) != set(obj_in_have['members']):
+ missing_members = list(set(members) - set(obj_in_have['members']))
+ for m in missing_members:
+ commands.extend(cmd)
+ commands.append('interface {0}'.format(m))
+ commands.append('channel-group {0} mode {1}'.format(group, mode))
+
+ superfluous_members = list(set(obj_in_have['members']) - set(members))
+ for m in superfluous_members:
+ commands.extend(cmd)
+ commands.append('interface {0}'.format(m))
+ commands.append('no channel-group')
+
+ if purge:
+ for h in have:
+ obj_in_want = search_obj_in_list(h['group'], want)
+ if not obj_in_want:
+ commands.append('no interface port-channel {0}'.format(h['group']))
+
+ return commands
+
+
+def map_params_to_obj(module):
+ obj = []
+
+ aggregate = module.params.get('aggregate')
+ if aggregate:
+ for item in aggregate:
+ for key in item:
+ if item.get(key) is None:
+ item[key] = module.params[key]
+
+ d = item.copy()
+ d['group'] = str(d['group'])
+
+ obj.append(d)
+ else:
+ obj.append({
+ 'group': str(module.params['group']),
+ 'mode': module.params['mode'],
+ 'members': module.params['members'],
+ 'state': module.params['state']
+ })
+
+ return obj
+
+
+def parse_mode(module, config, group, member):
+ mode = None
+ netcfg = CustomNetworkConfig(indent=1, contents=config)
+ parents = ['interface {0}'.format(member)]
+ body = netcfg.get_section(parents)
+
+ match_int = re.findall(r'interface {0}\n'.format(member), body, re.M)
+ if match_int:
+ match = re.search(r'channel-group {0} mode (\S+)'.format(group), body, re.M)
+ if match:
+ mode = match.group(1)
+
+ return mode
+
+
+def parse_members(module, config, group):
+ members = []
+
+ for line in config.strip().split('!'):
+ l = line.strip()
+ if l.startswith('interface'):
+ match_group = re.findall(r'channel-group {0} mode'.format(group), l, re.M)
+ if match_group:
+ match = re.search(r'^interface (\S+\s\S+)$', l, re.M)
+ if match:
+ members.append(match.group(1))
+
+ return members
+
+
+def get_channel(module, config, group):
+ match = re.findall(r'^interface (\S+\s\S+)$', config, re.M)
+
+ if not match:
+ return {}
+
+ channel = {}
+ for item in set(match):
+ member = item
+ channel['mode'] = parse_mode(module, config, group, member)
+ channel['members'] = parse_members(module, config, group)
+
+ return channel
+
+
+def map_config_to_obj(module):
+ objs = list()
+ config = get_config(module)
+
+ for line in config.split('\n'):
+ l = line.strip()
+ match = re.search(r'interface Port-channel (\S+)', l, re.M)
+ if match:
+ obj = {}
+ group = match.group(1)
+ obj['group'] = group
+ obj.update(get_channel(module, config, group))
+ objs.append(obj)
+
+ return objs
+
+
+def main():
+ """ main entry point for module execution
+ """
+ element_spec = dict(
+ group=dict(type='int'),
+ mode=dict(choices=['active', 'on', 'passive']),
+ members=dict(type='list'),
+ state=dict(default='present',
+ choices=['present', 'absent'])
+ )
+
+ aggregate_spec = deepcopy(element_spec)
+ aggregate_spec['group'] = dict(required=True)
+
+ required_one_of = [['group', 'aggregate']]
+ required_together = [['members', 'mode']]
+ mutually_exclusive = [['group', 'aggregate']]
+
+ # remove default in aggregate spec, to handle common arguments
+ remove_default_spec(aggregate_spec)
+
+ argument_spec = dict(
+ aggregate=dict(type='list', elements='dict', options=aggregate_spec,
+ required_together=required_together),
+ purge=dict(default=False, type='bool')
+ )
+
+ argument_spec.update(element_spec)
+
+ module = AnsibleModule(argument_spec=argument_spec,
+ required_one_of=required_one_of,
+ required_together=required_together,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True)
+
+ warnings = list()
+ result = {'changed': False}
+ if warnings:
+ result['warnings'] = warnings
+
+ want = map_params_to_obj(module)
+ have = map_config_to_obj(module)
+
+ commands = map_obj_to_commands((want, have), module)
+ result['commands'] = commands
+
+ if commands:
+ if not module.check_mode:
+ load_config(module, commands)
+ result['changed'] = True
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/units/modules/network/slxos/fixtures/slxos_config_config.cfg b/test/units/modules/network/slxos/fixtures/slxos_config_config.cfg
index f767f782d77..824235d3dad 100644
--- a/test/units/modules/network/slxos/fixtures/slxos_config_config.cfg
+++ b/test/units/modules/network/slxos/fixtures/slxos_config_config.cfg
@@ -10,3 +10,13 @@ interface Ethernet 0/1
description test string
shutdown
!
+interface Ethernet 0/10
+ channel-group 20 mode active
+ description Channel Group Member
+!
+interface Ethernet 0/11
+ channel-group 20 mode active
+ description Channel Group Member
+!
+interface Port-channel 20
+!
diff --git a/test/units/modules/network/slxos/test_slxos_linkagg.py b/test/units/modules/network/slxos/test_slxos_linkagg.py
new file mode 100644
index 00000000000..166135eccb1
--- /dev/null
+++ b/test/units/modules/network/slxos/test_slxos_linkagg.py
@@ -0,0 +1,160 @@
+#
+# (c) 2018 Extreme Networks Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+#
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+import json
+
+from ansible.compat.tests.mock import patch
+from ansible.modules.network.slxos import slxos_linkagg
+from units.modules.utils import set_module_args
+from .slxos_module import TestSlxosModule, load_fixture
+
+
+class TestSlxosLinkaggModule(TestSlxosModule):
+ module = slxos_linkagg
+
+ def setUp(self):
+ super(TestSlxosLinkaggModule, self).setUp()
+ self._patch_get_config = patch(
+ 'ansible.modules.network.slxos.slxos_linkagg.get_config'
+ )
+ self._patch_load_config = patch(
+ 'ansible.modules.network.slxos.slxos_linkagg.load_config'
+ )
+
+ self._get_config = self._patch_get_config.start()
+ self._load_config = self._patch_load_config.start()
+
+ def tearDown(self):
+ super(TestSlxosLinkaggModule, self).tearDown()
+ self._patch_get_config.stop()
+ self._patch_load_config.stop()
+
+ def load_fixtures(self, commands=None):
+ config_file = 'slxos_config_config.cfg'
+ self._get_config.return_value = load_fixture(config_file)
+ self._load_config.return_value = None
+
+ def test_slxos_linkagg_group_present(self, *args, **kwargs):
+ set_module_args(dict(
+ group='10',
+ state='present'
+ ))
+ result = self.execute_module(changed=True)
+ self.assertEqual(
+ result,
+ {
+ 'commands': [
+ 'interface port-channel 10',
+ 'exit'
+ ],
+ 'changed': True
+ }
+ )
+
+ def test_slxos_linkagg_group_members_active(self, *args, **kwargs):
+ set_module_args(dict(
+ group='10',
+ mode='active',
+ members=[
+ 'Ethernet 0/1',
+ 'Ethernet 0/2'
+ ]
+ ))
+ result = self.execute_module(changed=True)
+ self.assertEqual(
+ result,
+ {
+ 'commands': [
+ 'interface port-channel 10',
+ 'exit',
+ 'interface Ethernet 0/1',
+ 'channel-group 10 mode active',
+ 'interface Ethernet 0/2',
+ 'channel-group 10 mode active'
+ ],
+ 'changed': True
+ }
+ )
+
+ def test_slxos_linkagg_group_member_removal(self, *args, **kwargs):
+ set_module_args(dict(
+ group='20',
+ mode='active',
+ members=[
+ 'Ethernet 0/10',
+ ]
+ ))
+ result = self.execute_module(changed=True)
+ self.assertEqual(
+ result,
+ {
+ 'commands': [
+ 'interface port-channel 20',
+ 'exit',
+ 'interface Ethernet 0/11',
+ 'no channel-group'
+ ],
+ 'changed': True
+ }
+ )
+
+ def test_slxos_linkagg_group_members_absent(self, *args, **kwargs):
+ set_module_args(dict(
+ group='20',
+ state='absent'
+ ))
+ result = self.execute_module(changed=True)
+ self.assertEqual(
+ result,
+ {
+ 'commands': [
+ 'no interface port-channel 20'
+ ],
+ 'changed': True
+ }
+ )
+ set_module_args(dict(
+ group='10',
+ state='absent'
+ ))
+ result = self.execute_module(changed=False)
+ self.assertEqual(
+ result,
+ {
+ 'commands': [],
+ 'changed': False
+ }
+ )
+
+ def test_slxos_linkagg_invalid_argument(self, *args, **kwargs):
+ set_module_args(dict(
+ group='10',
+ shawshank='Redemption'
+ ))
+ result = self.execute_module(failed=True)
+ self.assertEqual(result['failed'], True)
+ self.assertTrue(re.match(
+ r'Unsupported parameters for \((basic.pyc|basic.py)\) module: '
+ 'shawshank Supported parameters include: aggregate, group, '
+ 'members, mode, purge, state',
+ result['msg']
+ ))