Merge pull request #1838 from jvantuyl/with_sequence

add with_sequence lookup plugin
This commit is contained in:
Daniel Hokka Zakrisson 2013-01-09 16:18:17 -08:00
commit 9649e4bf15
4 changed files with 273 additions and 6 deletions

View file

@ -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
````````````````````````````````

View file

@ -0,0 +1,202 @@
# (c) 2013, Jayson Vantuyl <jayson@aggressive.ly>
#
# 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 <http://www.gnu.org/licenses/>.
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

View file

@ -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')

View file

@ -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