Using Nerrvana – deployment & Jenkins (part 2)

Using Nerrvana - deployment & Jenkins (part 2)

Jenkins job creation – our web
application deployment in details – deployment
for Selenium testing completed

Part 1 – Using Nerrvana – our setup
Part 2 – Using Nerrvana – SVN hooks to Jenkins
Part 3 – Using Nerrvana – deployment & Jenkins (part 1)
Part 4 – Using Nerrvana – deployment & Jenkins (part 2) – this post

In the previous post we have finished configuring Jenkins and now we can create a Jenkins job to test our application.

Open the page http://your_jenkins_host/view/All/newJob and type in the job name. Since our application is Answers, we use it as our job name.

In practice, it is convenient to add branch information to your job name. For example, job ‘Answers TRUNK’ tests TRUNK, job ‘Answers Release 1.4′ tests Release 1.4.

New Jenkins job

Down in the section “Advanced Project Options” press the button “Advanced …” and set “Quiet period” to 0 seconds. We know that many people do builds a few times a day. In this case, you will configure this option differently. We build for each commit, and therefore we do not need to wait for 5 seconds. Jenkins help for this step describes the practical cases, when this option is required.

New Jenkins job - Advanced project options

Now we go to the section ‘Source Code Management’ and select SVN, which we use.

New Jenkins job - Source Code Management

Jenkins will ask you to enter SVN user details. Choose ‘Check out strategy’. We use – ‘Always check out a fresh copy’.

New Jenkins job - Check out strategy

The next section is ‘Build Triggers’. Use ‘Poll SCM’ option. In our case, the ‘push’ trigger, which Jenkins help advises to use, is the SVN hook that we added earlier. If you open context help for the option ‘Build periodically’, you will see the following remark: “When people first start continuous integration, they are often so used to the idea of regularly scheduled builds like nightly/weekly that they use this feature. However, the point of continuous integration is to start a build as soon as a change is made, to provide a quick feedback to the change.” This is why our builds start immediately after each commit.

New Jenkins job - Build Triggers

Skip the next section ‘Build Environment’ and go to the ‘Build’ section. Push the button ‘Add build step’ and then choose the ‘Invoke Ant’.

New Jenkins job - Add Build Step

As you can see, the Ant configuration file we use here is also in SVN. Upon completion of the checkout on the previous step, it falls into the right place at the right time. The purpose of this step is to prepare the code extracted from the version control system to be transferred to the deployment host where deployment will be completed. We want to do the maximum possible operations on jenkins host.

New Jenkins job - Invoke Ant

Next I’ll tell you what Ant does. Here’s what our build.xml is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="UTF-8"?>
<project name="${env.JOB_NAME}" default="build" basedir=".">
    <property environment="env"/>
    <property name="tests" value="${env.WORKSPACE}/tests"/>
    <property name="ci" value="${env.WORKSPACE}/ci"/>
    <property name="code" value="${env.WORKSPACE}/code"/>
 
    <target name="make_settings">
        <exec executable="sed" output="${code}/config/Settings.class.php.mysql">
            <arg line="-f ${ci}/deployment/config/mysql/settings-mysql.sed ${code}/config/Settings.class.template.php" />
        </exec>
    </target>
 
    <target name="make_sql">
        <exec executable="sed" output="${code}/install/mysql.sql">
            <arg line="-f ${ci}/deployment/config/mysql/install-mysql.sed ${code}/install/mysql.sql" />
        </exec>
    </target>
 
    <target name="move_files" depends="make_settings,make_sql">
        <!-- Move authentication files to WAUT root -->
       <move file="${ci}/deployment/misc/login.php" tofile="${code}/login.php" />
        <move file="${ci}/deployment/misc/logout.php" tofile="${code}/logout.php" />
 
        <!-- Move MySQL db creation files to install folder -->
        <move file="${ci}/deployment/config/mysql/crt-mysql.sql" tofile="${code}/install/crt-mysql.sql" />
 
        <!-- Move publish script to the workspace root, so it will be beside prj.zip -->
       <move file="${ci}/deployment/publish-over-ssh/publish.sh" tofile="${env.WORKSPACE}/publish.sh" />
    </target>
 
    <target name="save_commit_info" depends="move_files">
        <exec dir="${env.WORKSPACE}" executable="svn" >
                    <arg line="--username jenkins --password 123456 upgrade" />
        </exec>    
      <exec dir="${env.WORKSPACE}" executable="svn"  output="${env.WORKSPACE}/version.txt">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
      <exec dir="${env.WORKSPACE}" executable="svn">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
 
    </target>   
 
    <target name="svn" depends="save_commit_info">
       <java dir="." jar="${ci}/deployment/scm-decorator.jar" fork="true" failonerror="true">
         <arg value="svn"/>
         <arg value="${env.WORKSPACE}/version.txt"/> 
       </java>       
    </target>
 
    <target name="zip" depends="svn" description="Compress project files">
        <jar destfile="${env.WORKSPACE}/prj.zip"
            basedir="${env.WORKSPACE}/code"
            excludes=".svn" />
    </target>
 
   <target name="build" depends="zip" />
</project>

All other operations are performed on the deployment host after the results of this step are sent to it using the ‘Send files or execute commands over SSH’ build step.

I hope my explanation will help you create Ant build better, smarter and faster.

First of all, I want to say that if you get stuck with paths – paste code below into your Ant file and use it as a target – you will see in the Jenkins console output what paths Jenkins operates while executing your Ant file.

<target name="test">
    <echo message="Jenkins workspace: ${env.WORKSPACE}"/>
    <echo message="Job directory: ${env.WORKSPACE}/../../${env.JOB_NAME}"/>
    <echo message="Build data: ${env.WORKSPACE}/../../${env.JOB_NAME}/builds/${env.BUILD_ID}"/>
</target>

In lines 4-6, we give short names (aliases) to often used directories to make fewer mistakes down below in our commands and make the file easier to read.

<property name="tests" value="${env.WORKSPACE}/tests"/>
    <property name="ci" value="${env.WORKSPACE}/ci"/>
    <property name="code" value="${env.WORKSPACE}/code"/>

Next (lines 8-12), our task is to create a configuration file for the application. As we wrote earlier, our application is downloadable and is installed with the included installation script. Here we just use ‘sed’ to convert the template Settings.class.template used in the installation script into the application configuration file Settings.class.php.

<target name="make_settings">
        <exec executable="sed" output="${code}/config/Settings.class.php.mysql">
            <arg line="-f ${ci}/deployment/config/mysql/settings-mysql.sed ${code}/config/Settings.class.template.php" />
        </exec>
    </target>

I will not show the files in their entirety, but just a few lines from a template file – Settings.class.template, a few lines of settings-mysql.sed file and the same rows in the newly created configuration file Settings.class.php.

// Database IP or hostname (use colon, if runs on non-standard port: 127.0.0.1:563)
    const DB_HOST = "";
 
    // Username to connect to DB
    const DB_USER = "";
s/const DB_USER = "";/const DB_USER = "ANSWERS_DB_USER";/g
s/const DB_HOST = "";/const DB_HOST = "localhost";/g
// Database IP or hostname (use colon, if runs on non-standard port: 127.0.0.1:563)
    const DB_HOST = "localhost";
 
    // Username to connect to DB
    const DB_USER = "ANSWERS_DB_USER";

You can see that in the file names and paths we have ‘mysql’ That’s because we are also testing our application with PostgreSQL. I removed everything about PostgreSQL from this post and, as promised, will show the real configuration files in the final post. I do not like to be overloaded with too much information when someone is explaining something new to me, and I decided to show you the same courtesy.

As a result of this step, we created a configuration file for the application to work with MySQL – Settings.class.php.mysql, which is stored in the same folder with Settings.class.template.php – ${code}/config .

In lines 14-18, we create the necessary SQL file, which when executed on the deployment host, will recreate the MySQL database for our application.

<target name="make_sql">
        <exec executable="sed" output="${code}/install/mysql.sql">
            <arg line="-f ${ci}/deployment/config/mysql/install-mysql.sed ${code}/install/mysql.sql" />
        </exec>
    </target>

Again, the Answers installer uses a prepared for the installation script mysql.sql file. Based on user input, installer replaces, for example, the names of the tables, but we can use it unchanged. A few changes which are necessary are made using ‘sed’. In this case, we modify the original file ${code}/install/mysql.sql.

s/USERS_TABLE/USERS_VIEW_NAME/g
s/USE ANSWERS_DATABASE/USE answers/g

Next – target “move_files” (lines 20-30). As you will recall, our application is embedded and does not have its own login page. However, for testing we need login and logout pages, which we have created. They are in the ${ci}/deployment/misc/ folder and now it’s time to move them to the root of the application – $ {code}/, as these pages will be used by Selenium tests (lines 21-23).

<target name="move_files" depends="make_settings,make_sql">
        <!-- Move authentication files to WAUT root -->
       <move file="${ci}/deployment/misc/login.php" tofile="${code}/login.php" />
        <move file="${ci}/deployment/misc/logout.php" tofile="${code}/logout.php" />
 
        <!-- Move MySQL db creation files to install folder -->
        <move file="${ci}/deployment/config/mysql/crt-mysql.sql" tofile="${code}/install/crt-mysql.sql" />
 
        <!-- Move publish script to the workspace root, so it will be beside prj.zip -->
       <move file="${ci}/deployment/publish-over-ssh/publish.sh" tofile="${env.WORKSPACE}/publish.sh" />
    </target>

In lines 25-26, we move another SQL file – crt-mysql.sql, from ${ci}/deployment/config/mysql/ to $ {code}/install/, since we send to the deployment host only the content of ${code}/ folder.

What is in the file crt-mysql.sql file? Here again, because of the embeddable nature of our application that creates its tables in the database of your main application, we need at least to (re)create a basic application tables (users’ accounts). After that we would be able to execute the file mysql.sql, we have prepared earlier which will embed Answers data structures. That is the role of the script – rebuild the database, create the “main” application and its users, and delete the database user, the previous deployment run created. That’s how it looks without cuts:

DROP DATABASE answers;
CREATE DATABASE answers;
use answers;
 
DROP TABLE if exists USERS_SOURCE_TABLE;
CREATE TABLE `USERS_SOURCE_TABLE` (
  USERS_ID_COLUMN int(11),
  username varchar(50),
  USERS_DISPLAY_NAME_COLUMN varchar(50),
  USERS_EMAIL_COLUMN varchar(50),
  CONSTRAINT USERS_SOURCE_TABLE_pkey PRIMARY KEY (USERS_ID_COLUMN)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
INSERT INTO USERS_SOURCE_TABLE VALUES(1,'answ_test_admin@dslabs.lan','Arnie - Admin','answ_test_admin@dslabs.lan');
INSERT INTO USERS_SOURCE_TABLE VALUES(2,'answ_test_user@dslabs.lan','Ivan Danko - Captain','answ_test_user@dslabs.lan');
INSERT INTO USERS_SOURCE_TABLE VALUES(3,'answ_test_staff@dslabs.lan','Lee - Guru(Staff)','answ_test_staff@dslabs.lan');
 
DROP USER 'ANSWERS_DB_USER'@localhost;
FLUSH PRIVILEGES;

In conclusion of this part in lines 28-29, we move publsih.sh script to the root of our Jenkins job workspace. The script knows what to do with packed into zip file content of /code, which we have prepared. Publish.sh content will be discussed when we describe the step ‘Send files or execute commands over SSH’.

In lines 32-43, we put information about the commit which triggered our Jenkins build into a text file.

<target name="save_commit_info" depends="move_files">
        <exec dir="${env.WORKSPACE}" executable="svn" >
                    <arg line="--username jenkins --password 123456 upgrade" />
        </exec>    
      <exec dir="${env.WORKSPACE}" executable="svn"  output="${env.WORKSPACE}/version.txt">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>
      <exec dir="${env.WORKSPACE}" executable="svn">  
          <arg line="--username jenkins --password 123456 log -r ${env.SVN_REVISION}" />
      </exec>                  
    </target>

In lines 33-35, I have a workaround for the problem when your SVN server is a higher version than the one supported by the Jenkins SVN plugin. It can happen. In this case the command ‘svn info’ errors with version mismatch and asks to upgrade the working copy, which I do before running ‘svn info’. Lines 39-41 are only to output information we saved into the text file and also to the Jenkins console log. We will use the information saved into this file in a future step to convert it to a more pleasant format and add it later in the Nerrvana test run’s description.

Now we are using a utility which is written by us, and is available on GitHub, which takes the file shown below as an input and …

#cat version.txt
------------------------------------------------------------------------
r69 | igork | 2012-10-03 07:38:38 +0000 (Wed, 03 Oct 2012) | 1 line
 
Fixed bug #342
------------------------------------------------------------------------

… converts it into this file:

#cat version.txt
Revision: 69
Committer: igork
Date: 2012-10-03 07:38:38 +0000 (Wed, 03 Oct 2012)
Fixed bug #342

You can extend the code by adding support for a version control system that you use, if it will be useful to you.

That’s how conversion is done with Ant:

<target name="svn" depends="save_commit_info">
       <java dir="." jar="${ci}/deployment/scm-decorator.jar" fork="true" failonerror="true">
         <arg value="svn"/>
         <arg value="${env.WORKSPACE}/version.txt"/> 
       </java>       
    </target>

As you can see we added the parameter expecting someone else, or ourselves, to expand for Git later.

Finally, we zip the folder /code/ in lines 51-55 to file prj.zip.

<target name="zip" depends="svn" description="Compress project files">
        <jar destfile="${env.WORKSPACE}/prj.zip"
            basedir="${env.WORKSPACE}/code"
            excludes=".svn" />
    </target>

Go to the next build step (and last one for today) – transfer prepared files and do final steps of deployment on deployment host.

New Jenkins job - Send files or execute commands over SSH

You can see that we are transferring to the deployment host (determined by the first parameter – ‘SSH Server’ and we created it, when configured Jenkins – in this post) two files prj.zip and publish.sh. We also instruct Jenkins to run publish.sh after the transfer:

chmod +x publish.sh
./publish.sh

Now is the time to see what publish.sh script does.

#!/bin/sh
 
WWW_MYSQL_DIR='/var/www/answers/answers_mysql'
DB_NAME='answers'
 
rm -Rf $WWW_MYSQL_DIR/*
unzip -o prj.zip -d $WWW_MYSQL_DIR
mv $WWW_MYSQL_DIR/config/Settings.class.php.mysql $WWW_MYSQL_DIR/config/Settings.class.php
chmod -R 777 $WWW_MYSQL_DIR
mysql -v -u root  < $WWW_MYSQL_DIR/install/crt-mysql.sql
mysql -v -u root  < $WWW_MYSQL_DIR/install/mysql.sql $DB_NAME

Cleans Apache virtual host folder:

rm -Rf $WWW_MYSQL_DIR/*

Decompresses Answers into it:

unzip -o prj.zip -d $WWW_MYSQL_DIR

Renames prepared Answers configuration file to be used with MySQL database:

mv $WWW_MYSQL_DIR/config/Settings.class.php.mysql $WWW_MYSQL_DIR/config/Settings.class.php

Changes permissions (I admit, we could be a little bit more conservative here):

chmod -R 777 $WWW_MYSQL_DIR

Creates a ‘main’ application: (-v is used to be able to see verbose output from SQL script inside Jenkins console log):

mysql -v -u root < $WWW_MYSQL_DIR/install/crt-mysql.sql

Integrates Answers into the ‘main’ application:

mysql -v -u root < $WWW_MYSQL_DIR/install/mysql.sql $DB_NAME

We are done! We do a commit and see the build’s console logs in Jenkins until we have no errors and have a running Answers application on http://deployment_host/answers_mysql.

As you can see all the files used by Jenkins are also in the version control system. This allows us to not use custom workspaces in Jenkins (we started with them), which would require Jenkins pre-configuration as well as remembering to update files in our version control system and in Jenkins. All files fall into the right places after Jenkins updates the source code in the workspace after a commit. Also, with this approach, we can use the environment variables Jenkins sets in our configuration files.

Under the spoiler below you can find a sample console log with my comments. I removed some long outputs and marked it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Started by an SCM change
Building in workspace /var/lib/jenkins/jobs/Answers_deployment_only/workspace
Cleaning local Directory .
Checking out http://192.168.3.97/repos/answers/trunk at revision 140
A         tests
------ Output removed - comment by Igor --------------------------------- 
AU        ci/deployment/scm-decorator.jar
A         ci/deployment/build.xml
A         ci/toolchain
A         ci/toolchain/build-toolchain.xml
At revision 140
[deployment] $ ant -file build.xml build
Buildfile: build.xml
 
make_settings:
 
make_sql:
 
move_files:
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace/code
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace/code/install
     [move] Moving 1 file to /var/lib/jenkins/jobs/Answers_deployment_only/workspace
 
save_commit_info:
     [exec] Upgraded '.'
------ Output removed - comment by Igor --------------------------------- 
     [exec] Upgraded 'ci/deployment/misc'
     [exec] Upgraded 'ci/toolchain'
     [exec] ------------------------------------------------------------------------
     [exec] r140 | igork | 2012-10-08 11:46:01 +0000 (Mon, 08 Oct 2012) | 1 line
     [exec] 
     [exec] Test 2345
     [exec] ------------------------------------------------------------------------
 
svn:
 
zip:
      [jar] Building jar: /var/lib/jenkins/jobs/Answers_deployment_only/workspace/prj.zip
 
build:
 
BUILD SUCCESSFUL
Total time: 0 seconds
SSH: Connecting from host [mantis.deepshiftlabs.com]
SSH: Connecting with configuration [Answers test server] ...
SSH: EXEC: STDOUT/STDERR from command [chmod +x publish.sh
./publish.sh] ...
Archive:  prj.zip
   creating: /var/www/answers/answers_mysql/META-INF/
------ Output removed - comment by Igor ---------------------------------   
  inflating: /var/www/answers/answers_mysql/utils/Utils.class.php  
  inflating: /var/www/answers/answers_mysql/utils/init.php  
--------------
drop database answers
--------------
 
--------------
create database answers
--------------
------ Output removed - comment by Igor --------------------------------- 
--------------
GRANT SELECT ON USERS_VIEW_NAME TO 'ANSWERS_DB_USER'@localhost
--------------
 
SSH: EXEC: completed after 1,003 ms
SSH: Disconnecting configuration [Answers test server] ...
SSH: Transferred 2 file(s)
Build step 'Send files or execute commands over SSH' changed build result to SUCCESS
Finished: SUCCESS
 
1-11 - Jenkins responded to a commit and checked out a fresh copy
12-40 - build.xml is running with target the ‘build’
24-28 - 'svn upgrade' is updating a working copy (workaround I described)
29-33 – our SCM decorator parses ‘svn log’ output
44-46 - the results were sent to the deployment host and publish.sh is executed
49-52 - prj.zip unpacked to the root of the Apache’s virtual host
53-63 - publish.sh executes SQL script

In our next post we will extend Jenkins job we created today and launch Selenium tests in Nerrvana.

Print this post | Home

2 comments

  1. Emergy says:

    А где возможность отката?

  2. admin says:

    Алекс, вы имеете в виду откат в предыдущее состояние в случае, если в процессе деплоймента произошла ошибка? Дело в том, что в данном посте описывается процесс деплоймента тестовых систем, на которых затем запускаются селениумные тесты. Поэтому нет необходимости возращаться в предыдущее состояние, проще перезапустить процесс после устранения причин ошибки.