diff --git a/docsite/rst/playbooks2.rst b/docsite/rst/playbooks2.rst index 95a084fb3dd..a080ffd0a1c 100644 --- a/docsite/rst/playbooks2.rst +++ b/docsite/rst/playbooks2.rst @@ -415,8 +415,8 @@ More Loops .. versionadded: 0.8 Various 'lookup plugins' allow additional ways to iterate over data. Ansible will have more of these -over time. In 0.8, the only lookup plugin that comes stock is 'with_fileglob', but you can also write -your own. +over time. In 0.8, the only lookup plugins that comes stock are 'with_fileglob' and 'with_sequence', but +you can also write your own. 'with_fileglob' matches all files in a single directory, non-recursively, that match a pattern. It can be used like this:: @@ -433,6 +433,52 @@ be used like this:: - action: copy src=$item dest=/etc/fooapp/ owner=root mode=600 with_fileglob: /playbooks/files/fooapp/* +.. versionadded: 1.0 + +'with_sequence' generates a sequence of items in ascending numerical order. You +can specify a 'start', an 'end' value (inclusive), and a 'stride' value (to skip +some numbers of values), and a printf-style 'format' string. It accepts +arguments both as key-value pairs and in a shortcut of the form +"[start-]end[/stride][:format]". All numerical values can be specified in +hexadecimal (i.e. 0x3f8) or octal (i.e. 0644). Negative numbers are not +supported. Here is an example that leverages most of its features:: + + ---- + - hosts: all + + tasks: + + # create groups + - group: name=evens state=present + + - group: name=odds state=present + + # create 32 test users + - user: name=$item state=present groups=odds + with_sequence: 32/2:testuser%02x + + - user: name=$item state=present groups=evens + with_sequence: 2-32/2:testuser%02x + + # create a series of directories for some reason + - file: dest=/var/stuff/$item state=directory + with_sequence: start=4 end=16 + +The key-value form also supports a 'count' option, which always generates +'count' entries regardless of the stride. The count option is mostly useful for +avoiding off-by-one errors and errors calculating the number of entries in a +sequence when a stride is specified. The shortcut form cannot be used to +specify a count. As an example:: + + ---- + - hosts: all + + tasks: + + # create 4 groups + - group: name=group${item} state=present + with_sequence: count=4 + Getting values from files ````````````````````````` @@ -466,7 +512,7 @@ The following example shows how to template out a configuration file that was ve - /srv/templates/myapp/${ansible_distribution}.conf - /srv/templates/myapp/default.conf -first_available_file is only available to the copy and template modules. +first_available_file is only available to the copy and template modules. Asynchronous Actions and Polling ```````````````````````````````` diff --git a/lib/ansible/runner/lookup_plugins/sequence.py b/lib/ansible/runner/lookup_plugins/sequence.py new file mode 100644 index 00000000000..9777aea984b --- /dev/null +++ b/lib/ansible/runner/lookup_plugins/sequence.py @@ -0,0 +1,202 @@ +# (c) 2013, Jayson Vantuyl +# +# 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 . + +from ansible.errors import AnsibleError +from ansible.utils import parse_kv +from re import compile as re_compile, IGNORECASE + +# shortcut format +NUM = "(0?x?[0-9a-f]+)" +SHORTCUT = re_compile( + "^(" + # Group 0 + NUM + # Group 1: Start + "-)?" + + NUM + # Group 2: End + "(/" + # Group 3 + NUM + # Group 4: Stride + ")?" + + "(:(.+))?$", # Group 5, Group 6: Format String + IGNORECASE +) + + +class LookupModule(object): + """ + sequence lookup module + + Used to generate some sequence of items. Takes arguments in two forms. + + The simple / shortcut form is: + + [start-]end[/stride][:format] + + As indicated by the brackets: start, stride, and format string are all + optional. The format string is in the style of printf. This can be used + to pad with zeros, format in hexadecimal, etc. All of the numerical values + can be specified in octal (i.e. 0664) or hexadecimal (i.e. 0x3f8). + Negative numbers are not supported. + + Some examples: + + 5 -> ["1","2","3","4","5"] + 5-8 -> ["5", "6", "7", "8"] + 2-10/2 -> ["2", "4", "6", "8", "10"] + 4:host%02d -> ["host01","host02","host03","host04"] + + The standard Ansible key-value form is accepted as well. For example: + + start=5 end=11 stride=2 format=0x%02x -> ["0x05","0x07","0x09","0x0a"] + + This format takes an alternate form of "end" called "count", which counts + some number from the starting value. For example: + + count=5 -> ["1", "2", "3", "4", "5"] + start=0x0f00 count=4 format=%04x -> ["0f00", "0f01", "0f02", "0f03"] + start=0 count=5 stride=2 -> ["0", "2", "4", "6", "8"] + start=1 count=5 stride=2 -> ["1", "3", "5", "7", "9"] + + The count option is mostly useful for avoiding off-by-one errors and errors + calculating the number of entries in a sequence when a stride is specified. + """ + + def __init__(self, **kwargs): + """absorb any keyword args""" + pass + + def reset(self): + """set sensible defaults""" + self.start = 1 + self.count = None + self.end = None + self.stride = 1 + self.format = "%d" + + def parse_kv_args(self, args): + """parse key-value style arguments""" + for arg in ["start", "end", "count", "stride"]: + try: + arg_raw = args.pop(arg, None) + if arg_raw is None: + continue + arg_cooked = int(arg_raw, 0) + setattr(self, arg, arg_cooked) + except ValueError: + raise AnsibleError( + "can't parse arg %s=%r as integer" + % (arg, arg_raw) + ) + if 'format' in args: + self.format = args.pop("format") + if args: + raise AnsibleError( + "unrecognized arguments to with_sequence: %r" + % args.keys() + ) + + def parse_simple_args(self, term): + """parse the shortcut forms, return True/False""" + match = SHORTCUT.match(term) + if not match: + return False + + _, start, end, _, stride, _, format = match.groups() + + if start is not None: + try: + start = int(start, 0) + except ValueError: + raise AnsibleError("can't parse start=%s as integer" % start) + if end is not None: + try: + end = int(end, 0) + except ValueError: + raise AnsibleError("can't parse end=%s as integer" % end) + if stride is not None: + try: + stride = int(stride, 0) + except ValueError: + raise AnsibleError("can't parse stride=%s as integer" % stride) + + if start is not None: + self.start = start + if end is not None: + self.end = end + if stride is not None: + self.stride = stride + if format is not None: + self.format = format + + def sanity_check(self): + if self.count is None and self.end is None: + raise AnsibleError( + "must specify count or end in with_sequence" + ) + elif self.count is not None and self.end is not None: + raise AnsibleError( + "can't specify both count and end in with_sequence" + ) + elif self.count is not None: + # convert count to end + self.end = self.start + self.count * self.stride - 1 + del self.count + if self.end < self.start: + raise AnsibleError("can't count backwards") + if self.format.count('%') != 1: + raise AnsibleError("bad formatting string: %s" % self.format) + + def generate_sequence(self): + numbers = xrange(self.start, self.end + 1, self.stride) + + for i in numbers: + try: + formatted = self.format % i + yield formatted + except (ValueError, TypeError): + raise AnsibleError( + "problem formatting %r with %r" % self.format + ) + + def run(self, terms, **kwargs): + results = [] + + if isinstance(terms, basestring): + terms = [terms] + + for term in terms: + try: + self.reset() # clear out things for this iteration + + try: + if not self.parse_simple_args(term): + self.parse_kv_args(parse_kv(term)) + except Exception: + raise AnsibleError( + "unknown error parsing with_sequence arguments: %r" + % term + ) + + self.sanity_check() + + results.extend(self.generate_sequence()) + except AnsibleError: + raise + except Exception: + raise AnsibleError( + "unknown error generating sequence" + ) + + return results diff --git a/test/TestPlayBook.py b/test/TestPlayBook.py index 5fa63eb6238..1403edb8f33 100644 --- a/test/TestPlayBook.py +++ b/test/TestPlayBook.py @@ -172,9 +172,9 @@ class TestPlaybook(unittest.TestCase): print utils.jsonify(actual, format=True) expected = { "localhost": { - "changed": 7, + "changed": 9, "failures": 0, - "ok": 9, + "ok": 14, "skipped": 1, "unreachable": 0 } @@ -185,7 +185,7 @@ class TestPlaybook(unittest.TestCase): assert utils.jsonify(expected, format=True) == utils.jsonify(actual,format=True) print "len(EVENTS) = %d" % len(EVENTS) - assert len(EVENTS) == 26 + assert len(EVENTS) == 60 def test_includes(self): pb = os.path.join(self.test_dir, 'playbook-includer.yml') diff --git a/test/lookup_plugins.yml b/test/lookup_plugins.yml index faf4d84816c..ae73552ab66 100644 --- a/test/lookup_plugins.yml +++ b/test/lookup_plugins.yml @@ -26,6 +26,25 @@ - name: test LOOKUP and PIPE action: command test "$LOOKUP(pipe, cat sample.j2)" = "$PIPE(cat sample.j2)" + - name: test with_sequence, generate + command: touch /tmp/seq-${item} + with_sequence: 0-16/2:%02x + + - name: test with_sequence, fenceposts 1 + copy: src=/tmp/seq-00 dest=/tmp/seq-10 + + - name: test with_sequence, fenceposts 2 + file: dest=/tmp/seq-${item} state=absent + with_items: [11, 12] + + - name: test with_sequence, missing + file: dest=/tmp/seq-${item} state=absent + with_sequence: 0x10/02:%02x + + - name: test with_sequence,remove + file: dest=/tmp/seq-${item} state=absent + with_sequence: 0-0x10/02:%02x + - name: ensure test file doesnt exist # command because file will return differently action: command rm -f /tmp/ansible-test-with_lines-data