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 { 'id' : 'menus'
89 , 'name' : 'Menus'
90 , 'action' : '../dmd/editMenus'
91 , 'permissions' : ( "Manage DMD", )
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 )
114 },
115 )
116
117
118 security.declarePublic('getZenossVersion')
123
124
125 security.declarePublic('getZenossVersionShort')
128
129
131 """
132 This function returns a Version-ready tuple. For use with the Version
133 object, use extended call syntax:
134
135 v = Version(*getOSVersion())
136 v.full()
137 """
138 if os.name == 'posix':
139 sysname, nodename, version, build, arch = os.uname()
140 name = "%s (%s)" % (sysname, arch)
141 major, minor, micro = getVersionTupleFromString(version)
142 comment = ' '.join(os.uname())
143 elif os.name == 'nt':
144 from win32api import GetVersionEx
145 major, minor, micro, platformID, additional = GetVersionEx()
146 name = 'Windows %s (%s)' % (os.name.upper(), additional)
147 comment = ''
148 else:
149 raise VersionNotSupported
150 return Version(name, major, minor, micro, 0, comment)
151
152
154 """
155 This function returns a Version-ready tuple. For use with the Version
156 object, use extended call syntax:
157
158 v = Version(*getPythonVersion())
159 v.full()
160 """
161 name = 'Python'
162 major, minor, micro, releaselevel, serial = sys.version_info
163 return Version(name, major, minor, micro)
164
165
167 """
168 This function returns a Version-ready tuple. For use with the Version
169 object, use extended call syntax:
170
171 v = Version(*getMySQLVersion())
172 v.full()
173
174 The regex was tested against the following output strings:
175 mysql Ver 14.12 Distrib 5.0.24, for apple-darwin8.5.1 (i686) using readline 5.0
176 mysql Ver 12.22 Distrib 4.0.24, for pc-linux-gnu (i486)
177 mysql Ver 14.12 Distrib 5.0.24a, for Win32 (ia32)
178 /usr/local/zenoss/mysql/bin/mysql.bin Ver 14.12 Distrib 5.0.45, for unknown-linux-gnu (x86_64) using readline 5.0
179 """
180 cmd = 'mysql --version'
181 fd = os.popen(cmd)
182 output = fd.readlines()
183 version = "0"
184 if fd.close() is None and len(output) > 0:
185 output = output[0].strip()
186 regexString = '.*(mysql).*Ver [0-9]{2}\.[0-9]{2} '
187 regexString += 'Distrib ([0-9]+.[0-9]+.[0-9]+)(.*), for (.*\(.*\))'
188 regex = re.match(regexString, output)
189 if regex:
190 name, version, release, info = regex.groups()
191 comment = 'Ver %s' % version
192
193 name = 'MySQL'
194 major, minor, micro = getVersionTupleFromString(version)
195 return Version(name, major, minor, micro, 0, comment)
196
197
215
216
218 """
219 This function returns a Version-ready tuple. For use with the Version
220 object, use extended call syntax:
221
222 v = Version(*getTwistedVersion())
223 v.full()
224 """
225 from twisted._version import version as v
226
227 return Version('Twisted', v.major, v.minor, v.micro)
228
229
231 """
232 This function returns a Version-ready tuple. For use with the Version
233 object, use extended call syntax:
234
235 v = Version(*getZopeVersion())
236 v.full()
237 """
238 from App import version_txt as version
239
240 name = 'Zope'
241 major, minor, micro, status, release = version.getZopeVersion()
242 return Version(name, major, minor, micro)
243
244
246 """
247 Determine the Zenoss version number
248
249 @return: version number or ''
250 @rtype: string
251 """
252 try:
253 products = zenPath("Products")
254 cmd = "svn info '%s' 2>/dev/null | awk '/Revision/ {print $2}'" % products
255 fd = os.popen(cmd)
256 return fd.readlines()[0].strip()
257 except:
258 return ''
259
260
262 from pynetsnmp.netsnmp import lib
263 return Version.parse('NetSnmp %s ' % lib.netsnmp_get_version())
264
265
269
270
274
275
277 """
278 Return a list of version numbers for currently tracked component
279 software.
280 """
281 versions = (
282 {'header': 'Zenoss', 'data': self.getZenossVersion().full(),
283 'href': "http://www.zenoss.com" },
284 {'header': 'OS', 'data': self.getOSVersion().full(),
285 'href': "http://www.tldp.org" },
286 {'header': 'Zope', 'data': self.getZopeVersion().full(),
287 'href': "http://www.zope.org" },
288 {'header': 'Python', 'data': self.getPythonVersion().full(),
289 'href': "http://www.python.org" },
290 {'header': 'Database', 'data': self.getMySQLVersion().full(),
291 'href': "http://www.mysql.com" },
292 {'header': 'RRD', 'data': self.getRRDToolVersion().full(),
293 'href': "http://oss.oetiker.ch/rrdtool" },
294 {'header': 'Twisted', 'data': self.getTwistedVersion().full(),
295 'href': "http:///twistedmatrix.com/trac" },
296 )
297 try:
298 versions += (
299 {'header': 'NetSnmp', 'data': self.getNetSnmpVersion().full(),
300 'href': "http://net-snmp.sourceforge.net" },
301 )
302 except:
303 pass
304 try:
305 versions += (
306 {'header': 'PyNetSnmp', 'data': self.getPyNetSnmpVersion().full(),
307 'href': "http://www.zenoss.com" },
308 )
309 except:
310 pass
311 try:
312 versions += (
313 {'header': 'WMI', 'data': self.getWmiVersion().full(),
314 'href': "http://www.zenoss.com" },
315 )
316 except:
317 pass
318 return versions
319
320 security.declareProtected('View','getAllVersions')
321
322
324 """
325 Return a list of daemons with their uptimes.
326 """
327 app = self.getPhysicalRoot()
328 uptimes = []
329 zope = {
330 'header': 'Zope',
331 'data': app.Control_Panel.process_time(),
332 }
333 uptimes.append(zope)
334 return uptimes
335 security.declareProtected('View','getAllUptimes')
336
337
338
339 daemon_tooltips= {
340 "zeoctl": "Zope Enterprise Objects server (shares database between Zope instances)",
341 "zopectl": "The Zope open source web application server",
342 "zenhub": "Broker between the data layer and the collection daemons",
343 "zenping": "ICMP ping status monitoring",
344 "zensyslog": "Collection of and classification of syslog events",
345 "zenstatus": "Active TCP connection testing of remote daemons",
346 "zenactions": "Alerts (SMTP, SNPP and Maintenance Windows)",
347 "zentrap": "Receives SNMP traps and turns them into events",
348 "zenmodeler": "Configuration collection and configuration",
349 "zenperfsnmp": "High performance asynchronous SNMP performance collection",
350 "zencommand": "Runs plug-ins on the local box or on remote boxes through SSH",
351 "zenprocess": "Process monitoring using SNMP host resources MIB",
352 "zenwin": "Windows Service Monitoring (WMI)",
353 "zeneventlog": "Collect (WMI) event log events (aka NT Eventlog)",
354 "zenwinmodeler": "MS Windows configuration collection and configuration",
355 "zendisc": "Discover the network topology to find active IPs and devices",
356 "zenperfxmlrpc": "XML RPC data collection",
357 }
358
359
361 """
362 Return a data structures representing the states of the supported
363 Zenoss daemons.
364 """
365 states = []
366 activeButtons = {'button1': 'Restart', 'button2': 'Stop', 'button2state': True}
367 inactiveButtons = {'button1': 'Start', 'button2': 'Stop', 'button2state': False}
368 for daemon in self._getDaemonList():
369 pid = self._getDaemonPID(daemon)
370 if pid:
371 buttons = activeButtons
372 msg = 'Up'
373 color = '#0F0'
374 else:
375 buttons = inactiveButtons
376 msg = 'Down'
377 color = '#F00'
378
379 if daemon in self.daemon_tooltips:
380 tooltip= self.daemon_tooltips[ daemon ]
381 else:
382 tooltip= ''
383
384 states.append({
385 'name': daemon,
386 'pid': pid,
387 'msg': msg,
388 'tooltip': tooltip,
389 'color': color,
390 'buttons': buttons})
391
392 return states
393
394
396 try:
397 os.kill(pid, 0)
398 return pid
399 except OSError, ex:
400 import errno
401 errnum, msg = ex.args
402 if errnum == errno.EPERM:
403 return pid
404
405
407 """
408 For a given daemon name, return its PID from a .pid file.
409 """
410 if name == 'zopectl':
411 name = 'Z2'
412 elif name == 'zeoctl':
413 name = 'ZEO'
414 elif '_' in name:
415 collector, daemon = name.split('_', 1)
416 name = '%s-%s' % (daemon, collector)
417 else:
418 name = "%s-localhost" % name
419 pidFile = zenPath('var', '%s.pid' % name)
420 if os.path.exists(pidFile):
421 pid = open(pidFile).read()
422 try:
423 pid = int(pid)
424 except ValueError:
425 return None
426 return self._pidRunning(int(pid))
427 else:
428 pid = None
429 return pid
430
431
433 """
434 Get the list of supported Zenoss daemons.
435 """
436 masterScript = binPath('zenoss')
437 daemons = []
438 for line in os.popen("%s list" % masterScript).readlines():
439 daemons.append(line.strip())
440 return daemons
441
442
444 """
445 Return a data structures representing the config infor for the
446 supported Zenoss daemons.
447 """
448 return [ dict(name=x) for x in self._getDaemonList() ]
449
461
463 """
464 Returns the path the log file for the daemon this is monkey-patched
465 in the distributed collector zenpack to support the localhost
466 subdirectory.
467 """
468 return zenPath('log', "%s.log" % daemon)
469
471 """
472 Get the last kb kilobytes of a daemon's log file contents.
473 """
474 maxBytes = 1024 * int(kb)
475 if daemon == 'zopectl':
476 daemon = 'event'
477 elif daemon == 'zeoctl':
478 daemon = 'zeo'
479 if daemon == 'zopectl':
480 daemon = 'event'
481 elif daemon == 'zeoctl':
482 daemon = 'zeo'
483 filename = self._getLogPath(daemon)
484
485
486
487 data = ' '
488 try:
489 data = self._readLogFile(filename, maxBytes) or ' '
490 except IOError:
491 data = 'Error reading log file'
492 return data
493
494
496 if daemon == 'zopectl':
497 daemon = 'zope'
498 elif daemon == 'zeoctl':
499 daemon = 'zeo'
500 return zenPath('etc', "%s.conf" % daemon)
501
508
523
524
538
540 """
541 From the given configuration file construct a configuration object
542 """
543 configs = {}
544
545 config_file = open(filename)
546 try:
547 for line in config_file:
548 line = line.strip()
549 if line.startswith('#'): continue
550 if line == '': continue
551
552 try:
553 key, value = line.split(None, 1)
554 except ValueError:
555
556 continue
557 configs[key] = value
558 finally:
559 config_file.close()
560
561 return configs
562
564 """
565 Display the daemon configuration options in an XML format.
566 Merges the defaults with options in the config file.
567 """
568
569 if not daemon or daemon == '':
570 messaging.IMessageSender(self).sendToBrowser(
571 'Internal Error',
572 'Called without a daemon name',
573 priority=messaging.WARNING
574 )
575 return []
576
577 if daemon in [ 'zeoctl', 'zopectl' ]:
578 return []
579
580 xml_default_name = zenPath( "etc", daemon + ".xml" )
581 try:
582
583 log.debug("Creating XML config file for %s" % daemon)
584 make_xml = ' '.join([daemon, "genxmlconfigs", ">", xml_default_name])
585 proc = Popen(make_xml, shell=True, stdout=PIPE, stderr=PIPE)
586 output, errors = proc.communicate()
587 proc.wait()
588 if proc.returncode != 0:
589 log.error(errors)
590 messaging.IMessageSender(self).sendToBrowser(
591 'Internal Error', errors,
592 priority=messaging.CRITICAL
593 )
594 return [["Output", output, errors, make_xml, "string"]]
595 except Exception, ex:
596 msg = "Unable to execute '%s'\noutput='%s'\nerrors='%s'\nex=%s" % (
597 make_xml, output, errors, ex)
598 log.error(msg)
599 messaging.IMessageSender(self).sendToBrowser(
600 'Internal Error', msg,
601 priority=messaging.CRITICAL
602 )
603 return [["Error in command", output, errors, make_xml, "string"]]
604
605 try:
606 xml_defaults = parse( xml_default_name )
607 except:
608 info = traceback.format_exc()
609 msg = "Unable to parse XML file %s because %s" % (
610 xml_default_name, info)
611 log.error(msg)
612 messaging.IMessageSender(self).sendToBrowser(
613 'Internal Error', msg,
614 priority=messaging.CRITICAL
615 )
616 return [["Error parsing XML file", xml_default_name, "XML", info, "string"]]
617
618 configfile = self._getConfigFilename(daemon)
619 try:
620
621 current_configs = self.parseconfig( configfile )
622 except:
623 info = traceback.format_exc()
624 msg = "Unable to obtain current configuration from %s because %s" % (
625 configfile, info)
626 log.error(msg)
627 messaging.IMessageSender(self).sendToBrowser(
628 'Internal Error', msg,
629 priority=messaging.CRITICAL
630 )
631 return [["Configuration file issue", configfile, configfile, info, "string"]]
632
633 all_options = {}
634 ignore_options = ['configfile', 'cycle', 'daemon', 'weblog']
635 try:
636 for option in xml_defaults.getElementsByTagName('option'):
637 id = option.attributes['id'].nodeValue
638 if id in ignore_options:
639 continue
640 try:
641 help = unquote(option.attributes['help'].nodeValue)
642 except:
643 help = ''
644
645 try:
646 default = unquote(option.attributes['default'].nodeValue)
647 except:
648 default = ''
649 if default == '[]':
650 continue
651
652 all_options[id] = [
653 id,
654 current_configs.get(id, default),
655 default,
656 help,
657 option.attributes['type'].nodeValue,
658 ]
659
660 except:
661 info = traceback.format_exc()
662 msg = "Unable to merge XML defaults with config file" \
663 " %s because %s" % (configfile, info)
664 log.error(msg)
665 messaging.IMessageSender(self).sendToBrowser(
666 'Internal Error', msg,
667 priority=messaging.CRITICAL
668 )
669 return [["XML file issue", daemon, xml_default_name, info, "string"]]
670
671 return [all_options[name] for name in sorted(all_options.keys())]
672
673
675 """
676 Save the updated daemon configuration to disk.
677 """
678 if not REQUEST:
679 return
680 elif not hasattr(REQUEST, 'form'):
681 return
682
683
684 formdata = REQUEST.form
685 ignore_names = ['save_daemon_configs', 'zenScreenName', 'daemon_name']
686
687 daemon = formdata.get('daemon_name', '')
688 if not daemon or daemon in ['zeoctl', 'zopectl']:
689 return
690 for item in ignore_names:
691 del formdata[item]
692
693 if not formdata:
694 msg = "Received empty form data for %s config -- ignoring" % (
695 daemon)
696 log.error(msg)
697 messaging.IMessageSender(self).sendToBrowser(
698 'Internal Error', msg,
699 priority=messaging.CRITICAL
700 )
701 return
702
703 configfile = self._getConfigFilename(daemon)
704 config_file_pre = configfile + ".pre"
705 try:
706 config = open( config_file_pre, 'w' )
707 config.write("# Config file written out from GUI\n")
708 for key, value in formdata.items():
709 if value == '':
710 continue
711 if key == value:
712 value = True
713 config.write('%s %s\n' % (key, value))
714 config.close()
715 except Exception, ex:
716 msg = "Couldn't write to %s because %s" % (config_file_pre, ex)
717 log.error(msg)
718 messaging.IMessageSender(self).sendToBrowser(
719 'Internal Error', msg,
720 priority=messaging.CRITICAL
721 )
722 config.close()
723 try:
724 os.unlink(config_file_pre)
725 except:
726 pass
727 return
728
729
730 config_file_save = configfile + ".save"
731 try:
732 shutil.copy(configfile, config_file_save)
733 except:
734 log.error("Unable to make backup copy of %s" % configfile)
735
736 try:
737 shutil.move(config_file_pre, configfile)
738 except:
739 msg = "Unable to save contents to %s" % configfile
740 log.error(msg)
741 messaging.IMessageSender(self).sendToBrowser(
742 'Internal Error', msg,
743 priority=messaging.CRITICAL
744 )
745
746
758 security.declareProtected('Manage DMD','manage_daemonAction')
759
760
762 """
763 Do the given action (start, stop, restart) or the given daemon.
764 Block until the action is completed.
765 No return value.
766 """
767 import time
768 import subprocess
769 daemonPath = binPath(daemonName)
770 if not os.path.isfile(daemonPath):
771 return
772 log.info('Telling %s to %s' % (daemonName, action))
773 proc = subprocess.Popen([daemonPath, action], stdout=subprocess.PIPE,
774 stderr=subprocess.STDOUT)
775 output, _ = proc.communicate()
776 code = proc.wait()
777 if code:
778 log.info('Error from %s: %s (%s)' % (daemonName, output, code))
779 if action in ('stop', 'restart'):
780 time.sleep(2)
781
782
798 security.declareProtected('Manage DMD','manage_checkVersion')
799
800
805
806
813
814
815 InitializeClass(ZenossInfo)
816