Repository: ansible/ansible-modules-core

Issue:
service: nginx enabled=yes not enabling service ansible 1.7.2
##### Issue Type:

Bug Report
##### Ansible Version:

1.7.2

1.6.6
##### Environment:

Destination: Ubuntu 12.04.4 LTS

Control hosts: Mac OSX and Ubuntu 12.04.4 LTS
##### Summary:

When trying to enable nginx, which is a typical init.d startup script. It appears to not have any affect. 

Have tried with both ansible versions and with Mac OSX and Ubuntu 12.04 control hosts. Destination host is a Ubuntu 12.04
##### Steps To Reproduce:

Install nginx from deb and it will create a /etc/init.d/nginx script

In a task write:

```
  - name: start nginx on boot
    service: name=nginx state=started enabled=yes
    tags: nginx_boot
```

Run the playbook.

```
ansible-playbook site.yaml -i inventory/prod -l host02.example.com -t nginx_boot
```
##### Expected Results:

Service enabled
##### Actual Results:

Reports changed every time:

```
TASK: [nginx | start nginx on boot] *******************************************
changed: [host02.example.com]
```

chkconfig and rc.d directories unchanged:

```
root@host02:/etc# chkconfig nginx
nginx  off

root@host02:/etc# ls rc*/*nginx*
rc0.d/K00nginx  rc1.d/K00nginx  rc2.d/K00nginx  rc3.d/K00nginx  rc4.d/K00nginx  rc5.d/K00nginx  rc6.d/K00nginx
```



Discussion:
Outputs with -vvv

```
$ ansible-playbook site.yaml -i inventory/prod -l host02.example.com -t nginx_boot -vvv

PLAY [lbservers] **************************************************************

GATHERING FACTS ***************************************************************
<host02.example.com> ESTABLISH CONNECTION FOR USER: matt
<host02.example.com> REMOTE_MODULE setup
<host02.example.com> EXEC ['ssh', '-C', '-q', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=30m', '-o', 'ForwardAgent=yes', '-o', 'ControlPath=/Users/matt/.ansible/cp/ansible-ssh-%h-%p-%r', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey', '-o', 'PasswordAuthentication=no', '-o', 'ConnectTimeout=10', 'host02.example.com', u"/bin/sh -c 'LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /usr/bin/python'"]
ok: [host02.example.com]

TASK: [nginx | start nginx on boot] *******************************************
<host02.example.com> ESTABLISH CONNECTION FOR USER: matt
<host02.example.com> REMOTE_MODULE service name=nginx state=started enabled=yes
<host02.example.com> EXEC ['ssh', '-C', '-q', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=30m', '-o', 'ForwardAgent=yes', '-o', 'ControlPath=/Users/matt/.ansible/cp/ansible-ssh-%h-%p-%r', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey', '-o', 'PasswordAuthentication=no', '-o', 'ConnectTimeout=10', 'host02.example.com', u'/bin/sh -c \'sudo -k && sudo -H -S -p "[sudo via ansible, key=qftywqjfazjuzgpjzwtcjtilgmdimjof] password: " -u root /bin/sh -c \'"\'"\'echo SUDO-SUCCESS-qftywqjfazjuzgpjzwtcjtilgmdimjof; LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /usr/bin/python\'"\'"\'\'']
changed: [host02.example.com] => {"changed": true, "enabled": true, "name": "nginx", "state": "started"}

PLAY [logserver] **************************************************************
skipping: no hosts matched

PLAY RECAP ********************************************************************
host02.example.com      : ok=2    changed=1    unreachable=0    failed=0
```

vs just ansible:

```
$ ansible -m service -a "name=nginx enabled=yes" host02.example.com -vvv -i inventory/prod
<host02.example.com> ESTABLISH CONNECTION FOR USER: matt
<host02.example.com> REMOTE_MODULE service name=nginx enabled=yes
<host02.example.com> EXEC ['ssh', '-C', '-q', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=30m', '-o', 'ForwardAgent=yes', '-o', 'ControlPath=/Users/matt/.ansible/cp/ansible-ssh-%h-%p-%r', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey', '-o', 'PasswordAuthentication=no', '-o', 'ConnectTimeout=10', 'host02.example.com', u"/bin/sh -c 'LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /usr/bin/python'"]
host02.example.com | success >> {
    "changed": true,
    "enabled": true,
    "name": "nginx"
}
```

note not just for nginx, but memcached did the same thing too. Trying it on a different host also the same thing.

Looking at what commands it runs and what is installed (chkconfig vs update-rc.d) I decided to try see what it's doing. First I removed chkconfig to go back to normal (update-rc.d). And tried doing what I saw the python code was doing.

```
# update-rc.d -n nginx enable
update-rc.d: warning: nginx start runlevel arguments (none) do not match LSB Default-Start values (2 3 4 5)
update-rc.d: warning: nginx stop runlevel arguments (none) do not match LSB Default-Stop values (0 1 6)
 Enabling system startup links for /etc/init.d/nginx ...
 Removing any system startup links for /etc/init.d/nginx ...
   /etc/rc0.d/K00nginx
   /etc/rc1.d/K00nginx
   /etc/rc2.d/K00nginx
   /etc/rc3.d/K00nginx
   /etc/rc4.d/K00nginx
   /etc/rc5.d/K00nginx
   /etc/rc6.d/K00nginx
 System start/stop links for /etc/init.d/nginx already exist.
```

and for a service that is working:

```
# update-rc.d -n postfix enable
update-rc.d: warning: postfix start runlevel arguments (none) do not match LSB Default-Start values (2 3 4 5)
update-rc.d: warning: postfix stop runlevel arguments (none) do not match LSB Default-Stop values (0 1 6)
 Enabling system startup links for /etc/init.d/postfix ...
 Removing any system startup links for /etc/init.d/postfix ...
   /etc/rc0.d/K20postfix
   /etc/rc1.d/K20postfix
   /etc/rc2.d/S20postfix
   /etc/rc3.d/S20postfix
   /etc/rc4.d/S20postfix
   /etc/rc5.d/S20postfix
   /etc/rc6.d/K20postfix
 System start/stop links for /etc/init.d/postfix already exist.
```

if I went and removed it with update-rc.d -f nginx remove and then added it back in with defaults. It's working now.

```
# update-rc.d -f nginx remove
 Removing any system startup links for /etc/init.d/nginx ...
   /etc/rc0.d/K00nginx
   /etc/rc1.d/K00nginx
   /etc/rc2.d/K00nginx
   /etc/rc3.d/K00nginx
   /etc/rc4.d/K00nginx
   /etc/rc5.d/K00nginx
   /etc/rc6.d/K00nginx
```

```
etc# update-rc.d nginx defaults
 Adding system startup for /etc/init.d/nginx ...
   /etc/rc0.d/K20nginx -> ../init.d/nginx
   /etc/rc1.d/K20nginx -> ../init.d/nginx
   /etc/rc6.d/K20nginx -> ../init.d/nginx
   /etc/rc2.d/S20nginx -> ../init.d/nginx
   /etc/rc3.d/S20nginx -> ../init.d/nginx
   /etc/rc4.d/S20nginx -> ../init.d/nginx
   /etc/rc5.d/S20nginx -> ../init.d/nginx
```

Shouldn't have to do this though, ansible should actually check if a service is enabled. 

Note from man page:

```
   If  any   files   /etc/rcrunlevel.d/[SK]??name   already   exist   then
   update-rc.d  does nothing.  The program was written this way so that it
   will never change  an  existing  configuration,  which  may  have  been
   customized  by the system administrator.  The program will only install
   links if none are present, i.e., if it appears  that  the  service  has
   never been installed before.
```

So I must have had the K\* things previously, and so it was never enabled. Even though ansible was telling me it was.

I think this could be solved by changing:

https://github.com/ansible/ansible-modules-core/blob/devel/system/service.py#L781

```
(rc, out, err) = self.execute_command("%s -n %s %s" \
                                                  % (self.enable_cmd, self.name, action))
```

to not run in dry run mode. That command actually runs:

```
update-rc.d -n nginx enable
```

But the help for update-rc.d says -n means not really:

```
# update-rc.d --help
usage: update-rc.d [-n] [-f] <basename> remove
       update-rc.d [-n] <basename> defaults [NN | SS KK]
       update-rc.d [-n] <basename> start|stop NN runlvl [runlvl] [...] .
       update-rc.d [-n] <basename> disable|enable [S|2|3|4|5]
                -n: not really
                -f: force

The disable|enable API is not stable and might change in the future.
```

When I ran without: 

```
# update-rc.d nginx enable
```

It enables the service.

@oppianmatt https://github.com/ansible/ansible-modules-core/blob/devel/system/service.py#L797 just tries to detect if something would change. The real call is at https://github.com/ansible/ansible-modules-core/blob/devel/system/service.py#L838

So that should be correct.

I'm seeing this same problem in version 1.8.2 of ansible.

can you check current devel?

Through code inspection, I can see that the relevant code hasn't been altered in devel.  The problem should still exist.

It problem appears to be in the change detection logic that begins at https://github.com/ansible/ansible-modules-core/blob/release1.8.2/system/service.py#L797. In the output of a `update-rc.d -n nginx enable` call, three conditions are used to detect the need for a change.
- a line that begins with `rename` in stdout
- a line containing `do not exist` in stdout
- a line containing `no runlevel symlinks to modify` in stderr

Here's the actual stdout with nginx hasn't been enabled.

```
# update-rc.d -n nginx enable 2>/dev/null
 Enabling system startup links for /etc/init.d/nginx ...
 Removing any system startup links for /etc/init.d/nginx ...
   /etc/rc0.d/K00nginx
   /etc/rc1.d/K00nginx
   /etc/rc2.d/K00nginx
   /etc/rc3.d/K00nginx
   /etc/rc4.d/K00nginx
   /etc/rc5.d/K00nginx
   /etc/rc6.d/K00nginx
 System start/stop links for /etc/init.d/nginx already exist.
```

And here's the actual stderr.

```
# update-rc.d -n nginx enable >/dev/null
update-rc.d: warning: nginx start runlevel arguments (none) do not match LSB Default-Start values (2 3 4 5)
update-rc.d: warning: nginx stop runlevel arguments (none) do not match LSB Default-Stop values (0 1 6)
```

None of the conditions hold in the output.


Resolving patch:
diff --git a/system/service.py b/system/service.py
--- a/system/service.py
+++ b/system/service.py
@@ -105,6 +105,7 @@
 import select
 import time
 import string
+import glob
 
 from distutils.version import LooseVersion
 
@@ -734,44 +735,36 @@ def write_to_override_file(file_name, file_contents, ):
         # update-rc.d style
         #
         if self.enable_cmd.endswith("update-rc.d"):
-            if self.enable:
-                action = 'enable'
-            else:
-                action = 'disable'
 
-            if self.enable:
-                # make sure the init.d symlinks are created
-                # otherwise enable might not work
-                (rc, out, err) = self.execute_command("%s %s defaults" \
-                                                      % (self.enable_cmd, self.name))
+            enabled = False
+            slinks = glob.glob('/etc/rc?.d/S??' + self.name)
+            if slinks:
+                enabled = True
+
+            if self.enable != enabled:
+                self.changed = True
+
+                if self.enable:
+                    action = 'enable'
+                    klinks = glob.glob('/etc/rc?.d/K??' + self.name)
+                    if not klinks:
+                        (rc, out, err) = self.execute_command("%s %s defaults"  % (self.enable_cmd, self.name))
+                        if rc != 0:
+                            if err:
+                                self.module.fail_json(msg=err)
+                            else:
+                                self.module.fail_json(msg=out) % (self.enable_cmd, self.name, action)
+                else:
+                    action = 'disable'
+
+                (rc, out, err) = self.execute_command("%s %s %s"  % (self.enable_cmd, self.name, action))
                 if rc != 0:
                     if err:
                         self.module.fail_json(msg=err)
                     else:
-                        self.module.fail_json(msg=out)
+                        self.module.fail_json(msg=out) % (self.enable_cmd, self.name, action)
 
-            (rc, out, err) = self.execute_command("%s -n %s %s" \
-                                                  % (self.enable_cmd, self.name, action))
-            self.changed = False
-            for line in out.splitlines():
-                if line.startswith('rename'):
-                    self.changed = True
-                    break
-                elif self.enable and 'do not exist' in line:
-                    self.changed = True
-                    break
-                elif not self.enable and 'already exist' in line:
-                    self.changed = True
-                    break
-
-            # Debian compatibility
-            for line in err.splitlines():
-                if self.enable and 'no runlevel symlinks to modify' in line:
-                    self.changed = True
-                    break
-
-            if not self.changed:
-                return
+            return
 
         #
         # If we've gotten to the end, the service needs to be updated


Test patch: