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