Repository: ansible/ansible-modules-core

Issue:
cron duplicates cron jobs if '#Ansible: ' comment isn't the start of the preceeding line
##### Issue Type:

<!-- Please pick one and delete the rest: -->
- Bug Report
##### Plugin Name:

cron
##### Ansible Version:

```
ansible 1.9.4
  configured module search path = None
```
##### Ansible Configuration:

```
[defaults]
forks=0
inventory=./hosts
```
##### Environment:

<!-- Please mention the OS you are running Ansible from, and the OS you are managing,
or say “N/A” for anything that isn't platform-specific. -->

```
$ python --version
Python 2.7.10
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.2
BuildVersion:   15C50
```
##### Summary:

If a cronjob exists but lacks a corresponding `#Ansible:` comment on the previous line, a duplicate entry will be added. This is issue manifests itself when converting an existing crontab over to ansible. If you have preexisting cronjobs, and you'd like to now manage them via ansible, you'd expect that adding an ansible task for a preexisting cronjob wouldn't do anything (except maybe add `#Ansible: cronjob-name` to the preceeding line).

Specifically, the cron.CronTab class has an attribute `self.ansible == "#Ansible: "` that is used in the method `cron.CronTab.find_job` to find cronjobs via their preceding comments. If the cronjob doesn't have a corresponding comment, it's invisible to the cron module.
##### Steps To Reproduce:
1. Manually add cronjob via crontab -e
   
   ```
   $ sudo crontab -l
   # DO NOT EDIT THIS FILE - edit the master and reinstall.
   # (/tmp/crontab6ASD42 installed on Tue Mar 15 18:40:15 2016)
   # (Cron version V5.0 -- $Id: crontab.c,v 1.12 2004/01/23 18:56:42 vixie Exp $)
   * * * * * /bin/true
   ```
2. Add cron task with identical job
   
   ```
   - name: add test cronjob
   cron: name="test" user=root job="/bin/true"
   ```
##### Expected Results:

I expect a single cronjob to be present

```
$ sudo crontab -l
# DO NOT EDIT THIS FILE - edit the master and reinstall.
# (/tmp/crontab8GGP41 installed on Tue Mar 15 18:42:05 2016)
# (Cron version V5.0 -- $Id: crontab.c,v 1.12 2004/01/23 18:56:42 vixie Exp $)
#Ansible: test
* * * * * /bin/true
```
##### Actual Results:

A second, identical cronjob has been added

```
$ sudo crontab -l
# DO NOT EDIT THIS FILE - edit the master and reinstall.
# (/tmp/crontab8GGP41 installed on Tue Mar 15 18:42:05 2016)
# (Cron version V5.0 -- $Id: crontab.c,v 1.12 2004/01/23 18:56:42 vixie Exp $)
* * * * * /bin/true
#Ansible: test
* * * * * /bin/true
```



Discussion:
This is not a bug, and also noted in the docs.There must be a name to reference a cron entry. How would you handle this otherwise? What if you change the schedule? Without the name comment above it would just add another entry.

Should the 'name' param be required then?

It would be nice to allow the case of changed name (could be None -> "new name") with identical job, which is doable. Just look for the exact line and add/update the name.

So, if you omit the name (as I did), ansible will happily add the same job at each run. That's what is written in the documentation, but I am not sure to understand when would anyone want that in the first place.

@dsummersl, @EvanK, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/jctanner/ansibullbot/blob/ISSUE_TRIAGE_CLASS/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->


Resolving patch:
diff --git a/system/cron.py b/system/cron.py
--- a/system/cron.py
+++ b/system/cron.py
@@ -307,9 +307,12 @@ def write(self, backup_file=None):
             if rc != 0:
                 self.module.fail_json(msg=err)
 
+    def do_comment(self, name):
+        return "%s%s" % (self.ansible, name)
+
     def add_job(self, name, job):
         # Add the comment
-        self.lines.append("%s%s" % (self.ansible, name))
+        self.lines.append(self.do_comment(name))
 
         # Add the job
         self.lines.append("%s" % (job))
@@ -370,7 +373,8 @@ def remove_job_file(self):
         except:
             raise CronTabError("Unexpected error:", sys.exc_info()[0])
 
-    def find_job(self, name):
+    def find_job(self, name, job=None):
+        # attempt to find job by 'Ansible:' header comment
         comment = None
         for l in self.lines:
             if comment is not None:
@@ -381,6 +385,19 @@ def find_job(self, name):
             elif re.match( r'%s' % self.ansible, l):
                 comment = re.sub( r'%s' % self.ansible, '', l)
 
+        # failing that, attempt to find job by exact match
+        if job:
+            for i, l in enumerate(self.lines):
+                if l == job:
+                    # if no leading ansible header, insert one
+                    if not re.match( r'%s' % self.ansible, self.lines[i-1]):
+                        self.lines.insert(i, self.do_comment(name))
+                        return [self.lines[i], l, True]
+                    # if a leading blank ansible header AND job has a name, update header
+                    elif name and self.lines[i-1] == self.do_comment(None):
+                        self.lines[i-1] = self.do_comment(name)
+                        return [self.lines[i-1], l, True]
+
         return []
 
     def find_env(self, name):
@@ -431,7 +448,7 @@ def get_envnames(self):
         return envnames
 
     def _update_job(self, name, job, addlinesfunction):
-        ansiblename = "%s%s" % (self.ansible, name)
+        ansiblename = self.do_comment(name)
         newlines = []
         comment = None
 
@@ -652,17 +669,22 @@ def main():
                 crontab.remove_env(name)
                 changed = True
     else:
-        old_job = crontab.find_job(name)
-
         if do_install:
             job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time, disabled)
+            old_job = crontab.find_job(name, job)
+
             if len(old_job) == 0:
                 crontab.add_job(name, job)
                 changed = True
             if len(old_job) > 0 and old_job[1] != job:
                 crontab.update_job(name, job)
                 changed = True
+            if len(old_job) > 2:
+                crontab.update_job(name, job)
+                changed = True
         else:
+            old_job = crontab.find_job(name)
+
             if len(old_job) > 0:
                 crontab.remove_job(name)
                 changed = True


Test patch: