Sunday, June 6, 2010

Registering JIRA as a Solaris SMF-Managed Service

My company and many of its clients use Atlassian JIRA as a bug or project tracking solution. JIRA is distributed in two bundles:
  • As a Java EE application to deploy on a compliant application server.
  • As a standalone distribution bundled with Apache Tomcat.

I usually recommend, unless you already have a Java EE application server to leverage or some other requirement to take into account, to install JIRA standalone distribution. It's very easy to install, configure and deploy. I would also recommend you to deploy JIRA on Solaris.

Solaris has differential advantages when compared with other operating system. Just to name a few (not to repeat myself over and over):
  • ZFS and ZFS snapshots and clones.
  • Zones.
  • DTrace.
  • RBAC, Projects and Resource Caps.
  • SMF.

A Typical Deployment Scenario

My typical deployment scenario is the following:
  • I deploy JIRA standalone (once for each JIRA version) on a dedicated ZFS filesystem.
  • I snapshot and clone the corresponding filesystem whenever I need a new JIRA instance.
  • Every JIRA instance is configured and executed on a dedicated Solaris 10 Sparse Zone.
  • I define a resource cap for each zone that runs JIRA.
  • I configure JIRA as a Solaris SMF-Managed Service.

Most of the previous tasks have been described earlier in this blog. An overview of a JIRA installation on Solaris has been given on this post.

The purpose of this entry is describing how to configure JIRA as a Solaris SMF-Managed Service.

Configuring JIRA with Solaris SMF

Solaris 10 Service Management Facility (SMF) often praised advantages include the ability for Solaris to check the health and dependencies of your service and take a corrective action in case something goes wrong. Registering a service with Solaris SMF isn't strictly necessary but I encourage you to do so. Although JIRA is pretty easy to start and stop and has got very view dependencies (or none...) with other services, preparing a service manifest will enable you to install and deploy JIRA on Solaris in a question of minutes.

Dependencies

As stated above, JIRA's dependencies really depend on your deployment. As a minimum, JIRA global dependencies will include the two following (obvious) services:
  • svc:/milestone/network:default
  • svc:/system/filesystem/local:default

Because of its own nature, there's probably no reason to have JIRA instances running when network services are not available. The dependency with the local filesystem service is obvious, too.

Any other dependency will probably be a service instance dependency. Since JIRA, in the simplest deployment scenario, will just depend on a database, we will assume that our JIRA instance will depend on a specific Solaris PostgreSQL service:
  • svc:/application/database/postgresql:version_82

Our first manifest fragment will be the following:

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='application/atlassian/jira' type='service' version='0'>
    <dependency name='network' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/milestone/network:default'/>
    </dependency>
    <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/system/filesystem/local:default'/>
    </dependency>
    
    <instance name='version_411' enabled='true'>
      <dependency name='postgresql' grouping='require_all' restart_on='none' type='service'>
        <service_fmri value='svc:/application/database/postgresql:version_82'/>
      </dependency>
      
    </instance>
    <stability value='Unstable'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>Atlassian JIRA</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>

Beware that in this example we're using PostgreSQL services as defined in Solaris 10 05/10. Please check your operating system to define correct dependencies. A missing dependency will make Solaris put the service into the offline state until the dependency can be resolved.

Execution Methods

The next thing to do is define JIRA execution methods. We could directly use JIRA startup.sh and shutdown.sh scripts but we want to avoid coupling the SMF service definition with JIRA implementation details. Rather, we will take the common approach of writing a script to declare execution methods. This approach, moreover, will let us parametrize distinct JIRA instances in the SMF manifest and thus manage multiple JIRA instances with just a SMF service manifest and one execution methods script.

A draft script is the following:

#!/sbin/sh
#
#
#ident  "@(#)JIRA 4.1.1   06/05/10 SMI"

. /lib/svc/share/smf_include.sh

# SMF_FMRI is the name of the target service.
# This allows multiple instances 
# to use the same script.

getproparg() {
  val=`svcprop -p $1 $SMF_FMRI`
  [ -n "$val" ] && echo $val
}

# ATLBIN is the JIRA installation directory
# of each JIRA instance.
ATLBIN=`getproparg atlassian/bin`

# JIRASTARTUP is JIRA startup script
JIRASTARTUP=$ATLBIN/bin/startup.sh

# JIRASHUTDOWN is JIRA shutdown script
JIRASHUTDOWN=$ATLBIN/bin/shutdown.sh

# Check if the SMF framework is correctly initialized.
if [ -z $SMF_FMRI ]; then
  echo "Error: SMF framework variables are not initialized."
  exit $SMF_EXIT_ERR
fi

# check if JIRA scripts are available
if [ ! -x $JIRASTARTUP ]; then
  echo "Error: JIRA startup script cannot be found."
  exit $SMF_EXIT_ERR
fi

if [ ! -x $JIRASHUTDOWN ]; then
  echo "Error: JIRA shutdown script cannot be found."
  exit $SMF_EXIT_ERR
fi

case "$1" in
'start')
  $JIRASTARTUP
  ;;

'stop')
  $JIRASHUTDOWN
  ;;

*)
  echo "Usage: $0 {start|stop}"
  exit 1
  ;;

esac
exit $SMF_EXIT_OK
 
The previous scripts just define the start and stop service methods. As far as I know there's no easy JIRA refresh method so we will skip it in our scripts.

Service Instance Parameters

As shortly explained in the previous section, SMF services provide the ability of being parametrized by virtue of an easy syntax to declare parameters in the service manifest and use them in the service method script. In the previous example we're using a property, called atlassian/bin, to declare the installation directory of each of ours JIRA instances. Let's declare it in our service manifest for a fictional JIRA 4.1.1 instance:

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='application/atlassian/jira' type='service' version='0'>
    <dependency name='network' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/milestone/network:default'/>
    </dependency>
    <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/system/filesystem/local:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='/root/bin/svc/method/atlassian/jira start' timeout_seconds='60'/>
    <exec_method name='stop' type='method' exec='/root/bin/svc/method/atlassian/jira stop' timeout_seconds='60'/>
    <instance name='version_411' enabled='true'>
      <dependency name='postgresql' grouping='require_all' restart_on='none' type='service'>
        <service_fmri value='svc:/application/database/postgresql:version_82'/>
      </dependency>
      <property_group name='atlassian' type='application'>
        <propval name='bin' type='astring'
          value='/opt/atlassian/atlassian-jira-enterprise-4.1.1-standalone'/>
      </property_group>
    </instance>
    <stability value='Unstable'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>Atlassian JIRA</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>

Environment Variables

JIRA needs at least the JAVA_HOME environment variable to be set for startup and shutdown scripts to work correctly. Thus we need to modify our manifest so that SMF will set it before launching the execution methods:

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='application/atlassian/jira' type='service' version='0'>
    <dependency name='network' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/milestone/network:default'/>
    </dependency>
    <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/system/filesystem/local:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='/root/bin/svc/method/atlassian/jira start' timeout_seconds='60'>
      <method_context>
        <method_environment>
          <envvar name='JAVA_HOME' value='/opt/sun/jdk/latest'/>
        </method_environment>
      </method_context>
    </exec_method>
    <exec_method name='stop' type='method' exec='/root/bin/svc/method/atlassian/jira stop' timeout_seconds='60'>
      <method_context>
        <method_environment>
          <envvar name='JAVA_HOME' value='/opt/sun/jdk/latest'/>
        </method_environment>
      </method_context>
    </exec_method>
    <instance name='version_411' enabled='true'>
      <dependency name='postgresql' grouping='require_all' restart_on='none' type='service'>
        <service_fmri value='svc:/application/database/postgresql:version_82'/>
      </dependency>
      <property_group name='atlassian' type='application'>
        <propval name='bin' type='astring' value='/opt/atlassian/atlassian-jira-enterprise-4.1.1-standalone'/>
      </property_group>
    </instance>
    <stability value='Unstable'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>Atlassian JIRA</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>

Credentials, Projects and Resources

As explained in another post, SMF service manifest let the administrator specify advanced parameters for a service instance such as:
  • The credentials under which the service instance will be executed.
  • The Solaris project in which the service instance will be executed.
  • The resource pool assigned to a service instance.

In this case, being a multi instance service manifest, we will let the administrator declare such parameters in every service instance. This way, the maximum degree of service parametrization is achieved:

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='application/atlassian/jira' type='service' version='0'>
    <dependency name='network' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/milestone/network:default'/>
    </dependency>
    <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/system/filesystem/local:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='/root/bin/svc/method/atlassian/jira start' timeout_seconds='60'>
      <method_context>
        <method_environment>
          <envvar name='JAVA_HOME' value='/opt/sun/jdk/latest'/>
        </method_environment>
      </method_context>
    </exec_method>
    <exec_method name='stop' type='method' exec='/root/bin/svc/method/atlassian/jira stop' timeout_seconds='60'>
      <method_context>
        <method_environment>
          <envvar name='JAVA_HOME' value='/opt/sun/jdk/latest'/>
        </method_environment>
      </method_context>
    </exec_method>
    <instance name='version_411' enabled='true'>
      <dependency name='postgresql' grouping='require_all' restart_on='none' type='service'>
        <service_fmri value='svc:/application/database/postgresql:version_82'/>
      </dependency>
      <property_group name='method_context' type='framework'>
        <propval name='project' type='astring' value=':default'/>
        <propval name='resource_pool' type='astring' value=':default'/>
        <propval name='working_directory' type='astring' value=':default'/>
      </property_group>
      <property_group name='atlassian' type='application'>
        <propval name='bin' type='astring' value='/opt/atlassian/atlassian-jira-enterprise-4.1.1-standalone'/>
      </property_group>
    </instance>
    <stability value='Unstable'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>Atlassian JIRA</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>

Please note the slight different syntax for the method_context attributes with respect to the example in my previous post. This gives you an idea of SMF manifest flexibility. In that example, distinct execution methods were specified for every service instance. In this case, we specified the same service methods for every JIRA instance. The difference is that parametrization occurs in the service instance property groups. The same script, then, can be properly parametrized and reused for any of the instances you'll control with SMF.

Privileges Required to Open a Privileged Network Port

If you're running JIRA as a non-root user (that is considered a best practice but perhaps is questionable if you use a dedicated Solaris 10 Zone) you should be aware that a specific privilege is required for Tomcat to open a privileged network port (< 1024) such as port 80 (the default HTTP port.) If you plan to run JIRA as a non root user, then assign the net_privaddr privilege to the JIRA user as shown in the following manifest fragment:

[...snip...]
<exec_method name='start' type='method' exec='/root/bin/svc/method/atlassian/jira start' timeout_seconds='60'>
  <method_context>
    <method_credential
      user='jira'
      group='jira'
      privileges='basic,net_privaddr' />

[...snip...]


Conclusion

The following manifest and script can be used as a starting point for a working JIRA SMF service manifest. It's up to you, system administrator, to apply the modifications you need to fit it into your execution environment.

Service Manifest

<?xml version='1.0'?>
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
<service_bundle type='manifest' name='export'>
  <service name='application/atlassian/jira' type='service' version='0'>
    <dependency name='network' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/milestone/network:default'/>
    </dependency>
    <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'>
      <service_fmri value='svc:/system/filesystem/local:default'/>
    </dependency>
    <exec_method name='start' type='method' exec='/root/bin/svc/method/atlassian/jira start' timeout_seconds='60'>
      <method_context>
        <method_environment>
          <envvar name='JAVA_HOME' value='/opt/sun/jdk/latest'/>
        </method_environment>
      </method_context>
    </exec_method>
    <exec_method name='stop' type='method' exec='/root/bin/svc/method/atlassian/jira stop' timeout_seconds='60'>
      <method_context>
        <method_environment>
          <envvar name='JAVA_HOME' value='/opt/sun/jdk/latest'/>
        </method_environment>
      </method_context>
    </exec_method>
    <instance name='version_411' enabled='true'>
      <dependency name='postgresql' grouping='require_all' restart_on='none' type='service'>
        <service_fmri value='svc:/application/database/postgresql:version_82'/>
      </dependency>
      <property_group name='method_context' type='framework'>
        <propval name='project' type='astring' value=':default'/>
        <propval name='resource_pool' type='astring' value=':default'/>
        <propval name='working_directory' type='astring' value=':default'/>
      </property_group>
      <property_group name='atlassian' type='application'>
        <propval name='bin' type='astring' value='/opt/atlassian/atlassian-jira-enterprise-4.1.1-standalone'/>
      </property_group>
    </instance>
    <stability value='Unstable'/>
    <template>
      <common_name>
        <loctext xml:lang='C'>Atlassian JIRA</loctext>
      </common_name>
    </template>
  </service>
</service_bundle>

Execution Methods Script

#!/sbin/sh
#
#
#ident  "@(#)JIRA 4.1.1   06/05/10 SMI"

. /lib/svc/share/smf_include.sh

# SMF_FMRI is the name of the target service.
# This allows multiple instances 
# to use the same script.

getproparg() {
  val=`svcprop -p $1 $SMF_FMRI`
  [ -n "$val" ] && echo $val
}

# ATLBIN is the JIRA installation directory
# of each JIRA instance.
ATLBIN=`getproparg atlassian/bin`

# JIRASTARTUP is JIRA startup script
JIRASTARTUP=$ATLBIN/bin/startup.sh

# JIRASHUTDOWN is JIRA shutdown script
JIRASHUTDOWN=$ATLBIN/bin/shutdown.sh

# Check if the SMF framework is correctly initialized.
if [ -z $SMF_FMRI ]; then
  echo "Error: SMF framework variables are not initialized."
  exit $SMF_EXIT_ERR
fi

# check if JIRA scripts are available
if [ ! -x $JIRASTARTUP ]; then
  echo "Error: JIRA startup script cannot be found."
  exit $SMF_EXIT_ERR
fi

if [ ! -x $JIRASHUTDOWN ]; then
  echo "Error: JIRA shutdown script cannot be found."
  exit $SMF_EXIT_ERR
fi

case "$1" in
'start')
  $JIRASTARTUP
  ;;

'stop')
  $JIRASHUTDOWN
  ;;

*)
  echo "Usage: $0 {start|stop}"
  exit 1
  ;;

esac
exit $SMF_EXIT_OK








2 comments:

Anonymous said...

hi maria,

can u tell me how to just stop and start jira process under smf control( imean in bin directory start-jirs.sh and stop-jira.sh

Enrico Maria Crisostomo said...

Hi.

My name's Enrico Maria ;)

To enable/disable an SMF-managed service you can use the svcadm command:

$ svcadm enable [service]
$ svcadm disable [service]

Bye,
-- Enrico