Repository: ansible/ansible

Issue:
Cannot create subinterfaces using nxos_interface
Cannot create sub-interface using nxos_interface

##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
nxos_interface

##### ANSIBLE VERSION
```
ansible 2.4.0.0
  config file = /home/johannlo/ansible/core/ansible.cfg
  configured module search path = [u'/home/johannlo/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/johannlo/venv-ansible2.4/local/lib/python2.7/site-packages/ansible
  executable location = /home/johannlo/venv-ansible2.4/bin/ansible
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
```

##### CONFIGURATION
```
[defaults]
hostfile = hosts
host_key_checking = False
```

##### OS / ENVIRONMENT
nxos.7.0.3.I7.1.bin
Ubuntu 16.04 LTS

##### SUMMARY
using nxos_interface or nxos_ip_interface, you cannot create a subinterface. The parser appears to drop the dot (see attached debug). 

Also, there appears to be NO WAY using standard nxos modules to add the dot1q encapsulation. I have resorted to doing all this manually using nxos_config as a workaround.

##### STEPS TO REPRODUCE
Play
```
  - name: set physical interfaces
    nxos_interface:
      interface: "{{ item.interface }}"
      admin_state: up
      mode: "{{ item.mode }}"
      provider: "{{ provider }}"
    with_items: "{{ physical_interfaces | default([])  }}"
```

Vars
```
physical_interfaces:
  - interface: Ethernet1/3.80
    mode: layer3
```

##### EXPECTED RESULTS
Create subinterface Ethernet1/3.80
Make it a routed interface and no shut

Also, there appears to be NO WAY using standard nxos modules to add the dot1q encapsulation. I have resorted to doing all this manually using nxos_config as a workaround.

##### ACTUAL RESULTS
See the "interface" keypair - the dot is dropped! 
```
failed: [dc1-core-01] (item={u'interface': u'Ethernet1/3.80', u'mode': u'layer3'}) => {
    "changed": false, 
    "failed": true, 
    "interface": "Ethernet1/380", 
    "invocation": {
        "module_args": {
            "admin_state": "up", 
            "description": null, 
            "fabric_forwarding_anycast_gateway": null, 
            "host": "dc1-core-01", 
            "interface": "Ethernet1/3.80", 
            "interface_type": null, 
            "ip_forward": null, 
            "mode": "layer3", 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "port": 80, 
            "provider": {
                "host": "dc1-core-01", 
                "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "port": 80, 
                "ssh_keyfile": null, 
                "timeout": 10, 
                "transport": "nxapi", 
                "use_ssl": false, 
                "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
                "validate_certs": false
            }, 
            "ssh_keyfile": null, 
            "state": "present", 
            "timeout": 10, 
            "transport": "nxapi", 
            "url_password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "url_username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "use_ssl": false, 
            "username": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "validate_certs": false
        }
    }, 
    "item": {
        "interface": "Ethernet1/3.80", 
        "mode": "layer3"
    }, 
    "msg": "Invalid Ethernet interface provided."
}
```



Discussion:
cc @GGabriele @jedelman8 @mikewiebe @privateip @rahushen @rcarrillocruz @trishnaguha
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@wintermute000 it is not a parser issue; rather sub-interface configuration is not supported with this module as of 2.4.0
Also, for a sub-interface, a parent needs to be routed interface.

I will work on getting the needed functionality, along with VLAN encapsulation on sub-interface.

Resolving patch:
diff --git a/lib/ansible/modules/network/nxos/nxos_interface.py b/lib/ansible/modules/network/nxos/nxos_interface.py
--- a/lib/ansible/modules/network/nxos/nxos_interface.py
+++ b/lib/ansible/modules/network/nxos/nxos_interface.py
@@ -160,7 +160,7 @@ def is_default_interface(interface, module):
 
     try:
         body = execute_show_command(command, module)[0]
-    except IndexError:
+    except (IndexError, TypeError) as e:
         body = ''
 
     if body:
@@ -276,7 +276,6 @@ def get_interface(intf, module):
         body = execute_show_command(command, module)[0]
     except IndexError:
         body = []
-
     if body:
         interface_table = body['TABLE_interface']['ROW_interface']
         if interface_table.get('eth_mode') == 'fex-fabric':
@@ -343,8 +342,6 @@ def get_interface(intf, module):
                 temp_dict['description'] = "None"
             interface.update(temp_dict)
 
-    interface['type'] = intf_type
-
     return interface
 
 
@@ -526,7 +523,6 @@ def get_proposed(existing, normalized_interface, args):
 
 
 def smart_existing(module, intf_type, normalized_interface):
-
     # 7K BUG MAY CAUSE THIS TO FAIL
 
     all_interfaces = get_interfaces_dict(module)
@@ -534,12 +530,13 @@ def smart_existing(module, intf_type, normalized_interface):
         existing = get_interface(normalized_interface, module)
         is_default = is_default_interface(normalized_interface, module)
     else:
-        if intf_type == 'ethernet':
-            module.fail_json(msg='Invalid Ethernet interface provided.',
-                             interface=normalized_interface)
-        elif intf_type in ['loopback', 'portchannel', 'svi', 'nve']:
+        if (intf_type in ['loopback', 'portchannel', 'svi', 'nve'] or
+                intf_type == 'ethernet' and "." in normalized_interface):
             existing = {}
             is_default = 'DNE'
+        elif intf_type == 'ethernet':
+            module.fail_json(msg='Invalid Ethernet interface provided.',
+                             interface=normalized_interface)
     return existing, is_default
 
 
@@ -554,7 +551,10 @@ def execute_show_command(command, module):
     }]
 
     body = run_commands(module, cmds)
-    return body
+    if body and "Invalid" in body[0]:
+        return []
+    else:
+        return body
 
 
 def flatten_list(command_lists):
@@ -627,9 +627,13 @@ def main():
             module.fail_json(msg='The ip_forward and '
                                  'fabric_forwarding_anycast_gateway features '
                                  ' are only available for SVIs.')
+
         args = dict(interface=interface, admin_state=admin_state,
                     description=description, mode=mode, ip_forward=ip_forward,
                     fabric_forwarding_anycast_gateway=fabric_forwarding_anycast_gateway)
+        if (normalized_interface.startswith('Eth') or normalized_interface.startswith('po'))\
+           and "." in normalized_interface:
+            args["mode"] = None
 
         if intf_type == 'unknown':
             module.fail_json(msg='unknown interface type found-1',
@@ -650,10 +654,12 @@ def main():
                 if is_default != 'DNE':
                     cmds = ['no interface {0}'.format(normalized_interface)]
                     commands.append(cmds)
-            elif intf_type in ['ethernet']:
-                if is_default is False:
+            elif intf_type in ['ethernet'] and is_default is False:
+                if "." in normalized_interface:
+                    cmds = ['no interface {0}'.format(normalized_interface)]
+                else:
                     cmds = ['default interface {0}'.format(normalized_interface)]
-                    commands.append(cmds)
+                commands.append(cmds)
         elif state == 'present':
             if not existing:
                 cmds = get_interface_config_commands(proposed, normalized_interface, existing)
diff --git a/lib/ansible/modules/network/nxos/nxos_ip_interface.py b/lib/ansible/modules/network/nxos/nxos_ip_interface.py
--- a/lib/ansible/modules/network/nxos/nxos_ip_interface.py
+++ b/lib/ansible/modules/network/nxos/nxos_ip_interface.py
@@ -54,6 +54,12 @@
             - Subnet mask for IPv4 or IPv6 Address in decimal format.
         required: false
         default: null
+    dot1q:
+        description:
+            - Configures IEEE 802.1Q VLAN encapsulation on the subinterface. The range is from 2 to 4093.
+        required: false
+        default: null
+        version_added: "2.5"
     tag:
         description:
             - Route tag for IPv4 or IPv6 Address in integer format.
@@ -105,6 +111,16 @@
     addr: 20.20.20.20
     mask: 24
 
+- name: Ensure ipv4 address is configured on sub-intf with dot1q encapsulation
+  nxos_ip_interface:
+    interface: Ethernet1/32.10
+    transport: nxapi
+    version: v4
+    state: present
+    dot1q: 10
+    addr: 20.20.20.20
+    mask: 24
+
 - name: Configure ipv4 address as secondary if needed
   nxos_ip_interface:
     interface: Ethernet1/32
@@ -322,6 +338,35 @@ def parse_unstructured_data(body, interface_name, version, module):
     return interface
 
 
+def parse_interface_data(body):
+    body = body[0]
+    splitted_body = body.split('\n')
+
+    for index in range(0, len(splitted_body) - 1):
+            if "Encapsulation 802.1Q" in splitted_body[index]:
+                regex = '(.+?ID\s(?P<dot1q>\d+).*)?'
+                match = re.match(regex, splitted_body[index])
+                if match:
+                    match_dict = match.groupdict()
+                    if match_dict['dot1q'] is not None:
+                        return int(match_dict['dot1q'])
+    return 0
+
+
+def get_dot1q_id(interface_name, module):
+
+    if "." not in interface_name:
+        return 0
+
+    command = 'show interface {0}'.format(interface_name)
+    try:
+        body = execute_show_command(command, module)
+        dot1q = parse_interface_data(body)
+        return dot1q
+    except KeyError:
+        return 0
+
+
 def get_ip_interface(interface_name, version, module):
     body = send_show_command(interface_name, version, module)
     interface = parse_unstructured_data(body, interface_name, version, module)
@@ -329,7 +374,7 @@ def get_ip_interface(interface_name, version, module):
 
 
 def get_remove_ip_config_commands(interface, addr, mask, existing, version):
-    commands = ['interface {0}'.format(interface)]
+    commands = []
     if version == 'v4':
         # We can't just remove primary address if secondary address exists
         for address in existing['addresses']:
@@ -398,10 +443,6 @@ def get_config_ip_commands(delta, interface, existing, version):
         commands += get_remove_ip_config_commands(interface, delta['addr'], delta['mask'], existing, version)
 
     commands.append(command)
-
-    if commands[0] != 'interface {0}'.format(interface):
-        commands.insert(0, 'interface {0}'.format(interface))
-
     return commands
 
 
@@ -415,7 +456,7 @@ def flatten_list(command_lists):
     return flat_command_list
 
 
-def validate_params(addr, interface, mask, tag, allow_secondary, version, state, intf_type, module):
+def validate_params(addr, interface, mask, dot1q, tag, allow_secondary, version, state, intf_type, module):
     if state == "present":
         if addr is None or mask is None:
             module.fail_json(msg="An IP address AND a mask must be provided "
@@ -446,6 +487,13 @@ def validate_params(addr, interface, mask, tag, allow_secondary, version, state,
         except ValueError:
             module.fail_json(msg="Warning! Invalid ip address or mask set.", addr=addr, mask=mask)
 
+    if dot1q is not None:
+        try:
+            if 2 > dot1q > 4093:
+                raise ValueError
+        except ValueError:
+            module.fail_json(msg="Warning! 'dot1q' must be an integer between"
+                                 " 2 and 4093", dot1q=dot1q)
     if tag is not None:
         try:
             if 0 > tag > 4294967295:
@@ -470,6 +518,7 @@ def main():
         version=dict(required=False, choices=['v4', 'v6'],
                      default='v4'),
         mask=dict(type='str', required=False),
+        dot1q=dict(required=False, default=0, type='int'),
         tag=dict(required=False, default=0, type='int'),
         state=dict(required=False, default='present',
                    choices=['present', 'absent']),
@@ -494,13 +543,14 @@ def main():
     addr = module.params['addr']
     version = module.params['version']
     mask = module.params['mask']
+    dot1q = module.params['dot1q']
     tag = module.params['tag']
     allow_secondary = module.params['allow_secondary']
     interface = module.params['interface'].lower()
     state = module.params['state']
 
     intf_type = get_interface_type(interface)
-    validate_params(addr, interface, mask, tag, allow_secondary, version, state, intf_type, module)
+    validate_params(addr, interface, mask, dot1q, tag, allow_secondary, version, state, intf_type, module)
 
     mode = get_interface_mode(interface, intf_type, module)
     if mode == 'layer2':
@@ -509,21 +559,35 @@ def main():
 
     existing = get_ip_interface(interface, version, module)
 
-    args = dict(addr=addr, mask=mask, tag=tag, interface=interface, allow_secondary=allow_secondary)
+    dot1q_tag = get_dot1q_id(interface, module)
+    if dot1q_tag > 1:
+        existing['dot1q'] = dot1q_tag
+
+    args = dict(addr=addr, mask=mask, dot1q=dot1q, tag=tag, interface=interface, allow_secondary=allow_secondary)
     proposed = dict((k, v) for k, v in args.items() if v is not None)
     commands = []
     changed = False
     end_state = existing
 
-    if state == 'absent' and existing['addresses']:
-        if find_same_addr(existing, addr, mask):
-            command = get_remove_ip_config_commands(interface, addr,
-                                                    mask, existing, version)
+    commands = ['interface {0}'.format(interface)]
+    if state == 'absent':
+        if existing['addresses']:
+            if find_same_addr(existing, addr, mask):
+                command = get_remove_ip_config_commands(interface, addr,
+                                                        mask, existing, version)
+                commands.append(command)
+        if 'dot1q' in existing and existing['dot1q'] > 1:
+            command = 'no encapsulation dot1Q {0}'.format(existing['dot1q'])
             commands.append(command)
     elif state == 'present':
         if not find_same_addr(existing, addr, mask, full=True, tag=tag, version=version):
             command = get_config_ip_commands(proposed, interface, existing, version)
             commands.append(command)
+        if 'dot1q' not in existing and (intf_type in ['ethernet', 'portchannel'] and "." in interface):
+            command = 'encapsulation dot1Q {0}'.format(proposed['dot1q'])
+            commands.append(command)
+    if len(commands) < 2:
+        del commands[0]
     cmds = flatten_list(commands)
     if cmds:
         if module.check_mode:


Test patch:
diff --git a/test/units/modules/network/nxos/test_nxos_ip_interface.py b/test/units/modules/network/nxos/test_nxos_ip_interface.py
--- a/test/units/modules/network/nxos/test_nxos_ip_interface.py
+++ b/test/units/modules/network/nxos/test_nxos_ip_interface.py
@@ -58,7 +58,6 @@ def test_nxos_ip_interface_ip_present(self):
         self.assertEqual(result['commands'],
                          ['interface eth2/1',
                           'no ip address 1.1.1.1/8',
-                          'interface eth2/1',
                           'ip address 1.1.1.2/8'])
 
     def test_nxos_ip_interface_ip_idempotent(self):