1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
|
# (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
# based on the log_plays example
# skvidal@fedoraproject.org
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import os
import time
import json
import pwd
from ansible import utils
try:
from ansible.plugins.callback import CallbackBase
except ImportError:
# Ansible v1 compat
CallbackBase = object
TIME_FORMAT="%b %d %Y %H:%M:%S"
MSG_FORMAT="%(now)s\t%(count)s\t%(category)s\t%(name)s\t%(data)s\n"
LOG_PATH = '/var/log/ansible'
def getlogin():
try:
user = os.getlogin()
except OSError, e:
user = pwd.getpwuid(os.geteuid())[0]
return user
class LogMech(object):
def __init__(self):
self.started = time.time()
self.pid = str(os.getpid())
self._pb_fn = None
self._last_task_start = None
self.play_info = {}
self.logpath = LOG_PATH
if not os.path.exists(self.logpath):
try:
os.makedirs(self.logpath, mode=0750)
except OSError, e:
if e.errno != 17:
raise
# checksum of full playbook?
@property
def playbook_id(self):
if self._pb_fn:
return os.path.basename(self._pb_fn).replace('.yml', '').replace('.yaml', '')
else:
return "ansible-cmd"
@playbook_id.setter
def playbook_id(self, value):
self._pb_fn = value
@property
def logpath_play(self):
# this is all to get our path to look nice ish
tstamp = time.strftime('%Y/%m/%d/%H.%M.%S', time.localtime(self.started))
path = os.path.normpath(self.logpath + '/' + self.playbook_id + '/' + tstamp + '/')
if not os.path.exists(path):
try:
os.makedirs(path)
except OSError, e:
if e.errno != 17: # if it is not dir exists then raise it up
raise
return path
def play_log(self, content):
# record out playbook.log
# include path to playbook, checksums, user running playbook
# any args we can get back from the invocation
fd = open(self.logpath_play + '/' + 'playbook-' + self.pid + '.info', 'a')
fd.write('%s\n' % content)
fd.close()
def task_to_json(self, task):
res = {}
res['task_name'] = task.name
res['task_module'] = task.module_name
res['task_args'] = task.module_args
if self.playbook_id == 'ansible-cmd':
res['task_userid'] = getlogin()
for k in ("delegate_to", "environment", "with_first_found",
"local_action", "notified_by", "notify",
"register", "sudo", "sudo_user", "tags",
"transport", "when"):
v = getattr(task, k, None)
if v:
res['task_' + k] = v
return res
def log(self, host, category, data, task=None, count=0):
if not host:
host = 'HOSTMISSING'
if type(data) == dict:
name = data.get('module_name',None)
else:
name = "unknown"
# we're in setup - move the invocation info up one level
if 'invocation' in data:
invoc = data['invocation']
if not name and 'module_name' in invoc:
name = invoc['module_name']
#don't add this since it can often contain complete passwords :(
del(data['invocation'])
if task:
name = task.name
data['task_start'] = self._last_task_start
data['task_end'] = time.time()
data.update(self.task_to_json(task))
if 'task_userid' not in data:
data['task_userid'] = getlogin()
if category == 'OK' and data.get('changed', False):
category = 'CHANGED'
if self.play_info.get('check', False) and self.play_info.get('diff', False):
category = 'CHECK_DIFF:' + category
elif self.play_info.get('check', False):
category = 'CHECK:' + category
# Sometimes this is None.. othertimes it's fine. Othertimes it has
# trailing whitespace that kills logview. Strip that, when possible.
if name:
name = name.strip()
sanitize_host = host.replace(' ', '_').replace('>', '-')
fd = open(self.logpath_play + '/' + sanitize_host + '.log', 'a')
now = time.strftime(TIME_FORMAT, time.localtime())
fd.write(MSG_FORMAT % dict(now=now, name=name, count=count, category=category, data=json.dumps(data)))
fd.close()
logmech = LogMech()
class CallbackModule(CallbackBase):
"""
logs playbook results, per host, in /var/log/ansible/hosts
"""
CALLBACK_NAME = 'logdetail'
CALLBACK_TYPE = 'notification'
CALLBACK_VERSION = 2.0
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
self._task_count = 0
self._play_count = 0
def on_any(self, *args, **kwargs):
pass
def runner_on_failed(self, host, res, ignore_errors=False):
category = 'FAILED'
task = getattr(self,'task', None)
logmech.log(host, category, res, task, self._task_count)
def runner_on_ok(self, host, res):
category = 'OK'
task = getattr(self,'task', None)
logmech.log(host, category, res, task, self._task_count)
def runner_on_error(self, host, res):
category = 'ERROR'
task = getattr(self,'task', None)
logmech.log(host, category, res, task, self._task_count)
def runner_on_skipped(self, host, item=None):
category = 'SKIPPED'
task = getattr(self,'task', None)
res = {}
res['item'] = item
logmech.log(host, category, res, task, self._task_count)
def runner_on_unreachable(self, host, output):
category = 'UNREACHABLE'
task = getattr(self,'task', None)
res = {}
res['output'] = output
logmech.log(host, category, res, task, self._task_count)
def runner_on_no_hosts(self):
pass
def runner_on_async_poll(self, host, res, jid, clock):
pass
def runner_on_async_ok(self, host, res, jid):
pass
def runner_on_async_failed(self, host, res, jid):
category = 'ASYNC_FAILED'
task = getattr(self,'task', None)
logmech.log(host, category, res, task, self._task_count)
def playbook_on_start(self):
pass
def playbook_on_notify(self, host, handler):
pass
def playbook_on_no_hosts_matched(self):
pass
def playbook_on_no_hosts_remaining(self):
pass
def playbook_on_task_start(self, name, is_conditional):
logmech._last_task_start = time.time()
self._task_count += 1
def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
pass
def playbook_on_setup(self):
self._task_count += 1
pass
def playbook_on_import_for_host(self, host, imported_file):
task = getattr(self,'task', None)
res = {}
res['imported_file'] = imported_file
logmech.log(host, 'IMPORTED', res, task)
def playbook_on_not_import_for_host(self, host, missing_file):
task = getattr(self,'task', None)
res = {}
res['missing_file'] = missing_file
logmech.log(host, 'NOTIMPORTED', res, task)
def playbook_on_play_start(self, pattern):
self._task_count = 0
play = getattr(self, 'play', None)
if play:
# figure out where the playbook FILE is
path = os.path.abspath(play.playbook.filename)
# tel the logger what the playbook is
logmech.playbook_id = path
# if play count == 0
# write out playbook info now
if not self._play_count:
pb_info = {}
pb_info['playbook_start'] = time.time()
pb_info['playbook'] = path
pb_info['userid'] = getlogin()
pb_info['extra_vars'] = play.playbook.extra_vars
pb_info['inventory'] = play.playbook.inventory.host_list
pb_info['playbook_checksum'] = utils.md5(path)
pb_info['check'] = play.playbook.check
pb_info['diff'] = play.playbook.diff
logmech.play_log(json.dumps(pb_info, indent=4))
self._play_count += 1
# then write per-play info that doesn't duplcate the playbook info
info = {}
info['play'] = play.name
info['hosts'] = play.hosts
info['transport'] = play.transport
info['number'] = self._play_count
info['check'] = play.playbook.check
info['diff'] = play.playbook.diff
logmech.play_info = info
logmech.play_log(json.dumps(info, indent=4))
def playbook_on_stats(self, stats):
results = {}
for host in stats.processed.keys():
results[host] = stats.summarize(host)
logmech.log(host, 'STATS', results[host])
logmech.play_log(json.dumps({'stats': results}, indent=4))
logmech.play_log(json.dumps({'playbook_end': time.time()}, indent=4))
print 'logs written to: %s' % logmech.logpath_play
|