JBrisbin.com

Tomcat/tcServer Deployer using RabbitMQ

14
Apr

Tomcat/tcServer Deployer using RabbitMQ

I was prototyping a RabbitMQ-based web application deployer for SpringSource tcServer 6.0 today using the Groovy DSL for RabbitMQ. I'm starting to think this DSL is more than a simple administrative tool, though, because it took me about an hour (hour-and-a-half, tops) to create a consumer that listens to events emitted by the build server (in our case, TeamCity 5.0), downloads the new war file from the special URL and calls a couple very convenient JMX methods within tcServer to redeploy the application.

First Things First

First, I have to connect to the JMX server. The default port for tcServer's JMX listener is 6969, and the login information is still set to the out-of-the-box default:

def p = "hostname -s".execute()
def hostname = p.text

def credentials = [
  "jmx.remote.credentials": ["admin", "springsource"].toArray(new String[2])
]

JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:6969/jmxrmi")
MBeanServerConnection mbeanServer = JMXConnectorFactory.connect(
  jmxUrl, credentials
).MBeanServerConnection

Now, simply set up an exchange and queue to listen for webapp cloud events:

mq.exchange(name: "vcloud.events.artifacts", type: "topic") {

  queue(name: "${hostname}.webapps", routingKey: "webapps.#") {
    consume tag: "${hostname}.webapps", onmessage: {msg ->
    }
  }

}

Since this is our machine's personal queue, I want to listen for all webapp events, so I set a routing key of "webapps.#", which just means "run me whenever any webapp update event happens". If you wanted to get fancy, you could check the context path its asking you to update against your already-deployed context paths and ignore any requests that don't pertain to you. But for the purposes of this quick demo, we'll forgo the formalities there.

Download the File

The body of the event message is a URL link to the actual deployment artifact, which resides on the build server. We'll simply open an InputStream and read that into a temp file we create locally:

println "Downloading new file..."
URL url = new URL(msg.bodyAsString)
InputStream bytesIn = url.openConnection().getInputStream()
File tempFile = File.createTempFile("webapps_", ".war", new File("/tmp"))
FileOutputStream tempFileOut = new FileOutputStream(tempFile)
byte[] buff = new byte[4096]
for (int bytesRead = bytesIn.read(buff); bytesRead > 0;) {
  tempFileOut.write(buff, 0, bytesRead)
  bytesRead = bytesIn.read(buff)
}
tempFileOut.flush()
tempFileOut.close()
bytesIn.close()

Thank you, SpringSource

Thanks to some special code that comes with tcServer, SpringSource has provided a JMX MBean to handle WAR file deployments. We'll use Groovy's special GroovyMBean class to make working with this bean even easier:

def deployer = new GroovyMBean(mbeanServer, "tcServer:type=Serviceability,name=Deployer")
deployer.undeployApplication("Catalina", "localhost", contextPath)
println "Deploying new war file..."
deployer.deployApplication("Catalina", "localhost", contextPath, tempFile.absolutePath)

Prototype or full-on Application?

This was meant to be a prototype. A "get this out of my head and into code" rough draft. Admittedly, it lacks pinache and, notably, error-handling. But this is still a pretty functional piece of Groovy code that can live on the tcServer nodes and listens to my RabbitMQ servers for update events from my build server.

Here's the full code:

import javax.management.MBeanServerConnection
import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

mq.on error: {err ->
  println "ERROR: ${err.message}"
}

mq {channel ->
  //channel.exchangeDelete("vcloud.events.artifacts")
}

def p = "hostname -s".execute()
def hostname = p.text

def credentials = [
  "jmx.remote.credentials": ["admin", "springsource"].toArray(new String[2])
]

JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:6969/jmxrmi")
MBeanServerConnection mbeanServer = JMXConnectorFactory.connect(
  jmxUrl, credentials).MBeanServerConnection

mq.exchange(name: "vcloud.events.artifacts", type: "topic") {

  queue(name: "${hostname}.webapps", routingKey: "webapps.#") {
    consume tag: "${hostname}.webapps", onmessage: {msg ->
      String type = msg.properties.headers["type"]
      println "Received ${type} event for URL ${msg.bodyAsString}"

      String contextPath = "/${msg.envelope.routingKey.substring(8)}"
      println "Context path: /${msg.envelope.routingKey.substring(8)}"

      // Download new file
      println "Downloading new file..."
      URL url = new URL(msg.bodyAsString)
      InputStream bytesIn = url.openConnection().getInputStream()
      File tempFile = File.createTempFile("webapps_", ".war", new File("/tmp"))
      FileOutputStream tempFileOut = new FileOutputStream(tempFile)
      byte[] buff = new byte[4096]
      for (int bytesRead = bytesIn.read(buff); bytesRead > 0;) {
        tempFileOut.write(buff, 0, bytesRead)
        bytesRead = bytesIn.read(buff)
      }
      tempFileOut.flush()
      tempFileOut.close()
      bytesIn.close()

      // Get MBean reference using special Groovy class
      def deployer = new GroovyMBean(mbeanServer, "tcServer:type=Serviceability,name=Deployer")
      // Deploy this application
      deployer.undeployApplication("Catalina", "localhost", contextPath)
      println "Deploying new war file..."
      deployer.deployApplication("Catalina", "localhost", contextPath, tempFile.absolutePath)

      println "Deleting temp file..."
      tempFile.delete()

      return false
    }
  }

}

send("vcloud.events.artifacts", "webapps.rest",
    [ "type": "updated", "source": "buildserver" ],
    "http://teamcityserver/path/to/rest.war")

blog comments powered by Disqus