From fde00327b043a4ef6c9b358f6a92dc32f38e5077 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Sat, 24 Nov 2012 22:52:18 +0100 Subject: [PATCH 01/12] Rework "enabled" implementation for FreeBSD. When trying to perform enabled=yes followed by enabled=no against FreeBSD the module would die with the following error: TypeError: sub() takes at most 4 arguments (5 given) The target FreeBSD client (8.2) is running python 2.6.6. It seems the extra 'flags' argument was added to re.sub() in 2.7. In fixing this issue I have attempted to create a general atomic method for modifying a rc.conf file. Hopefully this will make it easier to add other rc based platorms. The strip/split magic was inspired by the user module. --- library/service | 80 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/library/service b/library/service index f52a949bb8b..355eb2be7c1 100644 --- a/library/service +++ b/library/service @@ -73,6 +73,7 @@ examples: import platform import os import re +import tempfile class Service(object): """ @@ -108,6 +109,9 @@ class Service(object): self.svc_initctl = None self.enable_cmd = None self.arguments = module.params.get('arguments', '') + self.rcconf_file = None + self.rcconf_key = None + self.rcconf_value = None # select whether we dump additional debug info through syslog self.syslogging = False @@ -194,6 +198,60 @@ class Service(object): out = '' return rc, out, err + def service_enable_rcconf(self): + if self.rcconf_file is None or self.rcconf_key is None or self.rcconf_value is None: + self.module.fail_json(msg="service_enable_rcconf() requires rcconf_file, rcconf_key and rcconf_value") + + changed = None + entry = '%s="%s"\n' % (self.rcconf_key, self.rcconf_value) + RCFILE = open(self.rcconf_file, "r") + new_rc_conf = [] + + # Build a list containing the possibly modified file. + for rcline in RCFILE: + # Only parse non-comment and non-empty lines. + if not re.search('^(#.*)?$', rcline): + key = rcline.split('=')[0] + # We need to strip any newline and " signs from the value. + value = rcline.split('=')[1].strip('\n"') + if key == self.rcconf_key: + if value == self.rcconf_value: + # Since the proper entry already exists we can stop iterating. + changed = False + break + else: + # We found the key but the value is wrong, replace with new entry. + rcline = entry + changed = True + + # Add line to the list. + new_rc_conf.append(rcline) + + # We are done with reading the current rc.conf, close it. + RCFILE.close() + + # If we did not see any trace of our entry we need to add it. + if changed is None: + new_rc_conf.append(entry) + changed = True + + if changed is True: + # Create a temporary file next to the current rc.conf (so we stay on the same filesystem). + # This way the replacement operation is atomic. + rcconf_dir = os.path.dirname(self.rcconf_file) + rcconf_base = os.path.basename(self.rcconf_file) + (TMP_RCCONF, tmp_rcconf_file) = tempfile.mkstemp(dir=rcconf_dir, prefix="%s-" % rcconf_base) + + # Write out the contents of the list into our temporary file. + for rcline in new_rc_conf: + os.write(TMP_RCCONF, rcline) + + # Close temporary file. + os.close(TMP_RCCONF) + + # Replace previous rc.conf. + self.module.atomic_replace(tmp_rcconf_file, self.rcconf_file) + # =========================================== # Subclass: Linux @@ -368,28 +426,18 @@ class FreeBsdService(Service): def service_enable(self): if self.enable: - rc = "YES" + self.rcconf_value = "YES" else: - rc = "NO" + self.rcconf_value = "NO" rcfiles = [ '/etc/rc.conf','/usr/local/etc/rc.conf' ] for rcfile in rcfiles: if os.path.isfile(rcfile): - rcconf = rcfile + self.rcconf_file = rcfile + + self.rcconf_key = "%s_enable" % self.name - entry = "%s_enable" % self.name - full_entry = '%s="%s"' % (entry,rc) - rc = open(rcconf,"r+") - rctext = rc.read() - if re.search("^%s" % full_entry,rctext,re.M) is None: - if re.search("^%s" % entry,rctext,re.M) is None: - rctext += "\n%s" % full_entry - else: - rctext = re.sub("^%s.*" % entry,full_entry,rctext,1,re.M) - rc.truncate(0) - rc.seek(0) - rc.write(rctext) - rc.close() + return self.service_enable_rcconf() def service_control(self): if self.action is "start": From d4af9e4c5c40e0e20831255346effc5696611384 Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Sun, 25 Nov 2012 03:24:49 +0100 Subject: [PATCH 02/12] Use shlex for rc.conf parsing. This makes the line parsing a lot more robust (and easier to read). Code supplied by @dhozac, thanks! Remove re import because this is not used anywhere. --- library/service | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/library/service b/library/service index 355eb2be7c1..3889e4fefe9 100644 --- a/library/service +++ b/library/service @@ -72,8 +72,8 @@ examples: import platform import os -import re import tempfile +import shlex class Service(object): """ @@ -209,11 +209,10 @@ class Service(object): # Build a list containing the possibly modified file. for rcline in RCFILE: - # Only parse non-comment and non-empty lines. - if not re.search('^(#.*)?$', rcline): - key = rcline.split('=')[0] - # We need to strip any newline and " signs from the value. - value = rcline.split('=')[1].strip('\n"') + # Parse line removing whitespaces, quotes, etc. + rcarray = shlex.split(rcline, comments=True) + if len(rcarray) >= 1 and '=' in rcarray[0]: + (key, value) = rcarray[0].split("=", 1) if key == self.rcconf_key: if value == self.rcconf_value: # Since the proper entry already exists we can stop iterating. From 94fc3006e929bf3f0e3fa465f850f3e2b41e2ca1 Mon Sep 17 00:00:00 2001 From: Daniel Hokka Zakrisson Date: Mon, 26 Nov 2012 21:16:13 +0100 Subject: [PATCH 03/12] Make variables local to the play Fixes #1677. --- lib/ansible/playbook/play.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index b3057450ccd..de6822b4227 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -161,7 +161,7 @@ class Play(object): if type(self.vars) not in [dict, list]: raise errors.AnsibleError("'vars' section must contain only key/value pairs") - vars = self.playbook.global_vars + vars = {} # translate a list of vars into a dict if type(self.vars) == list: From ebd31af940a72265a851d67ecc9b5b88e146a1ca Mon Sep 17 00:00:00 2001 From: Daniel Hokka Zakrisson Date: Mon, 26 Nov 2012 21:24:49 +0100 Subject: [PATCH 04/12] Don't prompt for vars in extra-vars Fixes #1622. --- lib/ansible/playbook/play.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py index de6822b4227..16af6a03999 100644 --- a/lib/ansible/playbook/play.py +++ b/lib/ansible/playbook/play.py @@ -187,12 +187,14 @@ class Play(object): salt_size = var.get("salt_size", None) salt = var.get("salt", None) - vars[vname] = self.playbook.callbacks.on_vars_prompt(vname, private, prompt,encrypt, confirm, salt_size, salt) + if vname not in self.playbook.extra_vars: + vars[vname] = self.playbook.callbacks.on_vars_prompt(vname, private, prompt,encrypt, confirm, salt_size, salt) elif type(self.vars_prompt) == dict: for (vname, prompt) in self.vars_prompt.iteritems(): prompt_msg = "%s: " % prompt - vars[vname] = self.playbook.callbacks.on_vars_prompt(varname=vname, private=False, prompt=prompt_msg) + if vname not in self.playbook.extra_vars: + vars[vname] = self.playbook.callbacks.on_vars_prompt(varname=vname, private=False, prompt=prompt_msg) else: raise errors.AnsibleError("'vars_prompt' section is malformed, see docs") From 1e0295c7e21e5c186d38e3d9898d76568bc0fce3 Mon Sep 17 00:00:00 2001 From: Daniel Hokka Zakrisson Date: Fri, 23 Nov 2012 00:31:38 +0100 Subject: [PATCH 05/12] Template handlers late This allows overriding variables in name, and removes templating from parsing. --- lib/ansible/playbook/__init__.py | 8 ++++---- lib/ansible/playbook/task.py | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 8af84124749..314492e7be1 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -308,13 +308,13 @@ class PlayBook(object): for host, results in results.get('contacted',{}).iteritems(): if results.get('changed', False): for handler_name in task.notify: - self._flag_handler(play.handlers(), utils.template(play.basedir, handler_name, task.module_vars), host) + self._flag_handler(play, utils.template(play.basedir, handler_name, task.module_vars), host) return hosts_remaining # ***************************************************** - def _flag_handler(self, handlers, handler_name, host): + def _flag_handler(self, play, handler_name, host): ''' if a task has any notify elements, flag handlers for run at end of execution cycle for hosts that have indicated @@ -322,8 +322,8 @@ class PlayBook(object): ''' found = False - for x in handlers: - if handler_name == x.name: + for x in play.handlers(): + if handler_name == utils.template(play.basedir, x.name, x.module_vars): found = True self.callbacks.on_notify(host, x.name) x.notified_by.append(host) diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 956c9eaee07..534ea058a3c 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -149,9 +149,6 @@ class Task(object): # allow the user to list comma delimited tags import_tags = import_tags.split(",") - self.name = utils.template(None, self.name, self.module_vars) - self.action = utils.template(None, self.action, self.module_vars) - # handle mutually incompatible options incompatibles = [ x for x in [ self.first_available_file, self.items_lookup_plugin ] if x is not None ] if len(incompatibles) > 1: From 43bdec8b20b9c05bc2eb00d7090ffaf833577829 Mon Sep 17 00:00:00 2001 From: Daniel Hokka Zakrisson Date: Mon, 26 Nov 2012 22:42:44 +0100 Subject: [PATCH 06/12] Throw an error if multiple actions have been specified --- lib/ansible/playbook/task.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 534ea058a3c..42e09601f5f 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -45,6 +45,8 @@ class Task(object): # code to allow for saying "modulename: args" versus "action: modulename args" if x in utils.plugins.module_finder: + if 'action' in ds: + raise errors.AnsibleError("multiple actions specified in task %s" % (ds.get('name', ds['action']))) ds['action'] = x + " " + ds[x] ds.pop(x) From 9870224991c8d360fe8c9b0c3a2dfbce668ac13b Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 26 Nov 2012 18:27:55 -0500 Subject: [PATCH 07/12] Tweak fireball docs for 0.9 --- docsite/rst/playbooks2.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docsite/rst/playbooks2.rst b/docsite/rst/playbooks2.rst index 3c20f6bac9f..9fd8bec0e0d 100644 --- a/docsite/rst/playbooks2.rst +++ b/docsite/rst/playbooks2.rst @@ -604,7 +604,7 @@ Fireball Mode .. versionadded:: 0.8 -Paramiko's core connection types of 'local', 'paramiko', and 'ssh' are augmented in version 0.8 by a new extra-fast +Ansible's core connection types of 'local', 'paramiko', and 'ssh' are augmented in version 0.8 and later by a new extra-fast connection type called 'fireball'. It can only be used with playbooks and does require some additional setup outside the lines of ansible's normal "no bootstrapping" philosophy. You are not required to use fireball mode to use Ansible, though some users may appreciate it. @@ -654,7 +654,10 @@ any platform. You will also need gcc and zeromq-devel installed from your packa - PyCrypto - python-keyczar -For more information about fireball, see the module documentation section. +Fedora and EPEL also have Ansible RPM subpackages available for fireball-dependencies. + +Also see the module documentation section. + Understanding Variable Precedence ````````````````````````````````` From c8d004434fa2ba97dd561fa97d2d6b50c50860be Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 26 Nov 2012 18:29:27 -0500 Subject: [PATCH 08/12] Limit seperator docs. --- docsite/rst/examples.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docsite/rst/examples.rst b/docsite/rst/examples.rst index 07d4cb6093b..b17491f5bd9 100644 --- a/docsite/rst/examples.rst +++ b/docsite/rst/examples.rst @@ -226,6 +226,8 @@ also works with ``ansible-playbook``:: $ ansible webservers:dbservers -m command -a "/bin/foo xyz" --limit region +As with other host patterns, values to limit can be seperated with ";", ":", or ",". + Now let's talk about range selection. Suppose you have 1000 servers in group 'datacenter', but only want to target one at a time. This is also easy:: $ ansible webservers[0-99] -m command -a "/bin/foo xyz" From 08b3c77dc780bc6118953c15a83d9846ae2f1c57 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 26 Nov 2012 18:29:54 -0500 Subject: [PATCH 09/12] Tweak docs --- docsite/rst/examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docsite/rst/examples.rst b/docsite/rst/examples.rst index b17491f5bd9..9b46c0e9719 100644 --- a/docsite/rst/examples.rst +++ b/docsite/rst/examples.rst @@ -226,7 +226,7 @@ also works with ``ansible-playbook``:: $ ansible webservers:dbservers -m command -a "/bin/foo xyz" --limit region -As with other host patterns, values to limit can be seperated with ";", ":", or ",". +Assuming version 0.9 or later, as with other host patterns, values to limit can be seperated with ";", ":", or ",". Now let's talk about range selection. Suppose you have 1000 servers in group 'datacenter', but only want to target one at a time. This is also easy:: From da90c5f75e4db5c92092fe8dc39d2169557b0a45 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 26 Nov 2012 18:37:44 -0500 Subject: [PATCH 10/12] Document 'when' and tweak the code to be a little more comprehensive on what is false. --- docsite/rst/playbooks2.rst | 41 ++++++++++++++++++++++++++++++++++++ lib/ansible/playbook/task.py | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docsite/rst/playbooks2.rst b/docsite/rst/playbooks2.rst index 9fd8bec0e0d..afc412a9571 100644 --- a/docsite/rst/playbooks2.rst +++ b/docsite/rst/playbooks2.rst @@ -288,6 +288,47 @@ While `only_if` is a pretty good option for advanced users, it exposes more guts we can do better. In 0.9, we will be adding `when`, which will be like a syntactic sugar for `only_if` and hide this level of complexity -- it will numerous built in operators. +Conditional Execution (Simplified) +`````````````````````````````````` + +In Ansible 0.9, we realized that only_if was a bit syntactically complicated, and exposed too much Python +to the user. As a result, the 'when' set of keywords was added. The 'when' statements do not have +to be quoted or casted to specify types, but you should seperate any variables used with whitespace. In +most cases users will be able to use 'when', but for more complex cases, only_if may still be required. + +Here are various examples of 'when' in use. 'when' is incompatible with 'only_if' in the same task:: + + - name: "do this if my favcolor is blue, and my dog is named fido" + action: shell /bin/false + when_string: $favcolor == 'blue' and $dog == 'fido' + + - name: "do this if my favcolor is not blue, and my dog is named fido" + action: shell /bin/true + when_string: $favcolor != 'blue' and $dog == 'fido' + + - name: "do this if my SSN is over 9000" + action: shell /bin/true + when_integer: $ssn > 9000 + + - name: "do this if I have one of these SSNs" + action: shell /bin/true + when_integer: $ssn in [ 8675309, 8675310, 8675311 ] + + - name: "do this if a variable named hippo is NOT defined" + action: shell /bin/true + when_unset: $hippo + + - name: "do this if a variable named hippo is defined" + action: shell /bin/true + when_set: $hippo + + - name: "do this if a variable named hippo is true" + action: shell /bin/true + when_boolean: $hippo + +The when_boolean check will look for variables that look to be true as well, such as the string 'True' or +'true', non-zero numbers, and so on. + Conditional Imports ``````````````````` diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 42e09601f5f..355db227d4d 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -232,7 +232,7 @@ class Task(object): tcopy = tokens[1:] for (i, t) in enumerate(tcopy): if t.find("$") != -1: - tcopy[i] = "(is_set('''%s''') and '''%s'''.lower() not in ('false', 'none', '0', ''))" % (t, t) + tcopy[i] = "(is_set('''%s''') and '''%s'''.lower() not in ('false', 'no', 'n', 'none', '0', ''))" % (t, t) return " ".join(tcopy) else: From 21d858f36da9198e0349223b37c1c4c82cb492ed Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 26 Nov 2012 18:47:33 -0500 Subject: [PATCH 11/12] Document new types of plugins. --- docsite/rst/api.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docsite/rst/api.rst b/docsite/rst/api.rst index 5ca2b561241..a901c86b727 100644 --- a/docsite/rst/api.rst +++ b/docsite/rst/api.rst @@ -303,7 +303,19 @@ directory. Lookup Plugins -------------- -Language constructs like "with_fileglob" are implemnted via lookup plugins. Just like other plugin types, you can write your own. +Language constructs like "with_fileglob" and "with_items" are implemented via lookup plugins. Just like other plugin types, you can write your own. + +Vars Plugins +------------ + +Playbook constructs like 'host_vars' and 'group_vars' work via 'vars' plugins. They inject additional variable +data into ansible runs that did not come from an inventory, playbook, or command line. Note that variables +can also be returned from inventory, so in most cases, you won't need to write or understand vars_plugins. + +Filter Plugins +-------------- + +If you want more Jinja2 filters available in a Jinja2 template (filters like to_yaml and to_json are provided by default), they can be extended by writing a filter plugin. Distributing Plugins -------------------- @@ -317,6 +329,8 @@ to /usr/share/ansible/plugins, in a subfolder for each plugin type:: * lookup_plugins * callback_plugins * connection_plugins + * filter_plugins + * vars_plugins To change this path, edit the ansible configuration file. From dd5a8474f839dff3d600d0ba09351db55f8c6672 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Mon, 26 Nov 2012 18:50:26 -0500 Subject: [PATCH 12/12] Have module formatter ignore more types of files. --- hacking/module_formatter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hacking/module_formatter.py b/hacking/module_formatter.py index 6a28be16f87..217d325f170 100755 --- a/hacking/module_formatter.py +++ b/hacking/module_formatter.py @@ -283,7 +283,8 @@ def main(): fname = os.path.join(options.module_dir, module) extra = os.path.join("inc", "%s.tex" % module) - if fname.endswith(".swp"): + # probably could just throw out everything with extensions + if fname.endswith(".swp") or fname.endswith(".orig") or fname.endswith(".rej"): continue print " processing module source ---> %s" % fname