1
2
3
4
5
6
7
8
9
10
11
12
13
14 import os
15 import os.path
16 import sys
17 import re
18 from urllib import unquote
19 from subprocess import Popen, PIPE, call
20 from xml.dom.minidom import parse
21 import shutil
22 import traceback
23 import logging
24 log = logging.getLogger("zen.ZenossInfo")
25
26 from Globals import InitializeClass
27 from OFS.SimpleItem import SimpleItem
28 from AccessControl import ClassSecurityInfo
29
30 from Products.ZenModel.ZenModelItem import ZenModelItem
31 from Products.ZenUtils import Time
32 from Products.ZenUtils.Version import *
33 from Products.ZenUtils.Utils import zenPath, binPath
34 from Products.ZenWidgets import messaging
35
36 from Products.ZenEvents.UpdateCheck import UpdateCheck, parseVersion
37
46
48
49 portal_type = meta_type = 'ZenossInfo'
50
51 security = ClassSecurityInfo()
52
53 _properties = (
54 {'id':'id', 'type':'string'},
55 {'id':'title', 'type':'string'},
56 )
57
58 factory_type_information = (
59 {
60 'immediate_view' : 'zenossInfo',
61 'actions' :
62 (
63 { 'id' : 'settings'
64 , 'name' : 'Settings'
65 , 'action' : '../dmd/editSettings'
66 , 'permissions' : ( "Manage DMD", )
67 },
68 { 'id' : 'manage'
69 , 'name' : 'Commands'
70 , 'action' : '../dmd/dataRootManage'
71 , 'permissions' : ('Manage DMD',)
72 },
73 { 'id' : 'users'
74 , 'name' : 'Users'
75 , 'action' : '../dmd/ZenUsers/manageUserFolder'
76 , 'permissions' : ( 'Manage DMD', )
77 },
78 { 'id' : 'packs'
79 , 'name' : 'ZenPacks'
80 , 'action' : '../dmd/ZenPackManager/viewZenPacks'
81 , 'permissions' : ( "Manage DMD", )
82 },
83 { 'id' : 'jobs'
84 , 'name' : 'Jobs'
85 , 'action' : '../dmd/joblist'
86 , 'permissions' : ( "Manage DMD", )
87 },
88
89
90
91
92
93 { 'id' : 'portlets'
94 , 'name' : 'Portlets'
95 , 'action' : '../dmd/editPortletPerms'
96 , 'permissions' : ( "Manage DMD", )
97 },
98 { 'id' : 'daemons'
99 , 'name' : 'Daemons'
100 , 'action' : 'zenossInfo'
101 , 'permissions' : ( "Manage DMD", )
102 },
103 { 'id' : 'versions'
104 , 'name' : 'Versions'
105 , 'action' : 'zenossVersions'
106 , 'permissions' : ( "Manage DMD", )
107 },
108 { 'id' : 'backups'
109 , 'name' : 'Backups'
110 , 'action' : '../dmd/backupInfo'
111 , 'permissions' : ( "Manage DMD", )
112 },
113 { 'id' : 'eventConfig'
114 , 'name' : 'Events'
115 , 'action' : 'eventConfig'
116 , 'permissions' : ( "Manage DMD", )
117 },
118 )
119 },
120 )
121
122
125
128
129 security.declarePublic('getZenossVersion')
134
135
136 security.declarePublic('getZenossVersionShort')
139
140
142 """
143 This function returns a Version-ready tuple. For use with the Version
144 object, use extended call syntax:
145
146 v = Version(*getOSVersion())
147 v.full()
148 """
149 if os.name == 'posix':
150 sysname, nodename, version, build, arch = os.uname()
151 name = "%s (%s)" % (sysname, arch)
152 major, minor, micro = getVersionTupleFromString(version)
153 comment = ' '.join(os.uname())
154 elif os.name == 'nt':
155 from win32api import GetVersionEx
156 major, minor, micro, platformID, additional = GetVersionEx()
157 name = 'Windows %s (%s)' % (os.name.upper(), additional)
158 comment = ''
159 else:
160 raise VersionNotSupported
161 return Version(name, major, minor, micro, 0, comment)
162
163
165 """
166 This function returns a Version-ready tuple. For use with the Version
167 object, use extended call syntax:
168
169 v = Version(*getPythonVersion())
170 v.full()
171 """
172 name = 'Python'
173 major, minor, micro, releaselevel, serial = sys.version_info
174 return Version(name, major, minor, micro)
175
176
178 """
179 This function returns a Version-ready tuple. For use with the Version
180 object, use extended call syntax:
181
182 v = Version(*getMySQLVersion())
183 v.full()
184
185 The regex was tested against the following output strings:
186 mysql Ver 14.12 Distrib 5.0.24, for apple-darwin8.5.1 (i686) using readline 5.0
187 mysql Ver 12.22 Distrib 4.0.24, for pc-linux-gnu (i486)
188 mysql Ver 14.12 Distrib 5.0.24a, for Win32 (ia32)
189 /usr/local/zenoss/mysql/bin/mysql.bin Ver 14.12 Distrib 5.0.45, for unknown-linux-gnu (x86_64) using readline 5.0
190 """
191 cmd = 'mysql --version'
192 fd = os.popen(cmd)
193 output = fd.readlines()
194 version = "0"
195 if fd.close() is None and len(output) > 0:
196 output = output[0].strip()
197 regexString = '.*(mysql).*Ver [0-9]{2}\.[0-9]{2} '
198 regexString += 'Distrib ([0-9]+.[0-9]+.[0-9]+)(.*), for (.*\(.*\))'
199 regex = re.match(regexString, output)
200 if regex:
201 name, version, release, info = regex.groups()
202 comment = 'Ver %s' % version
203
204 if os.environ.get("USE_ZENDS", None):
205 name = 'ZenDS'
206 else:
207 name = 'MySQL'
208 major, minor, micro = getVersionTupleFromString(version)
209 return Version(name, major, minor, micro, 0, comment)
210
211
229
230
232 """
233 This function returns a Version-ready tuple. For use with the Version
234 object, use extended call syntax:
235
236 v = Version(*getTwistedVersion())
237 v.full()
238 """
239 from twisted._version import version as v
240
241 return Version('Twisted', v.major, v.minor, v.micro)
242
243
245 """
246 This function returns a Version-ready tuple. For use with the Version
247 object, use extended call syntax:
248
249 v = Version(*getZopeVersion())
250 v.full()
251 """
252 from App import version_txt as version
253
254 name = 'Zope'
255 major, minor, micro, status, release = version.getZopeVersion()
256 return Version(name, major, minor, micro)
257
258
260 """
261 Determine the Zenoss version number
262
263 @return: version number or ''
264 @rtype: string
265 """
266 try:
267 products = zenPath("Products")
268 cmd = "svn info '%s' 2>/dev/null | awk '/Revision/ {print $2}'" % products
269 fd = os.popen(cmd)
270 return fd.readlines()[0].strip()
271 except:
272 return ''
273
274
276 from pynetsnmp.netsnmp import lib
277 return Version.parse('NetSnmp %s ' % lib.netsnmp_get_version())
278
279
283
284
288
289
291 """
292 Return a list of version numbers for currently tracked component
293 software.
294 """
295 versions = (
296 {'header': 'Zenoss', 'data': self.getZenossVersion().full(),
297 'href': "http://www.zenoss.com" },
298 {'header': 'OS', 'data': self.getOSVersion().full(),
299 'href': "http://www.tldp.org" },
300 {'header': 'Zope', 'data': self.getZopeVersion().full(),
301 'href': "http://www.zope.org" },
302 {'header': 'Python', 'data': self.getPythonVersion().full(),
303 'href': "http://www.python.org" },
304 {'header': 'Database', 'data': self.getMySQLVersion().full(),
305 'href': "http://www.mysql.com" },
306 {'header': 'RRD', 'data': self.getRRDToolVersion().full(),
307 'href': "http://oss.oetiker.ch/rrdtool" },
308 {'header': 'Twisted', 'data': self.getTwistedVersion().full(),
309 'href': "http:///twistedmatrix.com/trac" },
310 )
311 try:
312 versions += (
313 {'header': 'NetSnmp', 'data': self.getNetSnmpVersion().full(),
314 'href': "http://net-snmp.sourceforge.net" },
315 )
316 except:
317 pass
318 try:
319 versions += (
320 {'header': 'PyNetSnmp', 'data': self.getPyNetSnmpVersion().full(),
321 'href': "http://www.zenoss.com" },
322 )
323 except:
324 pass
325 try:
326 versions += (
327 {'header': 'WMI', 'data': self.getWmiVersion().full(),
328 'href': "http://www.zenoss.com" },
329 )
330 except:
331 pass
332 return versions
333
334 security.declareProtected('View','getAllVersions')
335
336
338 """
339 Return a list of daemons with their uptimes.
340 """
341 app = self.getPhysicalRoot()
342 uptimes = []
343 zope = {
344 'header': 'Zope',
345 'data': app.Control_Panel.process_time(),
346 }
347 uptimes.append(zope)
348 return uptimes
349 security.declareProtected('View','getAllUptimes')
350
351
352
353 daemon_tooltips= {
354 "zeoctl": "Zope Enterprise Objects server (shares database between Zope instances)",
355 "zopectl": "The Zope open source web application server",
356 "zenhub": "Broker between the data layer and the collection daemons",
357 "zenping": "ICMP ping status monitoring",
358 "zensyslog": "Collection of and classification of syslog events",
359 "zenstatus": "Active TCP connection testing of remote daemons",
360 "zenactiond": "Receives signals from processed events to execute notifications.",
361 "zentrap": "Receives SNMP traps and turns them into events",
362 "zenmodeler": "Configuration collection and configuration",
363 "zenperfsnmp": "High performance asynchronous SNMP performance collection",
364 "zencommand": "Runs plug-ins on the local box or on remote boxes through SSH",
365 "zenprocess": "Process monitoring using SNMP host resources MIB",
366 "zenwin": "Windows Service Monitoring (WMI)",
367 "zeneventlog": "Collect (WMI) event log events (aka NT Eventlog)",
368 "zendisc": "Discover the network topology to find active IPs and devices",
369 "zenrrdcached": "Controls the write cache for performance data",
370 "zenmail": "Listen for e-mail and convert messages to Zenoss events",
371 "zenpop3": "Connect via pop3 to an e-mail server and convert messages to Zenoss events",
372 }
373
374
376 """
377 Return a data structures representing the states of the supported
378 Zenoss daemons.
379 """
380 states = []
381 activeButtons = {'button1': 'Restart', 'button2': 'Stop', 'button2state': True}
382 inactiveButtons = {'button1': 'Start', 'button2': 'Stop', 'button2state': False}
383 alwaysOnButtons = {'button1': 'Restart', 'button2': 'Stop', 'button2state': False}
384 for daemon in self._getDaemonList():
385 pid = self._getDaemonPID(daemon)
386 if pid:
387 if daemon == 'zopectl':
388 buttons = alwaysOnButtons
389 else:
390 buttons = activeButtons
391 msg = 'Up'
392 color = '#0F0'
393 else:
394 buttons = inactiveButtons
395 msg = 'Down'
396 color = '#F00'
397
398 if daemon in self.daemon_tooltips:
399 tooltip= self.daemon_tooltips[ daemon ]
400 else:
401 tooltip= ''
402
403 states.append({
404 'name': daemon,
405 'pid': pid,
406 'msg': msg,
407 'tooltip': tooltip,
408 'color': color,
409 'buttons': buttons})
410
411 return states
412
413
415 try:
416 os.kill(pid, 0)
417 return pid
418 except OSError, ex:
419 import errno
420 errnum, msg = ex.args
421 if errnum == errno.EPERM:
422 return pid
423
424
426 """
427 For a given daemon name, return its PID from a .pid file.
428 """
429 if name == 'zopectl':
430 name = 'Z2'
431 elif name == 'zeoctl':
432 name = 'ZEO'
433 elif '_' in name:
434 collector, daemon = name.split('_', 1)
435 name = '%s-%s' % (daemon, collector)
436 else:
437 name = "%s-localhost" % name
438 pidFile = zenPath('var', '%s.pid' % name)
439 if os.path.exists(pidFile):
440 pid = open(pidFile).read()
441 try:
442 pid = int(pid)
443 except ValueError:
444 return None
445 return self._pidRunning(int(pid))
446 else:
447 pid = None
448 return pid
449
450
452 """
453 Get the list of supported Zenoss daemons.
454 """
455 masterScript = binPath('zenoss')
456 daemons = []
457 for line in os.popen("%s list" % masterScript).readlines():
458 if 'zenrrdcache' not in line:
459 daemons.append(line.strip())
460 return daemons
461
462
464 """
465 Return a data structures representing the config infor for the
466 supported Zenoss daemons.
467 """
468 return [ dict(name=x) for x in self._getDaemonList() ]
469
471 fh = open(filename)
472 try:
473 size = os.path.getsize(filename)
474 if size > maxBytes:
475 fh.seek(-maxBytes, 2)
476
477 fh.readline()
478 return fh.read()
479 finally:
480 fh.close()
481
483 """
484 Returns the path the log file for the daemon this is monkey-patched
485 in the distributed collector zenpack to support the localhost
486 subdirectory.
487 """
488 return zenPath('log', "%s.log" % daemon)
489
491 """
492 Get the last kb kilobytes of a daemon's log file contents.
493 """
494 maxBytes = 1024 * int(kb)
495 if daemon == 'zopectl':
496 daemon = 'event'
497 elif daemon == 'zeoctl':
498 daemon = 'zeo'
499 if daemon == 'zopectl':
500 daemon = 'event'
501 elif daemon == 'zeoctl':
502 daemon = 'zeo'
503 filename = self._getLogPath(daemon)
504
505
506
507 data = ' '
508 try:
509 data = self._readLogFile(filename, maxBytes) or ' '
510 except Exception, ex:
511 data = "Error reading %s log file '%s':\n%s" % (
512 daemon, filename, str(ex))
513 return data
514
515
517 if daemon == 'zopectl':
518 daemon = 'zope'
519 elif daemon == 'zeoctl':
520 daemon = 'zeo'
521 return zenPath('etc', "%s.conf" % daemon)
522
524 fh = open(filename)
525 try:
526 return fh.read()
527 finally:
528 fh.close()
529
531 """
532 Return the contents of the daemon's config file.
533 """
534 filename = self._getConfigFilename(daemon)
535
536
537
538 data = ' '
539 try:
540 data = self._readConfigFile(filename) or ' '
541 except IOError:
542 data = 'Unable to read config file'
543 return data
544
545
559
561 """
562 From the given configuration file construct a configuration object
563 """
564 configs = {}
565
566 config_file = open(filename)
567 try:
568 for line in config_file:
569 line = line.strip()
570 if line.startswith('#'): continue
571 if line == '': continue
572
573 try:
574 key, value = line.split(None, 1)
575 except ValueError:
576
577 continue
578 configs[key] = value
579 finally:
580 config_file.close()
581
582 return configs
583
585 """
586 Display the daemon configuration options in an XML format.
587 Merges the defaults with options in the config file.
588 """
589
590 if not daemon or daemon == '':
591 messaging.IMessageSender(self).sendToBrowser(
592 'Internal Error',
593 'Called without a daemon name',
594 priority=messaging.WARNING
595 )
596 return []
597
598 if daemon in [ 'zeoctl', 'zopectl' ]:
599 return []
600
601 xml_default_name = zenPath( "etc", daemon + ".xml" )
602 try:
603
604 log.debug("Creating XML config file for %s" % daemon)
605 make_xml = ' '.join([daemon, "genxmlconfigs", ">", xml_default_name])
606 proc = Popen(make_xml, shell=True, stdout=PIPE, stderr=PIPE)
607 output, errors = proc.communicate()
608 proc.wait()
609 if proc.returncode != 0:
610 log.error(errors)
611 messaging.IMessageSender(self).sendToBrowser(
612 'Internal Error', errors,
613 priority=messaging.CRITICAL
614 )
615 return [["Output", output, errors, make_xml, "string"]]
616 except Exception, ex:
617 msg = "Unable to execute '%s'\noutput='%s'\nerrors='%s'\nex=%s" % (
618 make_xml, output, errors, ex)
619 log.error(msg)
620 messaging.IMessageSender(self).sendToBrowser(
621 'Internal Error', msg,
622 priority=messaging.CRITICAL
623 )
624 return [["Error in command", output, errors, make_xml, "string"]]
625
626 try:
627 xml_defaults = parse( xml_default_name )
628 except:
629 info = traceback.format_exc()
630 msg = "Unable to parse XML file %s because %s" % (
631 xml_default_name, info)
632 log.error(msg)
633 messaging.IMessageSender(self).sendToBrowser(
634 'Internal Error', msg,
635 priority=messaging.CRITICAL
636 )
637 return [["Error parsing XML file", xml_default_name, "XML", info, "string"]]
638
639 configfile = self._getConfigFilename(daemon)
640 try:
641
642 current_configs = self.parseconfig( configfile )
643 except:
644 info = traceback.format_exc()
645 msg = "Unable to obtain current configuration from %s because %s" % (
646 configfile, info)
647 log.error(msg)
648 messaging.IMessageSender(self).sendToBrowser(
649 'Internal Error', msg,
650 priority=messaging.CRITICAL
651 )
652 return [["Configuration file issue", configfile, configfile, info, "string"]]
653
654 all_options = {}
655 ignore_options = ['configfile', 'cycle', 'daemon', 'weblog']
656 try:
657 for option in xml_defaults.getElementsByTagName('option'):
658 id = option.attributes['id'].nodeValue
659 if id in ignore_options:
660 continue
661 try:
662 help = unquote(option.attributes['help'].nodeValue)
663 except:
664 help = ''
665
666 try:
667 default = unquote(option.attributes['default'].nodeValue)
668 except:
669 default = ''
670 if default == '[]':
671 continue
672
673 all_options[id] = [
674 id,
675 current_configs.get(id, default),
676 default,
677 help,
678 option.attributes['type'].nodeValue,
679 ]
680
681 except:
682 info = traceback.format_exc()
683 msg = "Unable to merge XML defaults with config file" \
684 " %s because %s" % (configfile, info)
685 log.error(msg)
686 messaging.IMessageSender(self).sendToBrowser(
687 'Internal Error', msg,
688 priority=messaging.CRITICAL
689 )
690 return [["XML file issue", daemon, xml_default_name, info, "string"]]
691
692 return [all_options[name] for name in sorted(all_options.keys())]
693
694
696 """
697 Save the updated daemon configuration to disk.
698 """
699 if not REQUEST:
700 return
701 elif not hasattr(REQUEST, 'form'):
702 return
703
704
705 formdata = REQUEST.form
706 ignore_names = ['save_daemon_configs', 'zenScreenName', 'daemon_name']
707
708 daemon = formdata.get('daemon_name', '')
709 if not daemon or daemon in ['zeoctl', 'zopectl']:
710 return
711 for item in ignore_names:
712 del formdata[item]
713
714 if not formdata:
715 msg = "Received empty form data for %s config -- ignoring" % (
716 daemon)
717 log.error(msg)
718 messaging.IMessageSender(self).sendToBrowser(
719 'Internal Error', msg,
720 priority=messaging.CRITICAL
721 )
722 return
723
724 configfile = self._getConfigFilename(daemon)
725 config_file_pre = configfile + ".pre"
726 try:
727 config = open( config_file_pre, 'w' )
728 config.write("# Config file written out from GUI\n")
729 for key, value in formdata.items():
730 if value == '':
731 continue
732 if key == value:
733 value = True
734 config.write('%s %s\n' % (key, value))
735 config.close()
736 except Exception, ex:
737 msg = "Couldn't write to %s because %s" % (config_file_pre, ex)
738 log.error(msg)
739 messaging.IMessageSender(self).sendToBrowser(
740 'Internal Error', msg,
741 priority=messaging.CRITICAL
742 )
743 config.close()
744 try:
745 os.unlink(config_file_pre)
746 except:
747 pass
748 return
749
750
751 config_file_save = configfile + ".save"
752 try:
753 shutil.copy(configfile, config_file_save)
754 except:
755 log.error("Unable to make backup copy of %s" % configfile)
756
757 try:
758 shutil.move(config_file_pre, configfile)
759 except:
760 msg = "Unable to save contents to %s" % configfile
761 log.error(msg)
762 messaging.IMessageSender(self).sendToBrowser(
763 'Internal Error', msg,
764 priority=messaging.CRITICAL
765 )
766
767
779 security.declareProtected('Manage DMD','manage_daemonAction')
780
781
783 """
784 Do the given action (start, stop, restart) or the given daemon.
785 Block until the action is completed.
786 No return value.
787 """
788 import time
789 import subprocess
790 daemonPath = binPath(daemonName)
791 if not os.path.isfile(daemonPath):
792 return
793 log.info('Telling %s to %s' % (daemonName, action))
794 proc = subprocess.Popen([daemonPath, action], stdout=subprocess.PIPE,
795 stderr=subprocess.STDOUT)
796 output, _ = proc.communicate()
797 code = proc.wait()
798 if code:
799 log.info('Error from %s: %s (%s)' % (daemonName, output, code))
800 if action in ('stop', 'restart'):
801 time.sleep(2)
802
803
819 security.declareProtected('Manage DMD','manage_checkVersion')
820
821
826
827
834
835
836 InitializeClass(ZenossInfo)
837