Repository: ansible/ansible

Issue:
Serial percentage does not work
$ ansible --version
    ansible 2.0.0.2
      config file = /etc/ansible/ansible.cfg
      configured module search path = Default w/o overrides

```
$ grep -v "^#" /etc/ansible/ansible.cfg | grep -v "^$"
[defaults]
inventory      = /etc/ansible/hosts
remote_tmp     = $HOME/.ansible/tmp
pattern        = *
forks          = 64
poll_interval  = 15
sudo_user      = root
transport      = smart
module_lang    = C
gathering = implicit
host_key_checking = False
sudo_exe = sudo
timeout = 3
ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
action_plugins     = /usr/share/ansible_plugins/action_plugins
callback_plugins   = /usr/share/ansible_plugins/callback_plugins
connection_plugins = /usr/share/ansible_plugins/connection_plugins
lookup_plugins     = /usr/share/ansible_plugins/lookup_plugins
vars_plugins       = /usr/share/ansible_plugins/vars_plugins
filter_plugins     = /usr/share/ansible_plugins/filter_plugins
fact_caching = memory
retry_files_enabled = False
[privilege_escalation]
[paramiko_connection]
[ssh_connection]
control_path = %(directory)s/%%h-%%r
pipelining = True
[accelerate]
[selinux]
```

Environment: Arch linux, CentOS 7, CentOS 6

Summary:
Specifying a percentage, such as "10%", for the value of `serial` in a playbook does not work as expected; it runs against all hosts in parallel. Specifying a number, such as "1", works as expected; it runs against one host at a time.

Steps to reproduce:

``` sh
    $ cat inventory 
    someserver[01:02].surveysampling.com
    $ 
    $ 
    $ 
    $ cat site_number.yml 
    - hosts: all
      remote_user: "XXXXXXX"
      serial: "1"
      gather_facts: false
      tasks:
        - name: task1
          shell: "pwd"
        - name: task2
          shell: "ls"
    $ ansible-playbook -i inventory site_number.yml 

    PLAY ***************************************************************************

    TASK [task1] *******************************************************************
    changed: [someserver01.surveysampling.com]

    TASK [task2] *******************************************************************
    changed: [someserver01.surveysampling.com]

    PLAY ***************************************************************************

    TASK [task1] *******************************************************************
    changed: [someserver02.surveysampling.com]

    TASK [task2] *******************************************************************
    changed: [someserver02.surveysampling.com]

    PLAY RECAP *********************************************************************
    someserver01.surveysampling.com : ok=2    changed=2    unreachable=0    failed=0   
    someserver02.surveysampling.com : ok=2    changed=2    unreachable=0    failed=0   

    $ # one at a time, good
    $ 
    $ 
    $ 
    $ diff -u0 site_number.yml site_percent.yml 
    --- site_number.yml     2016-02-05 11:53:16.718032043 -0500
    +++ site_percent.yml    2016-02-05 14:30:21.029121069 -0500
    @@ -3 +3 @@
    -  serial: "1"
    +  serial: "10%"
    $ 
    $ 
    $ 
    $ cat site_percent.yml 
    - hosts: all
      remote_user: "XXXXXXXX"
      serial: "10%"
      gather_facts: false
      tasks:
        - name: task1
          shell: "pwd"
        - name: task2
          shell: "ls"
    $ ansible-playbook -i inventory site_percent.yml 

    PLAY ***************************************************************************

    TASK [task1] *******************************************************************
    changed: [someserver02.surveysampling.com]
    changed: [someserver01.surveysampling.com]

    TASK [task2] *******************************************************************
    changed: [someserver02.surveysampling.com]
    changed: [someserver01.surveysampling.com]

    PLAY RECAP *********************************************************************
    someserver01.surveysampling.com : ok=2    changed=2    unreachable=0    failed=0   
    someserver02.surveysampling.com : ok=2    changed=2    unreachable=0    failed=0   

    $ # expected: one at a time, actual: two at a time
```

(Server names and usernames are masked in the above output.)

See the documentation for the serial keyword here: http://docs.ansible.com/ansible/playbooks_delegation.html

Might be related to #12698.



Discussion:
Still seeing this, has it been looked at/fixed in later versions?

We are also seeing this. Version 2.0.1.0 
Good thing we caught it before running our deploy tasks on production!

This bug still affect Ansible 2.0.1.0
My temporary workaround to get %50 of nodes is:

> serial: "{{play_hosts | length // 2 }}"

But it totally depends on how your inventory looks like.

We are also seeing this running with the following:

ansible 2.1.0 (devel 0ab29f573a) last updated 2016/03/02 22:45:54 (GMT +000)
  lib/ansible/modules/core: (detached HEAD 0bbb7ba38d) last updated 2016/03/02 22:50:34 (GMT +000)
  lib/ansible/modules/extras: (detached HEAD 39e4040685) last updated 2016/03/02 22:50:34 (GMT +000)

Also seeing this issue with `ansible 2.0.1.0`


Resolving patch:
diff --git a/lib/ansible/executor/playbook_executor.py b/lib/ansible/executor/playbook_executor.py
--- a/lib/ansible/executor/playbook_executor.py
+++ b/lib/ansible/executor/playbook_executor.py
@@ -214,7 +214,7 @@ def _get_serialized_batches(self, play):
         # and convert it to an integer value based on the number of hosts
         if isinstance(play.serial, string_types) and play.serial.endswith('%'):
             serial_pct = int(play.serial.replace("%",""))
-            serial = int((serial_pct/100.0) * len(all_hosts))
+            serial = int((serial_pct/100.0) * len(all_hosts)) or 1
         else:
             if play.serial is None:
                 serial = -1


Test patch:
diff --git a/test/units/executor/test_playbook_executor.py b/test/units/executor/test_playbook_executor.py
--- a/test/units/executor/test_playbook_executor.py
+++ b/test/units/executor/test_playbook_executor.py
@@ -92,3 +92,14 @@ def test_playbook_executor__get_serialized_batches(self):
         mock_inventory.get_hosts.return_value = ['host0','host1','host2','host3','host4','host5','host6','host7','host8','host9']
         self.assertEqual(pbe._get_serialized_batches(play), [['host0','host1'],['host2','host3'],['host4','host5'],['host6','host7'],['host8','host9']])
 
+        # Test when serial percent is under 1.0
+        playbook = Playbook.load(pbe._playbooks[2], variable_manager=mock_var_manager, loader=fake_loader)
+        play = playbook.get_plays()[0]
+        mock_inventory.get_hosts.return_value = ['host0','host1','host2']
+        self.assertEqual(pbe._get_serialized_batches(play), [['host0'],['host1'],['host2']])
+
+        # Test when there is a remainder for serial as a percent
+        playbook = Playbook.load(pbe._playbooks[2], variable_manager=mock_var_manager, loader=fake_loader)
+        play = playbook.get_plays()[0]
+        mock_inventory.get_hosts.return_value = ['host0','host1','host2','host3','host4','host5','host6','host7','host8','host9','host10']
+        self.assertEqual(pbe._get_serialized_batches(play), [['host0','host1'],['host2','host3'],['host4','host5'],['host6','host7'],['host8','host9'],['host10']])