|
1 | | -############################################################################## |
2 | | -# |
3 | | -# Copyright (C) Zenoss, Inc. 2013, all rights reserved. |
4 | | -# |
5 | | -# This content is made available according to terms specified in |
6 | | -# License.zenoss under the directory where your Zenoss product is installed. |
7 | | -# |
8 | | -############################################################################## |
9 | | - |
10 | | -import logging |
11 | | -import re |
12 | | -import os |
13 | | -import time |
14 | | -import signal |
15 | | -from contextlib import contextmanager |
16 | | -from sre_parse import parse_template |
17 | | -from md5 import md5 |
18 | | - |
19 | | -from Products.ZenUtils.Utils import prepId |
20 | | - |
21 | | -log = logging.getLogger("zen.osprocessmatcher") |
22 | | - |
23 | | -BLANK_PARSE_TEMPLATE = ([],[]) |
24 | | - |
25 | | -class OSProcessClassMatcher(object): |
26 | | - """ |
27 | | - Mixin class, for process command line matching functionality common to |
28 | | - OSProcessClass and OSProcess. |
29 | | -
|
30 | | - Classes which mixin OSProcessClassMatcher must provide: |
31 | | - self.includeRegex: string |
32 | | - self.excludeRegex: string or None |
33 | | - self.replaceRegex: string or None |
34 | | - self.replacement: string or None |
35 | | - self.processClassPrimaryUrlPath(): string |
36 | | - """ |
37 | | - |
38 | | - def matches(self, processText): |
39 | | - """ |
40 | | - Compare the process name and parameters. |
41 | | -
|
42 | | - @return: Does the process's command line match this process matcher? |
43 | | - @rtype: Boolean |
44 | | - """ |
45 | | - if not processText: return False |
46 | | - processText = processText.strip() |
47 | | - if self._searchIncludeRegex(processText): |
48 | | - return not self._searchExcludeRegex(processText) |
49 | | - return False |
50 | | - |
51 | | - def generateId(self, processText): |
52 | | - """ |
53 | | - Generate the unique ID of the OSProcess that the process belongs |
54 | | - to, based on the given process's command line. Assumes that the |
55 | | - processText has already passed the OSProcessClass's matches method. |
56 | | -
|
57 | | - The ID is based on a digest of the result of generateName, scoped below |
58 | | - the OSProcessClass's primaryUrlPath. |
59 | | -
|
60 | | - (In order to get an "other" bucket, the replaceRegex should be written |
61 | | - to match the entire string, and every capture group should be optional.) |
62 | | -
|
63 | | - @return: The unique ID of the corresponding OSProcess |
64 | | - @rtype: string |
65 | | - """ |
66 | | - return self.generateIdFromName(self.generateName(processText)) |
67 | | - |
68 | | - def generateIdFromName(self, name): |
69 | | - """ |
70 | | - Generate the unique ID of the OSProcess that the process belongs |
71 | | - to, based on the results of generateName(). Assumes that the |
72 | | - processText has already passed the OSProcessClass's matches method, |
73 | | - and that the name provided came from generateName(processText). |
74 | | -
|
75 | | - The ID is based on a digest of the name, scoped below the |
76 | | - OSProcessClass's primaryUrlPath. |
77 | | -
|
78 | | - @return: The unique ID of the corresponding OSProcess |
79 | | - @rtype: string |
80 | | - """ |
81 | | - generatedId = prepId(self.processClassPrimaryUrlPath()) + "_" + \ |
82 | | - md5(name).hexdigest().strip() |
83 | | - log.debug("Generated unique ID: %s", generatedId) |
84 | | - return generatedId |
85 | | - |
86 | | - |
87 | | - def generateName(self, processText): |
88 | | - """ |
89 | | - Generate the name of an OSProcess. |
90 | | -
|
91 | | - Strips the processText of whitespace, applies the replacement |
92 | | - (globally), and strips any remaining leading and trailing whitespace. |
93 | | -
|
94 | | - @return: The name of the corresponding OSProcess |
95 | | - @rtype: string |
96 | | - """ |
97 | | - return self._applyReplacement(processText.strip()).strip() |
98 | | - |
99 | | - def _applyReplacement(self, processText): |
100 | | - regex = self._compiledRegex('replaceRegex') |
101 | | - if regex: |
102 | | - # We can't simply use re.sub, it blows up if the replacement |
103 | | - # back-references an optional capture group that captured None. |
104 | | - # In that case, we want it to replace the back-reference with ''. |
105 | | - try: |
106 | | - groups, literals = self._compiledReplacement('replaceRegex', |
107 | | - 'replacement') |
108 | | - except Exception: |
109 | | - log.warn("Invalid replacement rule on %s", self) |
110 | | - return processText |
111 | | - parts = [] |
112 | | - frontier = 0 |
113 | | - for match in regex.finditer(processText): |
114 | | - if match.start() == match.end(): |
115 | | - continue |
116 | | - parts.append(processText[frontier:match.start()]) |
117 | | - for index, group in groups: |
118 | | - try: |
119 | | - literal = match.group(group) or '' |
120 | | - except IndexError as e: |
121 | | - log.warn("Invalid replacement rule on %s", self) |
122 | | - return processText |
123 | | - literals[index] = literal |
124 | | - parts.extend(literals) |
125 | | - frontier = match.end() |
126 | | - parts.append(processText[frontier:]) |
127 | | - return ''.join(parts) |
128 | | - return processText |
129 | | - |
130 | | - def _searchIncludeRegex(self, processText): |
131 | | - r = self._compiledRegex('includeRegex') |
132 | | - return r and r.search(processText) |
133 | | - |
134 | | - def _searchExcludeRegex(self, processText): |
135 | | - r = self._compiledRegex('excludeRegex') |
136 | | - return r and r.search(processText) |
137 | | - |
138 | | - def _compiledRegex(self, field): |
139 | | - regex = getattr(self, field, None) |
140 | | - if not regex: return None |
141 | | - cache = self._compiledCache() |
142 | | - if field in cache and regex in cache[field]: |
143 | | - return cache[field][regex] |
144 | | - else: |
145 | | - try: |
146 | | - compiled = re.compile(regex) |
147 | | - cache[field] = {regex : compiled} |
148 | | - return compiled |
149 | | - except re.error as e: |
150 | | - log.warn("Invalid %s on %s", field, self) |
151 | | - cache[field] = {regex : None} |
152 | | - return None |
153 | | - |
154 | | - def _compiledReplacement(self, regexField, replField): |
155 | | - repl = getattr(self, replField, None) |
156 | | - if not repl: return BLANK_PARSE_TEMPLATE |
157 | | - cache = self._compiledCache() |
158 | | - regex = getattr(self, regexField, None) |
159 | | - if replField in cache and (regex,repl) in cache[replField]: |
160 | | - return cache[replField][(regex,repl)] |
161 | | - else: |
162 | | - try: |
163 | | - compiled = parse_template(repl, self._compiledRegex(regexField)) |
164 | | - cache[replField] = {(regex,repl) : compiled} |
165 | | - return compiled |
166 | | - except Exception: |
167 | | - log.warn("Invalid %s on %s", replField, self) |
168 | | - cache[replField] = {(regex,repl) : None} |
169 | | - return None |
170 | | - |
171 | | - def _compiledCache(self): |
172 | | - cache = getattr(self, '_compiled_cache', None) |
173 | | - if not cache: |
174 | | - cache = self._compiled_cache = {} |
175 | | - return cache |
176 | | - |
177 | | -class OSProcessMatcher(OSProcessClassMatcher): |
178 | | - """ |
179 | | - Mixin class, for process command line matching functionality in OSProcess. |
180 | | -
|
181 | | - Classes which mixin OSProcessMatcher must provide: |
182 | | - self.includeRegex: string |
183 | | - self.excludeRegex: string or None |
184 | | - self.replaceRegex: string or None |
185 | | - self.replacement: string or None |
186 | | - self.processClassPrimaryUrlPath(): string |
187 | | - self.generatedId: string |
188 | | - """ |
189 | | - def matches(self, processText): |
190 | | - if super(OSProcessMatcher, self).matches(processText): |
191 | | - generatedId = getattr(self,'generatedId',False) |
192 | | - return self.generateId(processText) == generatedId |
193 | | - return False |
194 | | - |
195 | | -class DataHolder(object): |
196 | | - def __init__(self, **attribs): |
197 | | - for k,v in attribs.items(): |
198 | | - setattr(self,k,v) |
199 | | - |
200 | | - def __repr__(self): |
201 | | - return "<" + self.__class__.__name__ + ": " + str(self.__dict__) + ">" |
202 | | - |
203 | | - def processClassPrimaryUrlPath(self): |
204 | | - return self.primaryUrlPath |
205 | | - |
206 | | - |
207 | | -class OSProcessClassDataMatcher(DataHolder, OSProcessClassMatcher): |
208 | | - pass |
209 | | - |
210 | | -class OSProcessDataMatcher(DataHolder, OSProcessMatcher): |
211 | | - pass |
212 | | - |
213 | | -def applyOSProcessClassMatchers(matchers, lines): |
214 | | - """ |
215 | | - @return (matched, unmatched), where... |
216 | | - matched is: {matcher => {generatedName => [line, ...], ...}, ...} |
217 | | - unmatched is: [line, ...] |
218 | | - """ |
219 | | - matched = {} |
220 | | - unmatched = [] |
221 | | - for line in lines: |
222 | | - log.debug("COMMAND LINE: %s", line) |
223 | | - unmatchedLine = True |
224 | | - for matcher in matchers: |
225 | | - if matcher.matches(line): |
226 | | - if matcher not in matched: |
227 | | - matched[matcher] = {} |
228 | | - generatedName = matcher.generateName(line) |
229 | | - if generatedName not in matched[matcher]: |
230 | | - matched[matcher][generatedName] = [] |
231 | | - matched[matcher][generatedName].append(line) |
232 | | - unmatchedLine = False |
233 | | - break |
234 | | - if unmatchedLine: |
235 | | - unmatched.append(line) |
236 | | - return (matched, unmatched) |
237 | | - |
238 | | -def applyOSProcessMatchers(matchers, lines): |
239 | | - """ |
240 | | - @return (matched, unmatched), where... |
241 | | - matched is: {generatedName => [line, ...], ...} |
242 | | - unmatched is: [line, ...] |
243 | | - """ |
244 | | - matched = {} |
245 | | - unmatched = [] |
246 | | - for line in lines: |
247 | | - log.debug("COMMAND LINE: %s", line) |
248 | | - unmatchedLine = True |
249 | | - for matcher in matchers: |
250 | | - if matcher.matches(line): |
251 | | - if matcher.generatedName not in matched: |
252 | | - matched[matcher.generatedName] = [] |
253 | | - matched[matcher.generatedName].append(line) |
254 | | - unmatchedLine = False |
255 | | - break |
256 | | - if unmatchedLine: |
257 | | - unmatched.append(line) |
258 | | - return (matched, unmatched) |
259 | | - |
260 | | -def buildObjectMapData(processClassMatchData, lines): |
261 | | - matchers = map(lambda(d):OSProcessClassDataMatcher(**d), processClassMatchData) |
262 | | - matched, unmatched = applyOSProcessClassMatchers(matchers, lines) |
263 | | - result = [] |
264 | | - for matcher, matchSet in matched.items(): |
265 | | - for name, matches in matchSet.items(): |
266 | | - result.append({ |
267 | | - 'id': matcher.generateIdFromName(name), |
268 | | - 'displayName': name, |
269 | | - 'setOSProcessClass': matcher.primaryDmdId, |
270 | | - 'monitoredProcesses': matches}) |
271 | | - return result |
| 1 | +from Products.ZenRRD.osprocess import ( |
| 2 | + OSProcessClassMatcher, |
| 3 | + OSProcessMatcher, |
| 4 | + OSProcessClassDataMatcher, |
| 5 | + OSProcessDataMatcher, |
| 6 | + applyOSProcessClassMatchers, |
| 7 | + applyOSProcessMatchers, |
| 8 | + buildObjectMapData, |
| 9 | +) |
| 10 | + |
| 11 | +__all__ = ( |
| 12 | + "OSProcessClassMatcher", |
| 13 | + "OSProcessMatcher", |
| 14 | + "OSProcessClassDataMatcher", |
| 15 | + "OSProcessDataMatcher", |
| 16 | + "applyOSProcessClassMatchers", |
| 17 | + "applyOSProcessMatchers", |
| 18 | + "buildObjectMapData", |
| 19 | +) |
0 commit comments