Repository: ansible/ansible

Issue:
Cows don't like unicode tasks
##### ISSUE TYPE
- Bug Report
##### COMPONENT NAME

Cowsay integration
##### ANSIBLE VERSION

```
ansible 2.1.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```

and

```
ansible 2.0.0.2
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

All defaults except `ANSIBLE_NOCOWS`
##### OS / ENVIRONMENT

Up-to-date Ubuntu 16.04
##### SUMMARY

Display cows yields unicode-related warning messages when runnin tasks with names containing non-ascii characters (ie. non-English phrases).
##### STEPS TO REPRODUCE

Run this playbook:

```

---
- name: Tâsk wïth Ûnïçöd€ name
  hosts: localhost
  user: root
  tasks:
    - ping:
```
##### EXPECTED RESULTS

Happy cows no matter the task.
##### ACTUAL RESULTS

With cowsay installed and ANSIBLE_NOCOWS unset, we get the following warning:

```
 [WARNING]: Failure using method (v2_playbook_on_play_start) in callback plugin
(<ansible.plugins.callback.default.CallbackModule object at 0x7f713dc6c110>): 'ascii' codec can't
decode byte 0xc3 in position 42: ordinal not in range(128)

 ______________
< TASK [setup] >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [localhost]
 _____________
< TASK [ping] >
 -------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [localhost]
 ____________
< PLAY RECAP >
 ------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

localhost                  : ok=2    changed=0    unreachable=0    failed=0   


```

With ANSIBLE_NOCOWS=1 no such warning is printed.

Tasks with only ASCII characters never get a warning message printed.



Discussion:
No such warning was printed in v1.9

This bug is not a big problem in itself, but it may confuse to users upgrading from v1.9, leading them to believe their playbooks are not compatible.


Resolving patch:
diff --git a/lib/ansible/plugins/callback/default.py b/lib/ansible/plugins/callback/default.py
--- a/lib/ansible/plugins/callback/default.py
+++ b/lib/ansible/plugins/callback/default.py
@@ -147,14 +147,14 @@ def _print_task_banner(self, task):
         # (shoulder surfing, logging stdout straight to a file, etc).
         args = ''
         if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
-            args = ', '.join(('%s=%s' % a for a in task.args.items()))
-            args = ' %s' % args
+            args = u', '.join(u'%s=%s' % a for a in task.args.items())
+            args = u' %s' % args
 
-        self._display.banner("TASK [%s%s]" % (task.get_name().strip(), args))
+        self._display.banner(u"TASK [%s%s]" % (task.get_name().strip(), args))
         if self._display.verbosity >= 2:
             path = task.get_path()
             if path:
-                self._display.display("task path: %s" % path, color=C.COLOR_DEBUG)
+                self._display.display(u"task path: %s" % path, color=C.COLOR_DEBUG)
 
         self._last_task_banner = task._uuid
 
@@ -167,9 +167,9 @@ def v2_playbook_on_handler_task_start(self, task):
     def v2_playbook_on_play_start(self, play):
         name = play.get_name().strip()
         if not name:
-            msg = "PLAY"
+            msg = u"PLAY"
         else:
-            msg = "PLAY [%s]" % name
+            msg = u"PLAY [%s]" % name
 
         self._play = play
 
diff --git a/lib/ansible/utils/display.py b/lib/ansible/utils/display.py
--- a/lib/ansible/utils/display.py
+++ b/lib/ansible/utils/display.py
@@ -58,7 +58,11 @@
     else:
         print("[WARNING]: log file at %s is not writeable and we cannot create it, aborting\n" % path, file=sys.stderr)
 
-
+b_COW_PATHS = (b"/usr/bin/cowsay",
+               b"/usr/games/cowsay",
+               b"/usr/local/bin/cowsay",  # BSD path for cowsay
+               b"/opt/local/bin/cowsay",  # MacPorts path for cowsay
+              )
 class Display:
 
     def __init__(self, verbosity=0):
@@ -71,35 +75,27 @@ def __init__(self, verbosity=0):
         self._warns        = {}
         self._errors       = {}
 
-        self.cowsay = None
+        self.b_cowsay = None
         self.noncow = C.ANSIBLE_COW_SELECTION
 
         self.set_cowsay_info()
 
-        if self.cowsay:
+        if self.b_cowsay:
             try:
-                cmd = subprocess.Popen([self.cowsay, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+                cmd = subprocess.Popen([self.b_cowsay, "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                 (out, err) = cmd.communicate()
-                self.cows_available = list(set(C.ANSIBLE_COW_WHITELIST).intersection(out.split()))
+                self.cows_available = [ to_bytes(c) for c in set(C.ANSIBLE_COW_WHITELIST).intersection(out.split())]
             except:
                 # could not execute cowsay for some reason
-                self.cowsay = False
+                self.b_cowsay = False
 
         self._set_column_width()
 
     def set_cowsay_info(self):
-
         if not C.ANSIBLE_NOCOWS:
-            if os.path.exists("/usr/bin/cowsay"):
-                self.cowsay = "/usr/bin/cowsay"
-            elif os.path.exists("/usr/games/cowsay"):
-                self.cowsay = "/usr/games/cowsay"
-            elif os.path.exists("/usr/local/bin/cowsay"):
-                # BSD path for cowsay
-                self.cowsay = "/usr/local/bin/cowsay"
-            elif os.path.exists("/opt/local/bin/cowsay"):
-                # MacPorts path for cowsay
-                self.cowsay = "/opt/local/bin/cowsay"
+            for b_cow_path in b_COW_PATHS:
+                if os.path.exists(b_cow_path):
+                    self.b_cowsay = b_cow_path
 
     def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False):
         """ Display a message to the user
@@ -231,7 +227,7 @@ def banner(self, msg, color=None):
         Prints a header-looking line with stars taking up to 80 columns
         of width (3 columns, minimum)
         '''
-        if self.cowsay:
+        if self.b_cowsay:
             try:
                 self.banner_cowsay(msg)
                 return
@@ -242,25 +238,25 @@ def banner(self, msg, color=None):
         star_len = (79 - len(msg))
         if star_len < 0:
             star_len = 3
-        stars = "*" * star_len
-        self.display("\n%s %s" % (msg, stars), color=color)
+        stars = u"*" * star_len
+        self.display(u"\n%s %s" % (msg, stars), color=color)
 
     def banner_cowsay(self, msg, color=None):
-        if ": [" in msg:
-            msg = msg.replace("[","")
-            if msg.endswith("]"):
+        if u": [" in msg:
+            msg = msg.replace(u"[", u"")
+            if msg.endswith(u"]"):
                 msg = msg[:-1]
-        runcmd = [self.cowsay,"-W", "60"]
+        runcmd = [self.b_cowsay, b"-W", b"60"]
         if self.noncow:
             thecow = self.noncow
-            if thecow == 'random':
+            if thecow == b'random':
                 thecow = random.choice(self.cows_available)
-            runcmd.append('-f')
+            runcmd.append(b'-f')
             runcmd.append(thecow)
-        runcmd.append(msg)
+        runcmd.append(to_bytes(msg))
         cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         (out, err) = cmd.communicate()
-        self.display("%s\n" % out, color=color)
+        self.display(u"%s\n" % to_text(out), color=color)
 
     def error(self, msg, wrap_text=True):
         if wrap_text:


Test patch: