[CalendarServer-changes] [15553] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Apr 25 18:17:01 PDT 2016


Revision: 15553
          http://trac.calendarserver.org//changeset/15553
Author:   sagen at apple.com
Date:     2016-04-25 18:17:01 -0700 (Mon, 25 Apr 2016)
Log Message:
-----------
Workers now post server alerts by sending them over AMP to the master, since elevated privileges are required.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/test/test_util.py
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/inbound.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/smtpsender.py

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2016-04-25 22:07:02 UTC (rev 15552)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2016-04-26 01:17:01 UTC (rev 15553)
@@ -120,7 +120,7 @@
     pgServiceFromConfig, getDBPool, MemoryLimitService,
     storeFromConfig, getSSLPassphrase, preFlightChecks,
     storeFromConfigWithDPSClient, storeFromConfigWithoutDPS,
-    serverRootLocation
+    serverRootLocation, AlertPoster
 )
 try:
     from calendarserver.version import version
@@ -972,6 +972,9 @@
         else:
             groupCacher = None
 
+        # Allow worker to post alerts to master
+        AlertPoster.setupForWorker(controlSocketClient)
+
         def decorateTransaction(txn):
             txn._pushDistributor = pushDistributor
             txn._rootResource = result.rootResource
@@ -1742,6 +1745,9 @@
         controlSocket = ControlSocket()
         controlSocket.addFactory(_LOG_ROUTE, logger)
 
+        # Allow master to receive alert posts from workers
+        AlertPoster.setupForMaster(controlSocket)
+
         # Optionally set up AMPPushMaster
         if (
             config.Notifications.Enabled and

Modified: CalendarServer/trunk/calendarserver/tap/test/test_util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/test/test_util.py	2016-04-25 22:07:02 UTC (rev 15552)
+++ CalendarServer/trunk/calendarserver/tap/test/test_util.py	2016-04-26 01:17:01 UTC (rev 15553)
@@ -16,12 +16,15 @@
 
 from calendarserver.tap.util import (
     MemoryLimitService, Stepper, verifyTLSCertificate, memoryForPID,
-    secondsSinceLastPost, recordTimeStamp
+    AlertPoster, AMPAlertProtocol,
+    AMPAlertSender
 )
 
 from twisted.internet.defer import succeed, inlineCallbacks
 from twisted.internet.task import Clock
+from twisted.protocols.amp import AMP
 from twisted.python.filepath import FilePath
+from twisted.test.testutils import returnConnected
 
 from twistedcaldav.config import ConfigDict
 from twistedcaldav.test.util import TestCase
@@ -311,21 +314,21 @@
 
         # Non existent timestamp file
         self.assertEquals(
-            secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=10),
+            AlertPoster.secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=10),
             10
         )
 
         # Existing valid, past timestamp file
-        recordTimeStamp("TestAlert", timestampsDirectory=timestampsDir, now=5)
+        AlertPoster.recordTimeStamp("TestAlert", timestampsDirectory=timestampsDir, now=5)
         self.assertEquals(
-            secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=12),
+            AlertPoster.secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=12),
             7
         )
 
         # Existing valid, future timestamp file
-        recordTimeStamp("TestAlert", timestampsDirectory=timestampsDir, now=20)
+        AlertPoster.recordTimeStamp("TestAlert", timestampsDirectory=timestampsDir, now=20)
         self.assertEquals(
-            secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=12),
+            AlertPoster.secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=12),
             -8
         )
 
@@ -334,6 +337,29 @@
         child = dirFP.child(".TestAlert.timestamp")
         child.setContent("not a number")
         self.assertEquals(
-            secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=12),
+            AlertPoster.secondsSinceLastPost("TestAlert", timestampsDirectory=timestampsDir, now=12),
             12
         )
+
+
+    def stubPostAlert(self, alertType, ignoreWithinSeconds, args):
+        self.alertType = alertType
+        self.ignoreWithinSeconds = ignoreWithinSeconds
+        self.args = args
+
+
+    def test_protocol(self):
+        self.patch(AlertPoster, "postAlert", self.stubPostAlert)
+        AlertPoster.setupForTest()
+
+        client = AMP()
+        server = AMPAlertProtocol()
+        pump = returnConnected(server, client)
+
+        sender = AMPAlertSender(protocol=client)
+        sender.sendAlert("alertType", ["arg1", "arg2"])
+        pump.flush()
+
+        self.assertEquals(self.alertType, "alertType")
+        self.assertEquals(self.ignoreWithinSeconds, 0)
+        self.assertEquals(self.args, ["arg1", "arg2"])

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2016-04-25 22:07:02 UTC (rev 15552)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2016-04-26 01:17:01 UTC (rev 15553)
@@ -25,7 +25,7 @@
     "getRootResource",
     "getSSLPassphrase",
     "MemoryLimitService",
-    "postAlert",
+    "AlertPoster",
     "preFlightChecks",
 ]
 
@@ -59,7 +59,9 @@
 from twisted.internet import reactor as _reactor
 from twisted.internet.defer import inlineCallbacks, returnValue, Deferred, succeed
 from twisted.internet.reactor import addSystemEventTrigger
+from twisted.internet.protocol import Factory
 from twisted.internet.tcp import Connection
+from twisted.protocols import amp
 from twisted.python.usage import UsageError
 
 from twistedcaldav.bind import doBind
@@ -1320,7 +1322,7 @@
                         cert=config.SSLCertificate
                     )
                 )
-                postAlert("MissingCertificateAlert", 0, ["path", config.SSLCertificate])
+                AlertPoster.getAlertPoster().postAlert("MissingCertificateAlert", 0, ["path", config.SSLCertificate])
                 return False, message
 
             length = os.stat(config.SSLCertificate).st_size
@@ -1385,7 +1387,7 @@
             try:
                 getAPNTopicFromConfig(protocol, accountName, protoConfig)
             except ValueError as e:
-                postAlert("PushNotificationCertificateAlert", 0, [])
+                AlertPoster.getAlertPoster().postAlert("PushNotificationCertificateAlert", 0, [])
                 return False, str(e)
 
             # Let OpenSSL try to use the cert
@@ -1420,7 +1422,7 @@
                             reason=str(e)
                         )
                     )
-                postAlert("PushNotificationCertificateAlert", 0, [])
+                AlertPoster.getAlertPoster().postAlert("PushNotificationCertificateAlert", 0, [])
                 return False, message
 
         return True, "APNS enabled"
@@ -1497,68 +1499,210 @@
     return None
 
 
+#
+# Server Alert Posting
+#
 
-def secondsSinceLastPost(alertType, timestampsDirectory=None, now=None):
-    if timestampsDirectory is None:
-        timestampsDirectory = config.DataRoot
-    if now is None:
-        now = int(time.time())
+class AlertPoster(object):
+    """
+    Encapsulates the posting of server alerts via a singleton which can be
+    configured differently depending on whether you want to directly spawn
+    the external alert program from this process, or send an AMP request to
+    another process to spawn.  Workers should call setupForWorker( ), and
+    then calls to postAlert( ) will send an AMP message to the master.  The
+    master should call setupForMaster( ), and then calls to postAlert( )
+    within the master will spawn the external alert program.
+    """
 
-    dirFP = FilePath(timestampsDirectory)
-    childFP = dirFP.child(".{}.timestamp".format(alertType))
-    if not childFP.exists():
-        timestamp = 0
-    else:
-        with childFP.open() as child:
-            try:
-                line = child.readline().strip()
-                timestamp = int(line)
-            except:
-                timestamp = 0
-    return now - timestamp
+    # Control socket message-routing constants
+    ALERT_ROUTE = "alert"
 
+    _alertPoster = None
 
 
-def recordTimeStamp(alertType, timestampsDirectory=None, now=None):
-    if timestampsDirectory is None:
-        timestampsDirectory = config.DataRoot
-    if now is None:
-        now = int(time.time())
+    @classmethod
+    def setupForMaster(cls, controlSocket):
+        cls._alertPoster = cls()
+        AMPAlertReceiver(controlSocket)
 
-    dirFP = FilePath(timestampsDirectory)
-    childFP = dirFP.child(".{}.timestamp".format(alertType))
-    childFP.setContent(str(now))
 
+    @classmethod
+    def setupForWorker(cls, controlSocket):
+        cls._alertPoster = cls(controlSocket)
 
 
-def postAlert(alertType, ignoreWithinSeconds, args):
-    if (
-        config.AlertPostingProgram and
-        os.path.exists(config.AlertPostingProgram)
-    ):
+    @classmethod
+    def setupForTest(cls):
+        cls._alertPoster = cls()
+
+
+    @classmethod
+    def getAlertPoster(cls):
+        return cls._alertPoster
+
+
+    def __init__(self, controlSocket=None):
+
+        if controlSocket is None:
+            self.sender = None
+        else:
+            self.sender = AMPAlertSender(controlSocket)
+
+
+    def postAlert(self, alertType, ignoreWithinSeconds, args):
+
+        if not config.AlertPostingProgram:
+            return
+
+        if not os.path.exists(config.AlertPostingProgram):
+            return
+
         if ignoreWithinSeconds:
-            seconds = secondsSinceLastPost(alertType)
+            seconds = self.secondsSinceLastPost(alertType)
             if seconds < ignoreWithinSeconds:
                 return
 
-        recordTimeStamp(alertType)
+        if self.sender is None:
+            # Just do it
 
-        try:
-            commandLine = [config.AlertPostingProgram, alertType]
-            commandLine.extend(args)
-            Popen(
-                commandLine,
-                stdout=PIPE,
-                stderr=PIPE,
-            ).communicate()
-        except Exception, e:
-            log.error(
-                "Could not post alert: {alertType} {args} ({error})",
-                alertType=alertType, args=args, error=e
+            self.recordTimeStamp(alertType)
+
+            try:
+                commandLine = [config.AlertPostingProgram, alertType]
+                commandLine.extend(args)
+                Popen(
+                    commandLine,
+                    stdout=PIPE,
+                    stderr=PIPE,
+                ).communicate()
+            except Exception, e:
+                log.error(
+                    "Could not post alert: {alertType} {args} ({error})",
+                    alertType=alertType, args=args, error=e
+                )
+
+        else:
+            # Send request to master over AMP
+            self.sender.sendAlert(alertType, args)
+
+
+
+    @classmethod
+    def secondsSinceLastPost(cls, alertType, timestampsDirectory=None, now=None):
+        if timestampsDirectory is None:
+            timestampsDirectory = config.DataRoot
+        if now is None:
+            now = int(time.time())
+
+        dirFP = FilePath(timestampsDirectory)
+        childFP = dirFP.child(".{}.timestamp".format(alertType))
+        if not childFP.exists():
+            timestamp = 0
+        else:
+            with childFP.open() as child:
+                try:
+                    line = child.readline().strip()
+                    timestamp = int(line)
+                except:
+                    timestamp = 0
+        return now - timestamp
+
+
+
+    @classmethod
+    def recordTimeStamp(cls, alertType, timestampsDirectory=None, now=None):
+        if timestampsDirectory is None:
+            timestampsDirectory = config.DataRoot
+        if now is None:
+            now = int(time.time())
+
+        dirFP = FilePath(timestampsDirectory)
+        childFP = dirFP.child(".{}.timestamp".format(alertType))
+        childFP.setContent(str(now))
+
+
+
+
+class AMPAlertSendingFactory(Factory):
+
+    def __init__(self, sender):
+        self.sender = sender
+
+    def buildProtocol(self, addr):
+        protocol = amp.AMP()
+        self.sender.protocol = protocol
+        return protocol
+
+
+
+class AMPAlertSender(object):
+    """
+    Runs in the workers, sends alerts to the master via AMP
+    """
+
+    def __init__(self, controlSocket=None, protocol=None):
+        self.protocol = protocol
+        if controlSocket is not None:
+            controlSocket.addFactory(AlertPoster.ALERT_ROUTE, AMPAlertSendingFactory(self))
+
+
+    def sendAlert(self, alertType, args):
+        return self.protocol.callRemote(
+            PostAlert, alertType=alertType, args=args
+        )
+
+
+
+class AMPAlertReceiverFactory(Factory):
+
+    def buildProtocol(self, addr):
+        return AMPAlertProtocol()
+
+
+
+class AMPAlertReceiver(object):
+    """
+    Runs in the master, receives alerts from workers, executes the alert posting program
+    """
+
+    def __init__(self, controlSocket):
+        if controlSocket is not None:
+            # Set up the listener which gets alerts from the slaves
+            controlSocket.addFactory(
+                AlertPoster.ALERT_ROUTE, AMPAlertReceiverFactory()
             )
 
 
 
+class PostAlert(amp.Command):
+    arguments = [
+        ('alertType', amp.String()),
+        ('args', amp.ListOf(amp.String())),
+    ]
+    response = [
+        ('status', amp.String()),
+    ]
+
+
+
+
+class AMPAlertProtocol(amp.AMP):
+    """
+    Defines the AMP protocol for sending alerts from worker to master
+    """
+
+    @PostAlert.responder
+    def postAlert(self, alertType, args):
+        """
+        The "PostAlert" handler in the master
+        """
+        AlertPoster.getAlertPoster().postAlert(alertType, 0, args)
+        return {
+            "status": "OK"
+        }
+
+
+
 def serverRootLocation():
     """
     Return the ServerRoot value from the OS X preferences plist.  If plist not

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/inbound.py	2016-04-25 22:07:02 UTC (rev 15552)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/inbound.py	2016-04-26 01:17:01 UTC (rev 15553)
@@ -43,7 +43,7 @@
 import email.utils
 
 from OpenSSL.SSL import Error as TLSError
-from calendarserver.tap.util import postAlert
+from calendarserver.tap.util import AlertPoster
 
 log = Logger()
 
@@ -640,7 +640,7 @@
 
     def connectionLost(self, reason):
         if reason.type is TLSError:
-            postAlert("MailCertificateAlert", 7 * 24 * 60 * 60, [])
+            AlertPoster.getAlertPoster().postAlert("MailCertificateAlert", 7 * 24 * 60 * 60, [])
 
 
     def serverGreeting(self, capabilities):

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/smtpsender.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/smtpsender.py	2016-04-25 22:07:02 UTC (rev 15552)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/imip/smtpsender.py	2016-04-26 01:17:01 UTC (rev 15553)
@@ -61,8 +61,8 @@
             )
             from OpenSSL.SSL import Error as TLSError
             if failure.type is TLSError:
-                from calendarserver.tap.util import postAlert
-                postAlert("MailCertificateAlert", 7 * 24 * 60 * 60, [])
+                from calendarserver.tap.util import AlertPoster
+                AlertPoster.getAlertPoster().postAlert("MailCertificateAlert", 7 * 24 * 60 * 60, [])
             return False
 
         deferred = defer.Deferred()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20160425/44147dd5/attachment-0001.html>


More information about the calendarserver-changes mailing list