diff --git a/lib/ansible/modules/network/ios/ios_vrf.py b/lib/ansible/modules/network/ios/ios_vrf.py
new file mode 100644
index 0000000000..d4ad21b446
--- /dev/null
+++ b/lib/ansible/modules/network/ios/ios_vrf.py
@@ -0,0 +1,361 @@
+#!/usr/bin/python
+#
+# 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 .
+#
+
+ANSIBLE_METADATA = {
+ 'status': ['preview'],
+ 'supported_by': 'core',
+ 'version': '1.0'
+}
+
+DOCUMENTATION = """
+---
+module: ios_vrf
+version_added: "2.3"
+author: "Peter Sprygada (@privateip)"
+short_description: Manage the collection of VRF definitions on IOS devices
+description:
+ - This module provides declarative management of VRF definitions on
+ Cisco IOS devices. It allows playbooks to manage individual or
+ the entire VRF collection. It also supports purging VRF definitions from
+ the configuration that are not explicitly defined.
+options:
+ vrfs:
+ description:
+ - The set of VRF definition objects to be configured on the remote
+ IOS device. Ths list entries can either be the VRF name or a hash
+ of VRF definitions and attributes. This argument is mutually
+ exclusive with the C(name) argument.
+ required: false
+ default: null
+ name:
+ description:
+ - The name of the VRF definition to be managed on the remote IOS
+ device. The VRF definition name is an ASCII string name used
+ to uniquely identify the VRF. This argument is mutually exclusive
+ with the C(vrfs) argument
+ required: false
+ default: null
+ description:
+ description:
+ - Provides a short description of the VRF definition in the
+ current active configuration. The VRF definition value accepts
+ alphanumberic characters used to provide additional information
+ about the VRF.
+ required: false
+ default: null
+ rd:
+ description:
+ - The router-distigusher value uniquely identifies the VRF to
+ routing processes on the remote IOS system. The RD value takes
+ the form of A:B where A and B are both numeric values.
+ required: false
+ default: null
+ interfaces:
+ description:
+ - The C(interfaces) argument identifies the set of interfaces that
+ should be configured in the VRF. Interfaces must be routed
+ interfaces in order to be placed into a VRF.
+ required: false
+ default: null
+ purge:
+ description:
+ - The C(purge) argument instructs the module to consider the
+ VRF definition absolute. It will remove any previously configured
+ VRFs on the device.
+ required: false
+ default: false
+ state:
+ description:
+ - The C(state) argument configures the state of the VRF definition
+ as it relates to the device operational configuration. When set
+ to I(present), the VRF should be configured in the device active
+ configuration and when set to I(absent) the VRF should not be
+ in the device active configuration
+ required: false
+ default: present
+ choices: ['present', 'absent']
+"""
+
+EXAMPLES = """
+- name: configure a vrf named management
+ ios_vrf:
+ name: management
+ description: oob mgmt vrf
+ interfaces:
+ - Management1
+
+- name: remove a vrf named test
+ ios_vrf:
+ name: test
+ state: absent
+
+- name: configure set of VRFs and purge any others
+ ios_vrf:
+ vrfs:
+ - red
+ - blue
+ - green
+ purge: yes
+"""
+
+RETURN = """
+commands:
+ description: The list of configuration mode commands to send to the device
+ returned: always
+ type: list
+ sample:
+ - vrf definition ansible
+ - description management vrf
+ - rd: 1:100
+start:
+ description: The time the job started
+ returned: always
+ type: str
+ sample: "2016-11-16 10:38:15.126146"
+end:
+ description: The time the job ended
+ returned: always
+ type: str
+ sample: "2016-11-16 10:38:25.595612"
+delta:
+ description: The time elapsed to perform all operations
+ returned: always
+ type: str
+ sample: "0:00:10.469466"
+"""
+import re
+
+from functools import partial
+
+from ansible.module_utils.local import LocalAnsibleModule
+from ansible.module_utils.ios import load_config, get_config
+from ansible.module_utils.netcfg import NetworkConfig
+from ansible.module_utils.six import iteritems
+
+
+def add_command_to_vrf(name, cmd, commands):
+ if 'vrf definition %s' % name not in commands:
+ commands.append('vrf definition %s' % name)
+ commands.append(cmd)
+
+def map_obj_to_commands(updates, module):
+ commands = list()
+ state = module.params['state']
+
+ for update in updates:
+ want, have = update
+
+ needs_update = lambda x: want.get(x) and (want.get(x) != have.get(x))
+
+ if want['state'] == 'absent':
+ commands.append('no vrf definition %s' % want['name'])
+ continue
+
+ if not have.get('state'):
+ commands.append('vrf definition %s' % want['name'])
+
+ if needs_update('description'):
+ cmd = 'description %s' % want['description']
+ add_command_to_vrf(want['name'], cmd, commands)
+
+ if needs_update('rd'):
+ cmd = 'rd %s' % want['rd']
+ add_command_to_vrf(want['name'], cmd, commands)
+
+ if want['interfaces'] is not None:
+ # handle the deletes
+ for intf in set(have.get('interfaces', [])).difference(want['interfaces']):
+ commands.extend(['interface %s' % intf,
+ 'no vrf forwarding %s' % want['name']])
+
+ # handle the adds
+ for intf in set(want['interfaces']).difference(have.get('interfaces', [])):
+ cfg = get_config(module)
+ configobj = NetworkConfig(indent=1, contents=cfg)
+ children = configobj['interface %s' % intf].children
+ intf_config = '\n'.join(children)
+
+ commands.extend(['interface %s' % intf,
+ 'vrf forwarding %s' % want['name']])
+
+ match = re.search('ip address .+', intf_config, re.M)
+ if match:
+ commands.append(match.group())
+
+ return commands
+
+def parse_description(configobj, name):
+ cfg = configobj['vrf definition %s' % name]
+ cfg = '\n'.join(cfg.children)
+ match = re.search(r'description (.+)$', cfg, re.M)
+ if match:
+ return match.group(1)
+
+def parse_rd(configobj, name):
+ cfg = configobj['vrf definition %s' % name]
+ cfg = '\n'.join(cfg.children)
+ match = re.search(r'rd (.+)$', cfg, re.M)
+ if match:
+ return match.group(1)
+
+def parse_interfaces(configobj, name):
+ vrf_cfg = 'vrf forwarding %s' % name
+ interfaces = list()
+
+ for intf in re.findall('^interface .+', str(configobj), re.M):
+ if vrf_cfg in '\n'.join(configobj[intf].children):
+ interfaces.append(intf.split(' ')[1])
+ return interfaces
+
+def map_config_to_obj(module):
+ config = get_config(module)
+ configobj = NetworkConfig(indent=1, contents=config)
+
+ match = re.findall(r'^vrf definition (\S+)', config, re.M)
+ if not match:
+ return list()
+
+ instances = list()
+
+ for item in set(match):
+ obj = {
+ 'name': item,
+ 'state': 'present',
+ 'description': parse_description(configobj, item),
+ 'rd': parse_rd(configobj, item),
+ 'interfaces': parse_interfaces(configobj, item)
+ }
+ instances.append(obj)
+ return instances
+
+
+def get_param_value(key, item, module):
+ # if key doesn't exist in the item, get it from module.params
+ if not item.get(key):
+ value = module.params[key]
+
+ # if key does exist, do a type check on it to validate it
+ else:
+ value_type = module.argument_spec[key].get('type', 'str')
+ type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type]
+ type_checker(item[key])
+ value = item[key]
+
+ # validate the param value (if validator func exists)
+ validator = globals().get('validate_%s' % key)
+ if validator:
+ validator(value, module)
+
+ return value
+
+def map_params_to_obj(module):
+ vrfs = module.params.get('vrfs')
+ if not vrfs:
+ if not module.params['name'] and module.params['purge']:
+ return list()
+ elif not module.params['name']:
+ module.fail_json(msg='name is required')
+ collection = [{'name': module.params['name']}]
+ else:
+ collection = list()
+ for item in vrfs:
+ if not isinstance(item, dict):
+ collection.append({'name': item})
+ elif 'name' not in item:
+ module.fail_json(msg='name is required')
+ else:
+ collection.append(item)
+
+ objects = list()
+
+ for item in collection:
+ get_value = partial(get_param_value, item=item, module=module)
+ item['description'] = get_value('description')
+ item['rd'] = get_value('rd')
+ item['interfaces'] = get_value('interfaces')
+ item['state'] = get_value('state')
+ objects.append(item)
+
+ return objects
+
+def update_objects(want, have):
+ updates = list()
+ for entry in want:
+ item = next((i for i in have if i['name'] == entry['name']), None)
+ if all((item is None, entry['state'] == 'present')):
+ updates.append((entry, {}))
+ else:
+ for key, value in iteritems(entry):
+ if value:
+ if isinstance(value, list):
+ if sorted(value) != sorted(item[key]):
+ if (entry, item) not in updates:
+ updates.append((entry, item))
+ elif value != item[key]:
+ if (entry, item) not in updates:
+ updates.append((entry, item))
+ return updates
+
+def main():
+ """ main entry point for module execution
+ """
+ argument_spec = dict(
+ vrfs=dict(type='list'),
+ name=dict(),
+
+ description=dict(),
+ rd=dict(),
+
+ interfaces=dict(type='list'),
+
+ purge=dict(type='bool', default=False),
+ state=dict(default='present', choices=['present', 'absent'])
+ )
+
+ mutually_exclusive = [('name', 'vrfs')]
+
+ module = LocalAnsibleModule(argument_spec=argument_spec,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True)
+
+ result = {'changed': False}
+
+ want = map_params_to_obj(module)
+ have = map_config_to_obj(module)
+
+ commands = map_obj_to_commands(update_objects(want,have), module)
+
+ if module.params['purge']:
+ want_vrfs = [x['name'] for x in want]
+ have_vrfs = [x['name'] for x in have]
+ for item in set(have_vrfs).difference(want_vrfs):
+ cmd = 'no vrf definition %s' % item
+ if cmd not in commands:
+ commands.append(cmd)
+
+ 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/ios/fixtures/ios_vrf_config.cfg b/test/units/modules/network/ios/fixtures/ios_vrf_config.cfg
new file mode 100644
index 0000000000..e09c2d1211
--- /dev/null
+++ b/test/units/modules/network/ios/fixtures/ios_vrf_config.cfg
@@ -0,0 +1,17 @@
+!
+vrf definition test_1
+ description test vrf 1
+ rd 1:100
+!
+vrf definition test_2
+ description test vrf 2
+!
+vrf definition test_3
+!
+interface Ethernet1
+ ip address 1.2.3.4/5
+!
+interface Ethernet2
+ ip address 1.2.3.4/5
+ vrf forwarding test_1
+!
diff --git a/test/units/modules/network/ios/test_ios_vrf.py b/test/units/modules/network/ios/test_ios_vrf.py
new file mode 100644
index 0000000000..5bc3ce67e9
--- /dev/null
+++ b/test/units/modules/network/ios/test_ios_vrf.py
@@ -0,0 +1,172 @@
+#
+# (c) 2016 Red Hat 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 .
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import json
+
+from ansible.compat.tests import unittest
+from ansible.compat.tests.mock import patch, MagicMock
+from ansible.errors import AnsibleModuleExit
+from ansible.modules.network.ios import ios_vrf
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+
+
+def set_module_args(args):
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+fixture_data = {}
+
+def load_fixture(name):
+ path = os.path.join(fixture_path, name)
+
+ if path in fixture_data:
+ return fixture_data[path]
+
+ with open(path) as f:
+ data = f.read()
+
+ try:
+ data = json.loads(data)
+ except:
+ pass
+
+ fixture_data[path] = data
+ return data
+
+
+class TestIosVrfModule(unittest.TestCase):
+
+ def setUp(self):
+ self.mock_get_config = patch('ansible.modules.network.ios.ios_vrf.get_config')
+ self.get_config = self.mock_get_config.start()
+
+ self.mock_load_config = patch('ansible.modules.network.ios.ios_vrf.load_config')
+ self.load_config = self.mock_load_config.start()
+
+ def tearDown(self):
+ self.mock_get_config.stop()
+ self.mock_load_config.stop()
+
+ def execute_module(self, failed=False, changed=False, commands=None, sort=True):
+
+ self.get_config.return_value = load_fixture('ios_vrf_config.cfg')
+ self.load_config.return_value = None
+
+ with self.assertRaises(AnsibleModuleExit) as exc:
+ ios_vrf.main()
+
+ result = exc.exception.result
+
+ if failed:
+ self.assertTrue(result['failed'], result)
+ else:
+ self.assertEqual(result.get('changed'), changed, result)
+
+ if commands:
+ if sort:
+ self.assertEqual(sorted(commands), sorted(result['commands']))
+ else:
+ self.assertEqual(commands, result['commands'], result['commands'])
+
+ return result
+
+ def test_ios_vrf_name(self):
+ set_module_args(dict(name='test_4'))
+ commands = ['vrf definition test_4']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+ def test_ios_vrf_name_unchanged(self):
+ set_module_args(dict(name='test_1', rd='1:100', description='test vrf 1'))
+ self.execute_module()
+
+ def test_ios_vrf_description(self):
+ set_module_args(dict(name='test_1', description='test string'))
+ commands = ['vrf definition test_1', 'description test string']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+ def test_ios_vrf_rd(self):
+ set_module_args(dict(name='test_1', rd='2:100'))
+ commands = ['vrf definition test_1', 'rd 2:100']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+ def test_ios_vrf_interfaces(self):
+ set_module_args(dict(name='test_1', interfaces=['Ethernet1']))
+ commands = ['interface Ethernet2', 'no vrf forwarding test_1',
+ 'interface Ethernet1', 'vrf forwarding test_1',
+ 'ip address 1.2.3.4/5']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+ def test_ios_vrf_state_absent(self):
+ set_module_args(dict(name='test_1', state='absent'))
+ commands = ['no vrf definition test_1']
+ self.execute_module(changed=True, commands=commands)
+
+ def test_ios_vrf_purge_all(self):
+ set_module_args(dict(purge=True))
+ commands = ['no vrf definition test_1', 'no vrf definition test_2',
+ 'no vrf definition test_3']
+ self.execute_module(changed=True, commands=commands)
+
+ def test_ios_vrf_purge_all_but_one(self):
+ set_module_args(dict(name='test_1', purge=True))
+ commands = ['no vrf definition test_2', 'no vrf definition test_3']
+ self.execute_module(changed=True, commands=commands)
+
+ def test_ios_vrfs_no_purge(self):
+ vrfs = [{'name': 'test_1'}, {'name': 'test_4'}]
+ set_module_args(dict(vrfs=vrfs))
+ commands = ['vrf definition test_4']
+ self.execute_module(changed=True, commands=commands)
+
+ def test_ios_vrfs_purge(self):
+ vrfs = [{'name': 'test_1'}, {'name': 'test_4'}]
+ set_module_args(dict(vrfs=vrfs, purge=True))
+ commands = ['no vrf definition test_2', 'no vrf definition test_3',
+ 'vrf definition test_4']
+ self.execute_module(changed=True, commands=commands)
+
+ def test_ios_vrfs_global_arg(self):
+ vrfs = [{'name': 'test_1'}, {'name': 'test_2'}]
+ set_module_args(dict(vrfs=vrfs, description='test string'))
+ commands = ['vrf definition test_1', 'description test string',
+ 'vrf definition test_2', 'description test string']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+ def test_ios_vrfs_local_override_description(self):
+ vrfs = [{'name': 'test_1', 'description': 'test vrf 1'},
+ {'name': 'test_2'}]
+ set_module_args(dict(vrfs=vrfs, description='test string'))
+ commands = ['vrf definition test_2', 'description test string']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+ def test_ios_vrfs_local_override_state(self):
+ vrfs = [{'name': 'test_1', 'state': 'absent'},
+ {'name': 'test_2'}]
+ set_module_args(dict(vrfs=vrfs, description='test string'))
+ commands = ['no vrf definition test_1', 'vrf definition test_2',
+ 'description test string']
+ self.execute_module(changed=True, commands=commands, sort=False)
+
+