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