2
2
Fabric tools for managing crontab tasks
3
3
"""
4
4
from __future__ import with_statement
5
+ from contextlib import closing
5
6
6
7
from tempfile import NamedTemporaryFile
7
8
@@ -13,9 +14,11 @@ def add_task(name, timespec, user, command):
13
14
"""
14
15
Add a cron task
15
16
"""
17
+ entry = '%(timespec)s %(user)s %(command)s\n ' % locals ()
16
18
with NamedTemporaryFile () as script :
17
- script .write ('%(timespec)s %(user)s %(command)s\n ' % locals ())
18
- script .flush ()
19
+ cm = CrontabManager (script .name )
20
+ cm .add_task (entry , entry )
21
+ cm .write_crontab ()
19
22
upload_template ('/etc/cron.d/%(name)s' % locals (),
20
23
script .name ,
21
24
context = {},
@@ -28,3 +31,122 @@ def add_daily(name, user, command):
28
31
Add a cron task to run daily
29
32
"""
30
33
add_task (name , '@daily' , user , command )
34
+
35
+
36
+ def remove_task (name , timespec , user , command ):
37
+ """
38
+ Add a cron task
39
+ """
40
+ entry = '%(timespec)s %(user)s %(command)s\n ' % locals ()
41
+ # FIXME: This is wrong, the remote cron file should be retrieved
42
+ # instead of writing to a temp file.
43
+ with NamedTemporaryFile () as script :
44
+ cm = CrontabManager (script .name )
45
+ cm .remove_task (entry , entry )
46
+ cm .write_crontab ()
47
+ upload_template ('/etc/cron.d/%(name)s' % locals (),
48
+ script .name ,
49
+ context = {},
50
+ chown = True ,
51
+ use_sudo = True )
52
+
53
+
54
+ class CrontabManager (object ):
55
+ """
56
+ Helper class to edit entries in user crontabs (see man 5 crontab)
57
+
58
+ Based heavily on the UserCrontabManager from z3c.recipe.usercrontab.
59
+ """
60
+ prepend = '# START %s (generated by fabtools)'
61
+ append = '# END %s (generated by fabtools)'
62
+
63
+ def __init__ (self , path ):
64
+ self .crontab = []
65
+ self ._path = path
66
+
67
+ def read_crontab (self ):
68
+ with closing (open (self ._path , 'r' )) as f :
69
+ self .crontab = [l .strip ("\n " ) for l in f ]
70
+
71
+ def write_crontab (self ):
72
+ with closing (open (self ._path , 'w' )) as f :
73
+ for l in self .crontab :
74
+ f .write ("%s\n " % l )
75
+ f .flush ()
76
+
77
+ def __repr__ (self ):
78
+ return "\n " .join (self .crontab )
79
+
80
+ def find_boundaries (self , name ):
81
+ start = None
82
+ end = None
83
+ for line_number , line in enumerate (self .crontab ):
84
+ if line .strip () == self .prepend % name :
85
+ if start is not None :
86
+ raise RuntimeError ("%s found twice in the same crontab. "
87
+ "Fix by hand." % (self .prepend % name ))
88
+ start = line_number
89
+ if line .strip () == self .append % name :
90
+ if end is not None :
91
+ raise RuntimeError ("%s found twice in the same crontab. "
92
+ "Fix by hand." % (self .append % name ))
93
+ end = line_number + 1
94
+ # ^^^ +1 as we want the range boundary and that is behind the
95
+ # element.
96
+ return start , end
97
+
98
+ def has_entry (self , name ):
99
+ """
100
+ Check if the crontab has an entry
101
+ """
102
+ start , end = self .find_boundaries (name )
103
+ return start is not None and end is not None
104
+
105
+ def add_entry (self , name , entry ):
106
+ """
107
+ Add an entry to the crontab.
108
+
109
+ Find lines enclosed by APPEND/PREPEND, zap and re-add.
110
+ """
111
+ start , end = self .find_boundaries (name )
112
+ inject_at = - 1 # By default at the end of the file.
113
+ if start is not None and end is not None :
114
+ # But preferably in our existing location.
115
+ self .crontab [start :end ] = []
116
+ inject_at = start
117
+ # There is some white space already, so we only insert
118
+ # entry and markers.
119
+ to_inject = [self .prepend % name , entry , self .append % name ]
120
+ else :
121
+ # Insert entry, markers, and white space
122
+ to_inject = ['' , self .prepend % name , entry , self .append % name ]
123
+
124
+ if inject_at == - 1 :
125
+ # [-1:-1] would inject before the last item...
126
+ self .crontab += to_inject
127
+ else :
128
+ self .crontab [inject_at :inject_at ] = to_inject
129
+
130
+ def del_entry (self , name ):
131
+ """
132
+ Remove an entry from a crontab.
133
+ """
134
+ start , end = self .find_boundaries (name )
135
+ if start is not None and end is not None :
136
+ if start > 0 :
137
+ if not self .crontab [start - 1 ].strip ():
138
+ # Also strip empty line in front.
139
+ start = start - 1
140
+ if end < len (self .crontab ):
141
+ if not self .crontab [end ].strip ():
142
+ # Also strip empty line after end marker.
143
+ # Note: not self.crontab[end + 1] as end is the location
144
+ # AFTER the end marker to selected it with [start:end].
145
+ end = end + 1
146
+ if end == len (self .crontab ):
147
+ end = None # Otherwise the last line stays in place
148
+ self .crontab [start :end ] = []
149
+ return 1 # Number of entries that are removed.
150
+
151
+ # Nothing removed.
152
+ return 0
0 commit comments