[CalendarServer-changes] [13926] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Fri Aug 29 16:52:53 PDT 2014
Revision: 13926
http://trac.calendarserver.org//changeset/13926
Author: sagen at apple.com
Date: 2014-08-29 16:52:53 -0700 (Fri, 29 Aug 2014)
Log Message:
-----------
calendarserver_diagnose
Modified Paths:
--------------
CalendarServer/trunk/setup.py
CalendarServer/trunk/support/Apple.make
Added Paths:
-----------
CalendarServer/trunk/calendarserver/tools/diagnose.py
CalendarServer/trunk/calendarserver/tools/test/test_diagnose.py
Added: CalendarServer/trunk/calendarserver/tools/diagnose.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/diagnose.py (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/diagnose.py 2014-08-29 23:52:53 UTC (rev 13926)
@@ -0,0 +1,759 @@
+#!/usr/bin/env python
+
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from __future__ import print_function
+import sys
+
+
+from getopt import getopt, GetoptError
+import os
+from plistlib import readPlist, readPlistFromString
+import re
+import subprocess
+import urllib2
+
+from twext.internet.ssl import ChainingOpenSSLContextFactory
+import OpenSSL
+
+
+PREFS_PLIST = "/Library/Preferences/com.apple.servermgr_calendar.plist"
+SSLPrivateKey = ""
+SSLCertAdmin = ""
+SSLPassPhraseDialog = ""
+SSLPort = ""
+ServerHostName = ""
+
+
+class FileNotFound(Exception):
+ """
+ Missing file exception
+ """
+
+
+def usage(e=None):
+ if e:
+ print(e)
+ print("")
+
+ name = os.path.basename(sys.argv[0])
+ print("usage: {} [options]".format(name))
+ print("options:")
+ print(" -h --help: print this help and exit")
+
+ if e:
+ sys.exit(64)
+ else:
+ sys.exit(0)
+
+
+def main():
+ try:
+ (optargs, args) = getopt(
+ sys.argv[1:], "h", [
+ "help",
+ ],
+ )
+ except GetoptError, e:
+ usage(e)
+
+ for opt, arg in optargs:
+
+ # Args come in as encoded bytes
+ arg = arg.decode("utf-8")
+
+ if opt in ("-h", "--help"):
+ usage()
+
+
+ osBuild = getOSBuild()
+ print("OS Build: {}".format(osBuild))
+
+ serverBuild = getServerBuild()
+ print("Server Build: {}".format(serverBuild))
+
+
+ print()
+
+ try:
+ if checkPlist(PREFS_PLIST):
+ print("{} exists and can be parsed".format(PREFS_PLIST))
+ else:
+ print("{} exists but cannot be parsed".format(PREFS_PLIST))
+ except FileNotFound:
+ print("{} does not exist (but that's ok)".format(PREFS_PLIST))
+
+ serverRoot = getServerRoot()
+ print("Prefs plist says ServerRoot directory is: {}".format(serverRoot))
+
+ systemPlist = os.path.join(serverRoot, "Config", "caldavd-system.plist")
+ try:
+ if checkPlist(systemPlist):
+ print("{} exists and can be parsed".format(systemPlist))
+ else:
+ print("{} exists but cannot be parsed".format(systemPlist))
+ except FileNotFound:
+ print("{} does not exist".format(systemPlist))
+
+
+ keys = showConfigKeys()
+
+ showProcesses()
+
+ showServerctlStatus()
+
+ showDiskSpace(serverRoot)
+
+ postgresRunning = showPostgresStatus(serverRoot)
+
+ if postgresRunning:
+ showPostgresContent()
+
+
+ password = getPasswordFromKeychain("com.apple.calendarserver")
+
+ connectToAgent(password)
+
+ if keys.get("EnableSSL", "False") == "True":
+ success, message = verifyTLSCertificate(keys)
+ if success:
+ print("TLS Certificate OK")
+ else:
+ print("Problem with TLS certificate: {}".format(message))
+ print("Try resetting the certificate for Calendar and Contacts in Server.app")
+ else:
+ print("TLS is disabled")
+
+ connectToCaldavd(keys)
+
+
+def showProcesses():
+
+ print()
+ print("Calendar and Contacts service processes:")
+
+ code, stdout, stderr = runCommand(
+ "/bin/ps", "ax",
+ "-o user",
+ "-o pid",
+ "-o %cpu",
+ "-o %mem",
+ "-o rss",
+ "-o etime",
+ "-o lstart",
+ "-o command"
+ )
+ for line in stdout.split("\n"):
+ if "_calendar" in line or "CalendarServer" in line or "COMMAND" in line:
+ print(line)
+
+
+def showServerctlStatus():
+
+ print()
+ print("Serverd status:")
+
+ code, stdout, stderr = runCommand(
+ "/Applications/Server.app/Contents/ServerRoot/usr/sbin/serverctl",
+ "list",
+ )
+ services = {
+ "org.calendarserver.agent": False,
+ "org.calendarserver.calendarserver": False,
+ "org.calendarserver.relocate": False,
+ }
+
+ enabledBucket = False
+
+ for line in stdout.split("\n"):
+ if "enabledServices" in line:
+ enabledBucket = True
+ if "disabledServices" in line:
+ enabledBucket = False
+
+ for service in services:
+ if service in line:
+ services[service] = enabledBucket
+
+ for service, enabled in services.iteritems():
+ print(
+ "{service} is {enabled}".format(
+ service=service,
+ enabled="enabled" if enabled else "disabled"
+ )
+ )
+
+
+def showDiskSpace(serverRoot):
+
+ print()
+ print("Disk space on boot volume:")
+
+ code, stdout, stderr = runCommand(
+ "/bin/df",
+ "-H",
+ "/",
+ )
+ print(stdout)
+
+ print("Disk space on service data volume:")
+
+ code, stdout, stderr = runCommand(
+ "/bin/df",
+ "-H",
+ serverRoot
+ )
+ print(stdout)
+
+ print("Disk space used by Calendar and Contacts service:")
+ code, stdout, stderr = runCommand(
+ "/usr/bin/du",
+ "-sh",
+ os.path.join(serverRoot, "Config"),
+ os.path.join(serverRoot, "Data"),
+ os.path.join(serverRoot, "Logs"),
+ )
+ print(stdout)
+
+
+def showPostgresStatus(serverRoot):
+
+ clusterPath = os.path.join(serverRoot, "Data", "Database.xpg", "cluster.pg")
+
+ print()
+ print("Postgres status for cluster {}:".format(clusterPath))
+
+ code, stdout, stderr = runCommand(
+ "/usr/bin/sudo",
+ "-u",
+ "calendar",
+ "/Applications/Server.app/Contents/ServerRoot/usr/bin/pg_ctl",
+ "status",
+ "-D",
+ clusterPath
+ )
+ if stdout:
+ print(stdout)
+ if stderr:
+ print(stderr)
+ if code:
+ return False
+ return True
+
+
+def runSQLQuery(query):
+
+ code, stdout, stderr = runCommand(
+ "/Applications/Server.app/Contents/ServerRoot/usr/bin/psql",
+ "-h",
+ "/var/run/caldavd/PostgresSocket",
+ "--dbname=caldav",
+ "--username=caldav",
+ "--command={}".format(query),
+ )
+ if stdout:
+ print(stdout)
+ if stderr:
+ print(stderr)
+
+
+def countFromSQLQuery(query):
+
+ code, stdout, stderr = runCommand(
+ "/Applications/Server.app/Contents/ServerRoot/usr/bin/psql",
+ "-h",
+ "/var/run/caldavd/PostgresSocket",
+ "--dbname=caldav",
+ "--username=caldav",
+ "--command={}".format(query),
+ )
+ lines = stdout.split("\n")
+ count = int(lines[2])
+ return count
+
+
+def listDatabases():
+
+ code, stdout, stderr = runCommand(
+ "/Applications/Server.app/Contents/ServerRoot/usr/bin/psql",
+ "-h",
+ "/var/run/caldavd/PostgresSocket",
+ "--dbname=caldav",
+ "--username=caldav",
+ "--list",
+ )
+ if stdout:
+ print(stdout)
+ if stderr:
+ print(stderr)
+
+
+def showPostgresContent():
+
+ print()
+ print("Postgres content:")
+ print()
+
+ listDatabases()
+
+ print("'calendarserver' table...")
+ runSQLQuery("select * from calendarserver;")
+
+ count = countFromSQLQuery("select count(*) from calendar_home;")
+ print("Number of calendar homes: {}".format(count))
+
+ count = countFromSQLQuery("select count(*) from calendar_object;")
+ print("Number of calendar events: {}".format(count))
+
+ count = countFromSQLQuery("select count(*) from addressbook_home;")
+ print("Number of contacts homes: {}".format(count))
+
+ count = countFromSQLQuery("select count(*) from addressbook_object;")
+ print("Number of contacts cards: {}".format(count))
+
+ count = countFromSQLQuery("select count(*) from delegates;")
+ print("Number of non-group delegate assignments: {}".format(count))
+
+ count = countFromSQLQuery("select count(*) from delegate_groups;")
+ print("Number of group delegate assignments: {}".format(count))
+
+
+def showConfigKeys():
+
+ print()
+ print("Configuration:")
+
+ code, stdout, stderr = runCommand(
+ "/Applications/Server.app/Contents/ServerRoot/usr/sbin/calendarserver_config",
+ "EnableCalDAV",
+ "EnableCardDAV",
+ "Notifications.Services.APNS.Enabled",
+ "Scheduling.iMIP.Enabled",
+ "Authentication.Basic.Enabled",
+ "Authentication.Digest.Enabled",
+ "Authentication.Kerberos.Enabled",
+ "EnableSSL",
+ "HTTPPort",
+ "SSLPort",
+ "RedirectHTTPToHTTPS",
+ "SSLCertificate",
+ "SSLPrivateKey",
+ "SSLAuthorityChain",
+ "SSLCertAdmin",
+ "SSLPassPhraseDialog",
+ "ServerHostName",
+ )
+ hidden = [
+ "SSLCertificate",
+ "SSLPrivateKey",
+ "SSLAuthorityChain",
+ "SSLCertAdmin",
+ "SSLPassPhraseDialog",
+ "ServerHostName",
+ ]
+ ifHasValue = [
+ "SSLCertificate",
+ "SSLPrivateKey",
+ "SSLAuthorityChain",
+ ]
+ keys = {}
+ for line in stdout.split("\n"):
+ if "=" in line:
+ key, value = line.strip().split("=", 1)
+ keys[key] = value
+ if key not in hidden:
+ print("{key} : {value}".format(key=key, value=value))
+ if key in ifHasValue and value:
+ print("{key} is set".format(key=key))
+ return keys
+
+
+
+def runCommand(commandPath, *args):
+ """
+ Run a command line tool and return the output
+ """
+ if not os.path.exists(commandPath):
+ raise FileNotFound
+
+ commandLine = [commandPath]
+ if args:
+ commandLine.extend(args)
+
+ child = subprocess.Popen(
+ args=commandLine,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ output, error = child.communicate()
+ return child.returncode, output, error
+
+
+def getOSBuild():
+ try:
+ code, stdout, stderr = runCommand("/usr/bin/sw_vers", "-buildVersion")
+ if not code:
+ return stdout.strip()
+ except:
+ return "Unknown"
+
+
+def getServerBuild():
+ try:
+ code, stdout, stderr = runCommand("/usr/sbin/serverinfo", "--buildversion")
+ if not code:
+ return stdout.strip()
+ except:
+ pass
+ return "Unknown"
+
+
+def getServerRoot():
+ """
+ Return the ServerRoot value from the servermgr_calendar.plist. If not
+ present, return the default.
+
+ @rtype: C{unicode}
+ """
+ try:
+ plist = "/Library/Preferences/com.apple.servermgr_calendar.plist"
+ serverRoot = u"/Library/Server/Calendar and Contacts"
+ if os.path.exists(plist):
+ serverRoot = readPlist(plist).get("ServerRoot", serverRoot)
+ if isinstance(serverRoot, str):
+ serverRoot = serverRoot.decode("utf-8")
+ return serverRoot
+ except:
+ return "Unknown"
+
+
+def checkPlist(plistPath):
+ if not os.path.exists(plistPath):
+ raise FileNotFound
+
+ try:
+ readPlist(plistPath)
+ except:
+ return False
+
+ return True
+
+##
+# Keychain access
+##
+
+passwordRegExp = re.compile(r'password: "(.*)"')
+
+
+def getPasswordFromKeychain(account):
+ code, stdout, stderr = runCommand(
+ "/usr/bin/security",
+ "find-generic-password",
+ "-a",
+ account,
+ "-g",
+ )
+ if code:
+ return None
+ else:
+ match = passwordRegExp.search(stderr)
+ if not match:
+ print(
+ "Password for {} not found in keychain".format(account)
+ )
+ return None
+ else:
+ return match.group(1)
+
+
+readCommand = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>command</key>
+ <string>readConfig</string>
+</dict>
+</plist>
+"""
+
+
+def connectToAgent(password):
+
+ print()
+ print("Agent:")
+
+ url = "http://localhost:62308/gateway/"
+ user = "com.apple.calendarserver"
+ auth_handler = urllib2.HTTPDigestAuthHandler()
+ auth_handler.add_password(
+ realm="/Local/Default",
+ uri=url,
+ user=user,
+ passwd=password
+ )
+ opener = urllib2.build_opener(auth_handler)
+ # ...and install it globally so it can be used with urlopen.
+ urllib2.install_opener(opener)
+
+ # Send HTTP POST request
+ request = urllib2.Request(url, readCommand)
+ try:
+ print("Attempting to send a request to the agent...")
+ response = urllib2.urlopen(request, timeout=30)
+ except Exception as e:
+ print("Can't connect to agent: {}".format(e))
+ return False
+
+ html = response.read()
+ code = response.getcode()
+ if code == 200:
+ try:
+ data = readPlistFromString(html)
+ except Exception as e:
+ print(
+ "Could not parse response from agent: {error}\n{html}".format(
+ error=e, html=html
+ )
+ )
+ return False
+
+ if "result" in data:
+ print("...success")
+ else:
+ print("Error in agent's response:\n{}".format(html))
+ return False
+ else:
+ print("Got an error back from the agent: {code} {html}".format(
+ code=code, html=html)
+ )
+
+ return True
+
+
+def connectToCaldavd(keys):
+
+ print()
+ print("Server connection:")
+
+ httpPort = keys.get("HTTPPort", "8008")
+ sslPort = keys.get("SSLPort", "8443")
+ # redirect = keys.get("RedirectHTTPToHTTPS", "False") == "True"
+ sslEnabled = keys.get("EnableSSL", "False") == "True"
+
+ if httpPort:
+ url = "http://localhost:{}/".format(httpPort)
+ try:
+ print("Attempting to send a request to port {}...".format(httpPort))
+ response = urllib2.urlopen(url, timeout=30)
+ html = response.read()
+ code = response.getcode()
+ print(code, html)
+ if code == 200:
+ print("Received 200 response")
+
+ except urllib2.HTTPError as e:
+ code = e.code
+ reason = e.reason
+
+ if code == 401:
+ print("Got the expected response")
+ else:
+ print(
+ "Got an unexpected response: {code} {reason}".format(
+ code=code, reason=reason
+ )
+ )
+
+ except Exception as e:
+ print(
+ "Can't connect to port {port}: {error}".format(
+ port=httpPort, error=e
+ )
+ )
+
+
+ if sslPort and sslEnabled:
+ url = "https://localhost:{}/".format(sslPort)
+ try:
+ print("Attempting to send a request to port {}...".format(sslPort))
+ response = urllib2.urlopen(url, timeout=30)
+ html = response.read()
+ code = response.getcode()
+ print(code, html)
+ if code == 200:
+ print("Received 200 response")
+
+ except urllib2.HTTPError as e:
+ code = e.code
+ reason = e.reason
+
+ if code == 401:
+ print("Got the expected response")
+ else:
+ print(
+ "Got an unexpected response: {code} {reason}".format(
+ code=code, reason=reason
+ )
+ )
+
+ except Exception as e:
+ print(
+ "Can't connect to port {port}: {error}".format(
+ port=sslPort, error=e
+ )
+ )
+ else:
+ print("Skipping TLS port since it's disabled")
+
+
+
+def getSSLPassphrase(*ignored):
+
+ if not SSLPrivateKey:
+ return None
+
+ if SSLCertAdmin and os.path.isfile(SSLCertAdmin):
+ child = subprocess.Popen(
+ args=[
+ "sudo", SSLCertAdmin,
+ "--get-private-key-passphrase", SSLPrivateKey,
+ ],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ )
+ output, error = child.communicate()
+
+ if child.returncode:
+ print(
+ "Could not get passphrase for key: {error}".format(
+ error=error
+ )
+ )
+ else:
+ print("Obtained passphrase for key")
+ return output.strip()
+
+ if (
+ SSLPassPhraseDialog and
+ os.path.isfile(SSLPassPhraseDialog)
+ ):
+ sslPrivKey = open(SSLPrivateKey)
+ try:
+ keyType = None
+ for line in sslPrivKey.readlines():
+ if "-----BEGIN RSA PRIVATE KEY-----" in line:
+ keyType = "RSA"
+ break
+ elif "-----BEGIN DSA PRIVATE KEY-----" in line:
+ keyType = "DSA"
+ break
+ finally:
+ sslPrivKey.close()
+
+ if keyType is None:
+ print("Could not get private key type for key")
+ else:
+ child = subprocess.Popen(
+ args=[
+ SSLPassPhraseDialog,
+ "{}:{}".format(ServerHostName, SSLPort),
+ keyType,
+ ],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ )
+ output, error = child.communicate()
+
+ if child.returncode:
+ print(
+ "Could not get passphrase for key: {error}".format(
+ error=error
+ )
+ )
+ else:
+ return output.strip()
+
+ return None
+
+
+
+def verifyTLSCertificate(keys):
+ """
+ If a TLS certificate is configured, make sure it exists, is non empty,
+ and that it's valid.
+ """
+ global SSLPrivateKey
+ global SSLCertAdmin
+ global SSLPassPhraseDialog
+ global SSLPort
+ global ServerHostName
+
+ certPath = keys.get("SSLCertificate", "")
+ keyPath = keys.get("SSLPrivateKey", "")
+ chainPath = keys.get("SSLAuthorityChain", "")
+
+ SSLPrivateKey = keyPath
+ SSLCertAdmin = keys.get("SSLCertAdmin", "")
+ SSLPassPhraseDialog = keys.get("SSLPassPhraseDialog", "")
+ SSLPort = keys.get("SSLPort", "")
+ ServerHostName = keys.get("ServerHostName", "")
+
+ print()
+ print("Checking TLS Certificate:")
+
+ if certPath:
+ if not os.path.exists(certPath):
+ message = (
+ "The configured TLS certificate ({cert}) is missing".format(
+ cert=certPath
+ )
+ )
+ return False, message
+ else:
+ return False, "EnableSSL is set to true, but certificate path not set"
+
+ length = os.stat(certPath).st_size
+ if length == 0:
+ message = (
+ "The configured TLS certificate ({cert}) is empty".format(
+ cert=certPath
+ )
+ )
+ return False, message
+
+ try:
+ ChainingOpenSSLContextFactory(
+ keyPath,
+ certPath,
+ certificateChainFile=chainPath,
+ passwdCallback=getSSLPassphrase,
+ sslmethod=getattr(OpenSSL.SSL, "SSLv23_METHOD"),
+ ciphers="RC4-SHA:HIGH:!ADH"
+ )
+ except Exception as e:
+ message = (
+ "The configured TLS certificate ({cert}) cannot be used: {reason}".format(
+ cert=certPath,
+ reason=str(e)
+ )
+ )
+ return False, message
+
+ return True, "TLS enabled"
+
+
+
+if __name__ == "__main__":
+ main()
Added: CalendarServer/trunk/calendarserver/tools/test/test_diagnose.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_diagnose.py (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/test/test_diagnose.py 2014-08-29 23:52:53 UTC (rev 13926)
@@ -0,0 +1,35 @@
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from __future__ import print_function
+
+from calendarserver.tools.diagnose import (
+ runCommand, FileNotFound
+)
+from twisted.trial.unittest import TestCase
+
+
+class DiagnoseTestCase(TestCase):
+
+ def test_runCommand(self):
+ code, stdout, stderr = runCommand(
+ "/bin/ls", "-al", "/"
+ )
+ self.assertEquals(code, 0)
+ self.assertEquals(stderr, "")
+ self.assertTrue("total" in stdout)
+
+ def test_runCommand_nonExistent(self):
+ self.assertRaises(FileNotFound, runCommand, "/xyzzy/plugh/notthere")
Modified: CalendarServer/trunk/setup.py
===================================================================
--- CalendarServer/trunk/setup.py 2014-08-29 18:44:34 UTC (rev 13925)
+++ CalendarServer/trunk/setup.py 2014-08-29 23:52:53 UTC (rev 13926)
@@ -163,6 +163,9 @@
"dbinspect":
("calendarserver.tools.dbinspect", "main"),
+ "diagnose":
+ ("calendarserver.tools.diagnose", "main"),
+
"dkimtool":
("calendarserver.tools.dkimtool", "main"),
Modified: CalendarServer/trunk/support/Apple.make
===================================================================
--- CalendarServer/trunk/support/Apple.make 2014-08-29 18:44:34 UTC (rev 13925)
+++ CalendarServer/trunk/support/Apple.make 2014-08-29 23:52:53 UTC (rev 13926)
@@ -149,6 +149,7 @@
caldavd \
calendarserver_config \
calendarserver_command_gateway \
+ calendarserver_diagnose \
calendarserver_export \
calendarserver_manage_principals \
calendarserver_purge_attachments \
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140829/8e9ae5ae/attachment-0001.html>
More information about the calendarserver-changes
mailing list