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