Skip to content

Instantly share code, notes, and snippets.

@lusis
Created August 8, 2014 17:11
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save lusis/861e449896b4f3dbcd20 to your computer and use it in GitHub Desktop.
Save lusis/861e449896b4f3dbcd20 to your computer and use it in GitHub Desktop.
rundeck slack hubot integration

This is a pretty opinionated solution that we use internally. It's strictly designed to post to slack via the API and it uses our notion of wrapping EVERYTHING with a role. All of our plugins automatically use brain storage as well. To be able to execute anything with hubot, you have to be a rundeck_admin role user.

You should be able to tease out the rundeck API stuff specifically.

It depends on a common format for your job defs in rundeck. We have two types of jobs in rundeck that we use via this plugin:

  • ad-hoc
  • predefined

ALL of our jobs have a common parameter called slack_channel. Hubot will automatically set this for you based on where/who it was talking to.

The ad-hoc jobs all have an option called nodename. When you call the job via hubot, you pass the nodename as the last option like so:

hubot rundeck adhoc why-run dcm-logstash-01

this would run chef in why-run mode on node dcm-logstash-01

We also have predefined jobs in rundeck that take no arguments. Those we simply run with:

hubot rundeck run do-some-thing

You can get the status of any job with:

hubot rundeck output <jobid>

This will post preformatted text to the slack api.

This was all largely written by Dan Ryan with a bit of tweaks from other team members.

# Description
# Rundeck integration with hubot
#
# Dependencies:
# "underscore": "^1.6.0"
# "strftime": "^0.8.0"
# "xml2js": "^0.4.1"
#
# Configuration:
# HUBOT_RUNDECK_URL
# HUBOT_RUNDECK_TOKEN
# HUBOT_RUNDECK_PROJECT
#
# Commands:
# hubot rundeck (list|jobs) - List all Rundeck jobs
# hubot rundeck show <name> - Show detailed info for the job <name>
# hubot rundeck run <name> - Execute a Rundeck job <name>
# hubot rundeck (adhoc|ad-hoc|ad hoc) <name> <nodename> - Execute an ad-hoc Rundeck job <name> on node <nodename>
# hubot rundeck output <id> - Print the output of execution <id>
#
# Notes:
# Todo:
# * make job name lookups case-insensitive
# * ability to show results of a job/execution
# * query job statistics
#
# Author:
# <dan.ryan@XXXXXXXXXX>
_ = require('underscore')
sys = require 'sys' # Used for debugging
querystring = require 'querystring'
url = require 'url'
inspect = require('util').inspect
strftime = require('strftime')
Parser = require('xml2js').Parser
Formatter = require('../src/formatter').formatter
class Rundeck
constructor: (@robot) ->
@logger = @robot.logger
@baseUrl = process.env.HUBOT_RUNDECK_URL
@authToken = process.env.HUBOT_RUNDECK_TOKEN
@project = process.env.HUBOT_RUNDECK_PROJECT
@adminRole = "rundeck_admin"
@headers =
"Accept": "application/xml"
"Content-Type": "application/xml"
"X-Rundeck-Auth-Token": "#{@authToken}"
@plainTextHeaders =
"Accept": "text/plain"
"Content-Type": "text/plain"
"X-Rundeck-Auth-Token": "#{@authToken}"
@cache = {}
@cache['jobs'] = {}
@logger = @robot.logger
@brain = @robot.brain.data
robot.brain.on 'loaded', =>
@logger.info("Loading rundeck jobs from brain")
if @brain.rundeck?
@logger.info("Loaded saved rundeck jobs")
@cache = @brain.rundeck
else
@logger.info("No saved rundeck jobs found ")
@brain.rundeck = @cache
cache: -> @cache
parser: -> new Parser()
jobs: -> new Jobs(@)
save: ->
@logger.info("Saving cached rundeck jobs to brain")
@brain.rundeck = @cache
getOutput: (url, cb) ->
@robot.http("#{@baseUrl}/#{url}").headers(@plainTextHeaders).get() (err, res, body) =>
if err?
@logger.err JSON.stringify(err)
else
cb body
get: (url, cb) ->
parser = new Parser()
@robot.http("#{@baseUrl}/#{url}").headers(@headers).get() (err, res, body) =>
if err?
@logger.error JSON.stringify(err)
else
parser.parseString body, (e, json) ->
cb json
class Job
constructor: (data) ->
@id = data["$"].id
@name = data.name[0]
@description = data.description[0]
@group = data.group[0]
@project = data.project[0]
format: ->
"Name: #{@name}\nId: #{@id}\nDescription: #{@description}\nGroup: #{@group}\nProject: #{@project}"
formatSlack: ->
"*Name:* #{@name}\n*ID:* #{@id}\n*Description:* #{@description}\n*Group:* #{@group}\n*Project:* #{@project}"
formatList: ->
"#{@name} - #{@description}"
formatListSlack: ->
"*#{@name}* - #{@description}"
class Jobs
constructor: (@rundeck) ->
@logger = @rundeck.logger
list: (cb) ->
jobs = []
@rundeck.get "project/#{@rundeck.project}/jobs", (results) ->
for job in results.jobs.job
jobs.push new Job(job)
cb jobs
find: (name, cb) ->
@list (jobs) =>
job = _.findWhere jobs, { name: name }
if job
cb job
else
cb false
run: (name, query, cb) ->
@find name, (job) =>
if job
uri = "job/#{job.id}/run"
uri += query if query?
@rundeck.get uri, (results) ->
cb job, results
else
cb null, false
module.exports = (robot) ->
logger = robot.logger
rundeck = new Rundeck(robot)
formatter = new Formatter(robot)
# hubot rundeck list
robot.respond /rundeck (?:list|jobs)$/i, (msg) ->
robot.authorize msg, rundeck.adminRole, ->
rundeck.jobs().list (jobs) ->
if jobs.length > 0
msg.send formatter.formatList(jobs)
else
msg.send "No Rundeck jobs found."
# hubot rundeck output <job-id>
# sample url:
robot.respond /rundeck output (.+)/i, (msg) ->
jobid = msg.match[1]
robot.authorize msg, rundeck.adminRole, ->
rundeck.getOutput "execution/#{jobid}/output", (output) ->
if output
msg.send "```#{output}```"
else
msg.send "Could not find output for Rundeck job \"#{jobid}\"."
# hubot rundeck show <name>
robot.respond /rundeck show ([\w -_]+)/i, (msg) ->
name = msg.match[1]
robot.authorize msg, rundeck.adminRole, ->
rundeck.jobs().find name, (job) ->
if job
msg.send formatter.format(job)
else
msg.send "Could not find Rundeck job \"#{name}\"."
# hubot rundeck run <name>
robot.respond /rundeck run ([\w -_]+)/i, (msg) ->
name = msg.match[1]
robot.authorize msg, rundeck.adminRole, ->
rundeck.jobs().run name, null, (job, results) ->
if job
msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
else
msg.send "Could not execute Rundeck job \"#{name}\"."
# takes all but last word as the name of our job
# hubot rundeck ad-hoc <name> <nodename>
robot.respond /rundeck (?:ad[ -]?hoc) ([\w -_]+) ([\w-]+)/i, (msg) ->
name = msg.match[1]
params = { argString: "-nodename #{msg.match[2].trim().toLowerCase()}" }
query = "?#{querystring.stringify(params)}"
robot.authorize msg, rundeck.adminRole, ->
rundeck.jobs().run name, query, (job, results) ->
if job
msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
else
msg.send "Could not execute Rundeck job \"#{name}\"."
@poolski
Copy link

poolski commented Aug 11, 2014

Is Formatter a custom package you guys wrote or is it available on the Interwebs?

I managed to adapt your code to what we're doing, but I can't get the XML into anything usable. Here's version of your Rundeck.get()

get: (url, cb) ->
    parser = new Parser()
    @robot.http("#{@baseUrl}/#{@apiVer}/#{url}").headers(@headers).get() (err, res, body) =>
      @logger.info body
      if err?
        @logger.error JSON.stringify(err)
      else
        @logger.info "Parsing response"
        parser.parseString body, (e, json) ->
          cb json 

parse.parseString body, (e, json) doesn't seem to work or return anything that I can see..

@steeef
Copy link

steeef commented Mar 9, 2015

I realize this is rather old, but its the simplest Rundeck plugin for Hubot I found. It looks like https://github.com/opentable/hubot-rundeck is based on this, but they've complicated it by allowing multiple projects and tokens, as well as storing the credentials and URL in the Hubot brain, which I don't need. The syntax of the call to Hubot that @lusis has created is much less verbose too, which I prefer.

I've forked this script here to fix what I could:
https://gist.github.com/steeef/6d647a87686afbf4c228

Notable differences:

  • Removed references to ../src/formatter, since I couldn't find it anywhere. It looks to be a way to specify the output format based on whether you're using Slack or not. For now, I've removed Slack-specific formatted messages.
  • Required the latest Rundeck API (version 12 as of this writing). I was getting undefined errors when returning the results of a run or ad-hoc call, and realized that the default return had changed.
  • It looks like @lusis is using another plugin for defining the robot.authorize method, as I couldn't find it here. Instead, I'm relying on hubot-auth to be installed. You still have to be a member of rundeck_admin to use this plugin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment