Repository: ansible/ansible

Issue:
Possibly confusing error messages when using --limit with range-like selector
ansible 1.3.3 under both OSX and Linux. Reproducer setup:

```
==> hosts.bug <==
[storage]
storage1.example.com
storage2.example.com
storage3.example.com
storage4.example.com
storage5.example.com
storage6.example.com
storage7.example.com
storage8.example.com
storage9.example.com
dev-storage1.example.com
dev-storage2.example.com
monster3.example.com
monster4.example.com
[all:vars]
ansible_connection=local

==> bug.yml <==
- hosts: all
  gather_facts: no
  tasks:
    - action: debug msg="Talking to {{inventory_hostname}}"
```

Then run the following command:

```
$ ansible-playbook bug.yml -i hosts.bug -l 'storage[8:9].example.com'

PLAY [all] ********************************************************************

TASK: [debug msg="Talking to {{inventory_hostname}}"] *************************
ok: [storage5.example.com] => {"msg": "Talking to storage5.example.com"}

PLAY RECAP ********************************************************************
storage5.example.com       : ok=2    changed=0    unreachable=0    failed=0
```

This is rather confusing, as the inventory deals with ranges differently to -l.

Digging through code, it looks like `group[a:b]` is supposed to start at offset 'a' with limit 'b'. So I would expect `[0:2]` to talk to two hosts, but it doesn't:

```
$ ansible-playbook bug.yml -i hosts.bug -l 'storage[0:2]'

PLAY [all] ********************************************************************

TASK: [debug msg="Talking to {{inventory_hostname}}"] *************************
ok: [dev-storage1.example.com] => {"msg": "Talking to dev-storage1.example.com"}

PLAY RECAP ********************************************************************
dev-storage1.example.com   : ok=2    changed=0    unreachable=0    failed=0
```

Personally I think I'd find it more useful to be able to name a range of servers, than get an arbitrary subset of them.

Then I changed the group name `[storage]` to `[storage_servers]` in the inventory, to avoid confusion. Now the host list pattern I give results in an empty host list, which is reasonable if it's not meant to match anything:

```
$ ansible-playbook bug.yml -i hosts.bug -l 'storage[8:9].example.com'
ERROR: provided hosts list is empty
```

However, if I use a dash rather than a colon to give the range (which I thought was not allowed), I then get an exception:

```
$ ansible-playbook bug.yml -i hosts.bug -l 'storage[8-9].example.com'
  File "/Users/brian/git/ansible/lib/ansible/inventory/__init__.py", line 229, in _apply_ranges
    right=int(right)
ValueError: invalid literal for int() with base 10: '9.example.com'
```

This _is_ within code which is something to do with ranges. I don't understand what the code is doing though. A dash is treated as a special character:

```
        if "-" in rest:
            (left, right) = rest.split("-",1)
            return (first, (left, right))
        else:
            return (first, (rest, rest))
```

I did try very hard to find any documentation about the expected semantics of the `--limit` flag to ansible-playbook, but failed. It is not in:
- man ansible-playbook
- http://www.ansibleworks.com/docs/playbooks.html
- any of the pages linked in the section http://www.ansibleworks.com/docs/#special-topics-in-playbooks
- http://www.ansibleworks.com/docs/faq.html

except a mention in passing at
http://www.ansibleworks.com/docs/playbooks_checkmode.html#showing-differences-with-diff



Discussion:

Resolving patch:
diff --git a/lib/ansible/inventory/__init__.py b/lib/ansible/inventory/__init__.py
--- a/lib/ansible/inventory/__init__.py
+++ b/lib/ansible/inventory/__init__.py
@@ -207,24 +207,19 @@ def _enumeration_info(self, pattern):
         a tuple of (start, stop) or None
         """
 
-        if not "[" in pattern or pattern.startswith('~'):
-            return (pattern, None)
-        (first, rest) = pattern.split("[")
-        rest = rest.replace("]","")
-        try:
-            # support selectors like webservers[0]
-            x = int(rest)
-            return (first, (x,x)) 
-        except:
-            pass
-        if "-" in rest:
-            (left, right) = rest.split("-",1)
-            return (first, (left, right))
-        elif ":" in rest:
-            (left, right) = rest.split(":",1)
-            return (first, (left, right))
+        # The regex used to match on the range, which can be [x] or [x-y].
+        pattern_re = re.compile("^(.*)\[([0-9]+)(?:(?:-)([0-9]+))?\](.*)$")
+        m = pattern_re.match(pattern)
+        if m:
+            (target, first, last, rest) = m.groups()
+            first = int(first)
+            if last:
+                last = int(last)
+            else:
+                last = first
+            return (target, (first, last))
         else:
-            return (first, (rest, rest))
+            return (pattern, None)
 
     def _apply_ranges(self, pat, hosts):
         """


Test patch:
diff --git a/test/TestInventory.py b/test/TestInventory.py
--- a/test/TestInventory.py
+++ b/test/TestInventory.py
@@ -206,6 +206,11 @@ def test_subset(self):
         inventory.subset('odin;thor,loki')
         self.assertEqual(sorted(inventory.list_hosts()),  sorted(['thor','odin','loki']))
 
+    def test_subset_range(self):
+        inventory = self.simple_inventory()
+        inventory.subset('greek[0-2];norse[0]')
+        self.assertEqual(sorted(inventory.list_hosts()),  sorted(['zeus','hera','thor']))
+
     def test_subset_filename(self):
         inventory = self.simple_inventory()
         inventory.subset('@' + os.path.join(self.test_dir, 'restrict_pattern'))