CFUnited: Automating the Build & Deploy Process with ANT

Tuesday, June 24, 2008

I was presenting on an old ghetto laptop that couldn't run connect (although in all fairness to the P.O.S. machine, connect is a total hog.). Anyway, the most important part other than my charm and grace is the code, right? So the presentation, with all promised code, jar file dependencies, etc is here at the bottom of the page. My "wingman", the goatee'd fellow asking really good questions, asked me another great one the following day and I thought it bore mentioning here. During the 2nd part of the presentation, I demo'd macrodef and used an example of looping over a query of servers, and for each server, running a bat file that would open up a connection to that server. Then, it'd copy the code. Then, it'd run that same bat file and close the connection to the server. Currently, our environment is completely locked out of the prod environment, rightfully so. And so in my example, I was saying that the bat file in question would temporariliy open up a connection to each production server and then close that connection. My goatee'd wingman asked, "but that doesn't prevent you from running that bat file any other time, does it?" And he's absolutely right. Here's a case where I'm hoping that our network admins will work with us, understanding that we have nothing to gain by opening up those connections except during the deploy process. But that remains to be seen. This is definitely a case of "let's compromise". So, thanks to all who showed up for the session, and extra special thanks for the brave folks who stuck around for the 2nd half of it! For the curious and bored, here's the code in question. The contents of OpenOrClose.bat are irrelevant here... it's the concept we were discussing that matters. The "deployToAllServers" target is the big daddy.
<project name="CFUnited (j): SQL, for, MacroDef, and exec" basedir="." default="getServers">

 <target name="init">

     <property name="app.name" value="Client1App" />

     <property name="dev.root" location="DEVSERVER" />
     <property name="test.root" location="TESTSERVER" />
     <property name="locations.dev.clientroot" location="${dev.root}\${app.name}" />
     <property name="locations.test.clientroot" location="${test.root}\${app.name}" />

     <property name="locations.dev.customtags" location="${dev.root}\CustomTags" />
     <property name="locations.test.customtags" location="${test.root}\CustomTags" />

     <property name="locations.test.deploy" location="${locations.test.clientroot}\deploy" />
     <property name="locations.test.deployzip" location="${locations.test.deploy}\${app.name}.zip" />



     <!-- read all our 'secure' properties from this file; this defines the sql.userid and sql.password properties -->
     <property file="unames.properties" />

     <!-- create a classpath for ANT to use for finding and running the jdbc driver(s) -->
     <property name="jdbclibdir" location="lib" />
     <path id="jdbc.classpath">
         <fileset dir="${jdbclibdir}">
             <include name="**/*.jar" />
         </fileset>
     </path>

     <!-- these properties would be better placed in a .properties file -->
     <!-- this will use the jtds.jar in the classpath -->
     <property name="sqlserver.driver" value="net.sourceforge.jtds.jdbc.Driver" />
     <property name="sqlserver.url" value="jdbc:jtds:sqlserver://localhost:1436/ANT;instance=NetSDK" />

     <!-- this will use the mysql-connector jar in the classpath -->
     <property name="mysql.driver" value="com.mysql.jdbc.Driver" />
     <property name="mysql.url" value="jdbc:mysql://localhost/ANT" />

     <!-- this is where the sql resultset will be stored -->
     <property name="db.output" value="serverslist.txt" />

     <!-- http://sourceforge.net/projects/ant-contrib -->
     <taskdef resource="net/sf/antcontrib/antlib.xml" classpathref="jdbc.classpath" />

 </target>


 <target name="getServers" depends="init" description="Queries a db for a list of servers">

     <sql driver="${mysql.driver}" url="${mysql.url}" userid="${sql.username}" password="${sql.password}" classpathref="jdbc.classpath" print="yes" output="${db.output}" showheaders="false" showtrailers="false">
     select ServerIP from servers where ActiveFlag=1
     </sql>

 </target>


 <!--

 IMAGINE: You have a database table of servers to which you'll deploy.
 You want to query for the "active" servers, and for each server
 Copy your code onto it. This would assume an active network connection between
 the machine you're on and the servers to which you are deploying
 -->




 <target name="loopOverServers" depends="init,getServers">
     <loadfile srcFile="${db.output}" property="serverlist" />
     <for list="${serverlist}" param="server" delimiter="${line.separator}">
         <sequential>

             <echo>Copying to @{server}\Apps\${app.name}</echo>


             <copy toDir="@{server}\Apps\${app.name}" preserveLastModified="true" includeEmptyDirs="false">
                 <fileset dir="${locations.test.clientroot}" />
             </copy>

             <tstamp>
                 <format pattern="MM/dd/yyyy hh:mm aa" offset="-5" unit="year" property="customtagfilter" />
             </tstamp>
             <copy toDir="@{server}\CustomTags" preserveLastModified="true" includeEmptyDirs="false">
                 <fileset dir="${locations.test.customtags}">
                     <date datetime="${customtagfilter}" when="after" />
                 </fileset>
             </copy>
          
         </sequential>
     </for>

 </target>






 <!--  NOW IMAGINE:

 You want to do the same as above, but you need to open and close the connection to each server
 because your network people run a tight ship, and they don't want connections open
 between your dev environment and your other environments except
 during the brief time it takes to copy code

 -->

 <target name="deployToAllServers" depends="init,getServers">
     <!-- open the connections using a bat file provided to you by your network admins;
     we'll call the bat file using a self-made task that we create with macrodef -->
     <OpenOrCloseConnections action="open" />

     <!-- call the loopOverServers target and copy all code -->
     <antcall target="loopOverServers" />

     <!-- close the connections -->
     <OpenOrCloseConnections action="close" />
 </target>


 <!-- MACRODEF: this little gem lits you define your own tasks! -->

 <macrodef name="OpenOrCloseConnections">
     <attribute name="action" default="close" />
     <!-- imagine: this OpenOrClose.bat is provided to you by your network people to open
     up the appropriate connections, and then close them, for the life of your script.
     In this way, you can directly copy files across the network during a brief window of
     access and then automatically remove that access once the deployment is over -->
     <sequential>
         <exec executable="cmd">
             <arg value="/c" />
             <arg value="OpenOrClose.bat" />
             <arg value="-@{action}" />
         </exec>
     </sequential>
 </macrodef>


 <!-- FINALLY, IMAGINE:

 Imagine a table called "Deployments" and a table called "J_Deployments_Servers". And
 every time you deploy, you insert a row into the deployments table, get the ID
 of that deployment, and for each server you copy code to, you insert a row
 into J_Deployments_Servers with some audit information (IP address of machine
 from which the deployment was run, the exact time of the copy, etc).

 How would you do that using sql and macrodef up in your loopOverServers target?



 -->



</project>

2 comments:

Jim Priest said...

That's cool.

Marc Esher said...

it'll be cooler if I can get them to agree to let us try it out. This one issue .... of being able to copy the code from A to B, could save sooo much time and aggravation when compared with zipping, uploading, unzipping, verification, etc.

now that I say that, i'm virtually certain that they won't let us precisely because i think network people like to know you're jumping through a whole lot of hoops to get things to work. it's like "how much pain did you cause today?" is the only question that matters.