Repository: ansible/ansible

Issue:
INI inventory regression: empty groups are no longer rejected
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
inventory/ini.py

##### ANSIBLE VERSION
stable-2.4 (based on code review)

##### CONFIGURATION
Default

##### OS / ENVIRONMENT
N/A

##### SUMMARY
With the inventory refactoring in 2.4, the ini module it no longer rejects empty groups.
In 1.9 empty groups was not checked. When I tested my playbooks with 2.4 I did not notice any errors. 
When testing with 2.3 (since thats what's available out-of-the-box on RHEL6), I noticed that empty groups are not supported.
Judging from code comments, it should still not be allowed:
 https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/inventory/ini.py#L180
##### STEPS TO REPRODUCE
Example hosts file:
```
[all:vars]
a=a

[test:vars]
a=b

[other]
myhost
```

##### EXPECTED RESULTS
Running ansible 2.3, targeting `myhost` , gives:
```
ERROR!  Attempted to read "hosts" as ini file: hosts:3: Section [test:vars] not valid for undefined group: test
```

##### ACTUAL RESULTS
Running the same on 2.4 does not fail. 


My guess of the problem:

 https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/inventory/ini.py#L180

When a `[...:vars]` line is seen, `invantory.add_group` is called, and entry is added to `pending_declarations` (as `state=='vars'`).
Eventually, on line 242, `pending_declarations` is iterated but as we have already called `add_group` , the  check  `if g not in self.inventory.groups` ensure that we never throw the error.




Discussion:
confirmed, currently groups only defined as a 'vars' section don't cause errors.

Resolving patch:
diff --git a/lib/ansible/plugins/inventory/ini.py b/lib/ansible/plugins/inventory/ini.py
--- a/lib/ansible/plugins/inventory/ini.py
+++ b/lib/ansible/plugins/inventory/ini.py
@@ -153,7 +153,6 @@ def _parse(self, path, lines):
         pending_declarations = {}
         groupname = 'ungrouped'
         state = 'hosts'
-
         self.lineno = 0
         for line in lines:
             self.lineno += 1
@@ -176,24 +175,21 @@ def _parse(self, path, lines):
                     self._raise_error("Section [%s] has unknown type: %s" % (title, state))
 
                 # If we haven't seen this group before, we add a new Group.
-                #
-                # Either [groupname] or [groupname:children] is sufficient to
-                # declare a group, but [groupname:vars] is allowed only if the
-                # group is declared elsewhere (not necessarily earlier). We add
-                # the group anyway, but make a note in pending_declarations to
-                # check at the end.
-
-                self.inventory.add_group(groupname)
-
-                if state == 'vars':
-                    pending_declarations[groupname] = dict(line=self.lineno, state=state, name=groupname)
+                if groupname not in self.inventory.groups:
+                    # Either [groupname] or [groupname:children] is sufficient to declare a group,
+                    # but [groupname:vars] is allowed only if the # group is declared elsewhere.
+                    # We add the group anyway, but make a note in pending_declarations to check at the end.
+                    if state == 'vars':
+                        pending_declarations[groupname] = dict(line=self.lineno, state=state, name=groupname)
 
-                # When we see a declaration that we've been waiting for, we can
-                # delete the note.
+                    self.inventory.add_group(groupname)
 
+                # When we see a declaration that we've been waiting for, we process and delete.
                 if groupname in pending_declarations and state != 'vars':
                     if pending_declarations[groupname]['state'] == 'children':
                         self._add_pending_children(groupname, pending_declarations)
+                    elif pending_declarations[groupname]['state'] == 'vars':
+                        del pending_declarations[groupname]
 
                 continue
             elif line.startswith('[') and line.endswith(']'):
@@ -229,22 +225,18 @@ def _parse(self, path, lines):
                         pending_declarations[child]['parents'].append(groupname)
                 else:
                     self.inventory.add_child(groupname, child)
-
-            # This is a fencepost. It can happen only if the state checker
-            # accepts a state that isn't handled above.
             else:
+                # This can happen only if the state checker accepts a state that isn't handled above.
                 self._raise_error("Entered unhandled state: %s" % (state))
 
         # Any entries in pending_declarations not removed by a group declaration above mean that there was an unresolved reference.
         # We report only the first such error here.
-
         for g in pending_declarations:
-            if g not in self.inventory.groups:
-                decl = pending_declarations[g]
-                if decl['state'] == 'vars':
-                    raise AnsibleError("%s:%d: Section [%s:vars] not valid for undefined group: %s" % (path, decl['line'], decl['name'], decl['name']))
-                elif decl['state'] == 'children':
-                    raise AnsibleError("%s:%d: Section [%s:children] includes undefined group: %s" % (path, decl['line'], decl['parents'].pop(), decl['name']))
+            decl = pending_declarations[g]
+            if decl['state'] == 'vars':
+                raise AnsibleError("%s:%d: Section [%s:vars] not valid for undefined group: %s" % (path, decl['line'], decl['name'], decl['name']))
+            elif decl['state'] == 'children':
+                raise AnsibleError("%s:%d: Section [%s:children] includes undefined group: %s" % (path, decl['line'], decl['parents'].pop(), decl['name']))
 
     def _add_pending_children(self, group, pending):
         for parent in pending[group]['parents']:
@@ -326,7 +318,7 @@ def _expand_hostpattern(self, hostpattern):
 
         try:
             (pattern, port) = parse_address(hostpattern, allow_ranges=True)
-        except:
+        except Exception:
             # not a recognizable host pattern
             pattern = hostpattern
             port = None


Test patch: