Repository: ansible/ansible

Issue:
Group Names with Dashes are Invalid in 2.4 YAML inventories
##### ISSUE TYPE
 - Bug Report


##### ANSIBLE VERSION
```
2.4.0.0
```

Hello

Group names with dashes are invalid in Ansible 2.4.

Reproducible only in static YAML/JSON inventories.

Dynamic and INI inventories support group names with dashes in 2.4.

**inventory.yml**
```yaml
all:
  children:
    webservers-usa:
      hosts:
        foo.example.com:
    webservers-canada:
      hosts:
        foo.example.com:
```

**test.yml**
```yaml
- hosts: webservers-usa
  connection: local
  gather_facts: no
  tasks:
    - debug: msg='hello'
```

# Ansible 2.4
```shell
# ansible-playbook -i inventory.yml test.yml 
 [WARNING]: Skipping 'webservers-usa' as this is not a valid group name

ERROR! webservers-usa is not a known host nor group
```

# Ansible 2.3
```shell
# ansible-playbook -i inventory.yml test.yml 
ok: [foo.example.com] => {
    "msg": "hello"
}
```
Please fix the ASAP.
Thank you


Discussion:
@lystor Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information. 

Here are the items we could not find in your description:
- issue type
- ansible version
- component name

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
It also does not allow groups with equal signs in them either, which are also able to be used by dynamic inventories.
the updated validation accepts 'valid python variable names'  for groups, as those characters actually break some functionality i.e  `groups.group-name` , creates an error
It would be nice for the validation to be applied across all inventory types then.
Running into this just now, and was just about to submit a regression 

If a file extension isn't given, you get the following error:

```
root@awx-world0:~/awx-resources# ansible-inventory -i all --list                                         
 [WARNING]:  * Failed to parse /root/awx-resources/all with ini plugin: /root/awx-resources/all:14:
Expected key=value host variable assignment, got: root

 [WARNING]: Unable to parse /root/awx-resources/all as an inventory source

 [WARNING]: No inventory was parsed, only implicit localhost is available
```
Please consider adding a note to the porting guides regarding this.
Some of Ansible customers use third-party CRM software that provides dynamic Ansible inventories with dashes inside group names. They will not be able to migrate to Ansible 2.4. At the same time official Ansible PPA provides only the latest version of ansible package.
@lystor by design the PPA can only supply one version, that is why we create PPAs for the previous version on release: https://launchpad.net/~ansible/+archive/ubuntu/ansible-2.3

As for this issue, i'm looking at both generalizing (applying to all plugins) and making it configurable (allow users to set 'valid characters'). 

The current behaviour was added after many users complained that the plugin allowed them to define names that would cause errors when used in their playbooks.

Resolving patch:
diff --git a/lib/ansible/plugins/inventory/yaml.py b/lib/ansible/plugins/inventory/yaml.py
--- a/lib/ansible/plugins/inventory/yaml.py
+++ b/lib/ansible/plugins/inventory/yaml.py
@@ -68,9 +68,6 @@ class InventoryModule(BaseFileInventoryPlugin):
     def __init__(self):
 
         super(InventoryModule, self).__init__()
-        self.patterns = {}
-
-        self._compile_patterns()
 
     def verify_file(self, path):
 
@@ -104,31 +101,27 @@ def parse(self, inventory, loader, path, cache=True):
 
     def _parse_group(self, group, group_data):
 
-        if self.patterns['groupname'].match(group):
-
-            self.inventory.add_group(group)
+        self.inventory.add_group(group)
 
-            if isinstance(group_data, MutableMapping):
-                # make sure they are dicts
-                for section in ['vars', 'children', 'hosts']:
-                    if section in group_data and isinstance(group_data[section], string_types):
-                        group_data[section] = {group_data[section]: None}
+        if isinstance(group_data, MutableMapping):
+            # make sure they are dicts
+            for section in ['vars', 'children', 'hosts']:
+                if section in group_data and isinstance(group_data[section], string_types):
+                    group_data[section] = {group_data[section]: None}
 
-                if group_data.get('vars', False):
-                    for var in group_data['vars']:
-                        self.inventory.set_variable(group, var, group_data['vars'][var])
+            if group_data.get('vars', False):
+                for var in group_data['vars']:
+                    self.inventory.set_variable(group, var, group_data['vars'][var])
 
-                if group_data.get('children', False):
-                    for subgroup in group_data['children']:
-                        self._parse_group(subgroup, group_data['children'][subgroup])
-                        self.inventory.add_child(group, subgroup)
+            if group_data.get('children', False):
+                for subgroup in group_data['children']:
+                    self._parse_group(subgroup, group_data['children'][subgroup])
+                    self.inventory.add_child(group, subgroup)
 
-                if group_data.get('hosts', False):
-                    for host_pattern in group_data['hosts']:
-                        hosts, port = self._parse_host(host_pattern)
-                        self.populate_host_vars(hosts, group_data['hosts'][host_pattern] or {}, group, port)
-        else:
-            self.display.warning("Skipping '%s' as this is not a valid group name" % group)
+            if group_data.get('hosts', False):
+                for host_pattern in group_data['hosts']:
+                    hosts, port = self._parse_host(host_pattern)
+                    self.populate_host_vars(hosts, group_data['hosts'][host_pattern] or {}, group, port)
 
     def _parse_host(self, host_pattern):
         '''
@@ -162,9 +155,3 @@ def _expand_hostpattern(self, hostpattern):
             hostnames = [pattern]
 
         return (hostnames, port)
-
-    def _compile_patterns(self):
-        '''
-        Compiles the regular expressions required to parse the inventory and stores them in self.patterns.
-        '''
-        self.patterns['groupname'] = re.compile(u'''^[A-Za-z_][A-Za-z0-9_]*$''')


Test patch: