Hack and /

Update Tickets from the Command Line

Kyle Rankin

Issue #278, June 2017

Why use that pesky mouse when you can pipe command-line output and have it appear as a comment in your ticketing system?

In the April 2017 issue, I wrote about how to use ticketing systems as a sysadmin to organize your tasks better. In that article, I made a brief reference to the fact that I've integrated some of my own scripts with my ticketing system, so here I'm going to talk about a basic bash script I whipped up in a few minutes that can add a comment to a Jira ticket. Although my examples specifically are for use with the Jira ticketing system, you can adapt the same kind of idea to any ticketing system that allows you to interact with it via a web-based API.

One reason many sysadmins dislike ticketing systems is due to the fact that updating and maintaining tickets requires a shift in focus and a break from their regular workflow. To me, tickets are a great way to build an audit trail to show that you've completed a task, and one of the best ways to demonstrate that a task is complete is to paste in the proof of your work into comments in a ticket. Unfortunately, all of that copying and pasting can slow things down and discourages sysadmins from updating their tickets with command output.

My solution to the pain of keeping tickets updated with command output is to create a script I can pipe output to and have it appear as a comment in the ticket I specify. My tasks often generate output into log files, and it's nice just to pipe those log files into a script and have them show up in a ticket. I generally use my create_jira_comment script like this:

$ somecommand | create_jira_ticket -t TICKETNAME

My command may be as simple as an echo command or something much more sophisticated. In some cases, I've wanted to wrap the output inside a code block within the ticket comment and pass along a header to describe what the code block is, so I've added -C and -H options, respectively:

$ somecommand | create_jira_ticket -t TICKETNAME -C -H "Here 
 ↪is the output from somecommand"

This makes it really easy for an administrator to update a ticket with command output without messing with copying and pasting pages of output into a ticket. The output shows up formatted properly, and when it's in a code block, the ticket shows it in such a way that someone can scroll through it to read the whole thing, but it doesn't fill up a whole page.

Before I get into the Jira-specific bits, let me go over the more generic parts of the script. First, there's the opening section of the script where I define a few variables, set some defaults and source a settings file so I don't have to have the password be hard-coded into this script:

#!/bin/bash

JIRA_HOST="somehost.example.com"
JIRA_USER="someuser"
JIRA_PASS="somepass"
# Set the user and password in a settings file 
# instead of in the script
. /etc/default/jira_settings

OUTFILE="/tmp/create_jira_comment-$(date +%Y%m%d-%H%M%S)"

Next, I add a typical usage function (like all good script writers should) to output if someone doesn't use the right syntax with my script:

# Show usage information
usage() {
  cat >&2 <<EOF
Usage:
  $0 [-h | -t TICKET <-f FILENAME> <-H "Header text"> 
 ↪<-F "Footer text"> <-C>]

This script adds a comment to a Jira ticket based on command-line
arguments.

OPTIONS:
  -h              Show usage information (this message).
  -t TICKET       The Jira ticket name (ie SA-101)
  -f FILENAME     A file containing content to past in the Jira 
comment (or - to read from pipe)
  -H HEADER_TEXT  Text to put at the beginning of the comment
  -F FOOTER_TEXT  Text to put at the end of the comment
  -C              Wrap comment in a {code} tags
EOF
}

As you can see in the usage output, the script can accept a number of command-line arguments. The one required option is -t, which specifies the name of the ticket to which you want to add the comment. All of the other options are optional.

As I started using this script, I realized that I often was piping command output into this ticket, and the default formatting inside a Jira comment just made it a huge wall of text. The -C option will wrap all of the text into a tag so that it shows up like code and is easier to read. Once I started wrapping output inside proper formatting, I realized sometimes I wanted to add text above or below the code block that explained what the code block was. I added the -H and -F arguments at that point, which let me specify text to use as a header or footer around the code block.

The next section of the script is where I gather up the command-line options using the standard getopts tool:


# Parse Options
while getopts ":t:f:H:F:Ch" flag; do
  case "$flag" in
    h)
      usage
      exit 3
      ;;
    t)
      TICKET="${OPTARG}"
      ;;
    f)
      FILENAME="${OPTARG}"
      ;;
    H)
      HEADER="${OPTARG}"
      ;;
    F)
      FOOTER="${OPTARG}"
      ;;
    C)
      CODETAG='{code}'
      ;;
    \?)
      echo "Invalid option: -$OPTARG"
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument"
      exit 1
      ;;
  esac
done

# Shift past all parsed arguments
shift $((OPTIND-1))

test -z "$TICKET" && usage && echo "No ticket specified!" && exit 1
test -z "$FILENAME" && FILENAME='-'

There's really not all that much to elaborate on with the getopts tool. You can see how I handle the case where a ticket isn't defined and how I set the default file to be pipe input if the user doesn't set it.

Now that I have all of the options, I can do the actual work of creating the Jira ticket. First, I need to create a file that's formatted in JSON in a way that the JIRA API can accept:

echo -n -e '{\n  "body": "' > ${OUTFILE}.json
test -z "$HEADER" || echo -n -e "${HEADER}\n" >> ${OUTFILE}.json
test -z "$CODETAG" || echo -n -e "${CODETAG}\n" >> ${OUTFILE}.json
cat ${FILENAME} | perl -pe 's/\r//g; s/\n/\\r\\n/g; s/\"/\\"/g' >>
 ↪${OUTFILE}.json
test -z "$CODETAG" || echo -n -e "\n${CODETAG}" >> ${OUTFILE}.json
test -z "$FOOTER" || echo -n -e "\n${FOOTER}" >> ${OUTFILE}.json
echo -e '"\n}' >> ${OUTFILE}.json

You can see in the previous code where I test whether a header, code argument or footer was defined, and if so, I inject the text or the appropriate code tags at the right places in the JSON file. That said, the meat of the formatting is right in the middle where I cat the main output into a series of Perl regular expressions to clean up carriage returns and newlines in the output and also escape quotes properly. This would be the section where you'd apply any other cleanup to your output if you noticed it broke JSON formatting.

Once I have a valid JSON file, I can use curl to send it to Jira in a POST request with the following command:

curl -s -S -u $JIRA_USER:$JIRA_PASS -X POST --data @${OUTFILE}.json -H
 ↪"Content-Type: application/json"
 ↪https://$JIRA_HOST/rest/api/latest/issue/${TICKET}/comment 
 ↪2>&1 >> $OUTFILE

if [ $? -ne 0 ]; then
  echo "Creating Jira Comment failed"
  exit 1
fi

If the command fails, I alert the user, and since I captured the curl output in the $OUTFILE log file, I can review it to see what went wrong.

Here is the full script all in one piece:

#!/bin/bash

JIRA_HOST="somehost.example.com"
JIRA_USER="someuser"
JIRA_PASS="somepass"
# Set the user and password in a settings file 
# instead of in the script
. /etc/default/prod_release

OUTFILE="/tmp/create_jira_comment-$(date +%Y%m%d-%H%M%S)"

# Show usage information
usage() {
  cat >&2 <<EOF
Usage:
  $0 [-h | -t TICKET <-f FILENAME> <-H "Header text"> 
 ↪<-F "Footer text"> <-C>]

This script adds a comment to a Jira ticket based on 
command-line arguments.

OPTIONS:
  -h              Show usage information (this message).
  -t TICKET       The Jira ticket name (ie SA-101)
  -f FILENAME     A file containing content to past in the Jira 
 ↪comment (or - to read from pipe)
  -H HEADER_TEXT  Text to put at the beginning of the comment
  -F FOOTER_TEXT  Text to put at the end of the comment
  -C              Wrap comment in a {code} tags
EOF
}

# Parse Options
while getopts ":t:f:H:F:Ch" flag; do
  case "$flag" in
    h)
      usage
      exit 3
      ;;
    t)
      TICKET="${OPTARG}"
      ;;
    f)
      FILENAME="${OPTARG}"
      ;;
    H)
      HEADER="${OPTARG}"
      ;;
    F)
      FOOTER="${OPTARG}"
      ;;
    C)
      CODETAG='{code}'
      ;;
    \?)
      echo "Invalid option: -$OPTARG"
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument"
      exit 1
      ;;
  esac
done

# Shift past all parsed arguments
shift $((OPTIND-1))

test -z "$TICKET" && usage && echo "No ticket specified!" 
 ↪&& exit 1
test -z "$FILENAME" && FILENAME='-'

echo -n -e '{\n  "body": "' > ${OUTFILE}.json
test -z "$HEADER" || echo -n -e "${HEADER}\n" >> ${OUTFILE}.json
test -z "$CODETAG" || echo -n -e "${CODETAG}\n" >> ${OUTFILE}.json
cat ${FILENAME} | perl -pe 's/\r//g; s/\n/\\r\\n/g; 
 ↪s/\"/\\"/g' >> ${OUTFILE}.json
test -z "$CODETAG" || echo -n -e "\n${CODETAG}" >> ${OUTFILE}.json
test -z "$FOOTER" || echo -n -e "\n${FOOTER}" >> ${OUTFILE}.json
echo -e '"\n}' >> ${OUTFILE}.json

curl -s -S -u $JIRA_USER:$JIRA_PASS -X POST --data @${OUTFILE}.json -H
 ↪"Content-Type: application/json"
 ↪https://$JIRA_HOST/rest/api/latest/issue/${TICKET}/comment 
 ↪2>&1 >> $OUTFILE

if [ $? -ne 0 ]; then
  echo "Creating Jira Comment failed"
  exit 1
fi

I've found I now use this script all the time to interact with my ticketing system. In the past, there were times when I could get a bit lazy with archiving proof of work into Jira tickets unless I knew it was truly necessary, but with this script, it's easy, so I find I do it more. In general, I've found if you can make the correct workflow the easiest workflow, your team is more likely to follow it. This script is just one example of how that can work in practice.

Kyle Rankin is VP of engineering operations at Final, Inc., the author of many books including Linux Hardening in Hostile Networks, DevOps Troubleshooting and The Official Ubuntu Server Book, and a columnist for Linux Journal. Follow him @kylerankin.