Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
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:
# 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.on 'loaded', =>"Loading rundeck jobs from brain")
if @brain.rundeck?"Loaded saved rundeck jobs")
@cache = @brain.rundeck
else"No saved rundeck jobs found ")
@brain.rundeck = @cache
cache: -> @cache
parser: -> new Parser()
jobs: -> new Jobs(@)
save: ->"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)
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)
parser.parseString body, (e, json) ->
cb json
class Job
constructor: (data) ->
@id = data["$"].id
@name =[0]
@description = data.description[0]
@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
jobs.push new Job(job)
cb jobs
find: (name, cb) ->
@list (jobs) =>
job = _.findWhere jobs, { name: name }
if job
cb job
cb false
run: (name, query, cb) ->
@find name, (job) =>
if job
uri = "job/#{}/run"
uri += query if query?
@rundeck.get uri, (results) ->
cb job, results
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, -> (jobs) ->
if jobs.length > 0
msg.send formatter.formatList(jobs)
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}```"
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, -> name, (job) ->
if job
msg.send formatter.format(job)
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, -> name, null, (job, results) ->
if job
msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
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, -> name, query, (job, results) ->
if job
msg.send "Running job #{name}: #{results.result.executions[0].execution[0]['$'].href}"
msg.send "Could not execute Rundeck job \"#{name}\"."

This comment has been minimized.

Copy link

@poolski 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) => body
      if err?
        @logger.error JSON.stringify(err)
      else "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..


This comment has been minimized.

Copy link

@steeef steeef commented Mar 9, 2015

I realize this is rather old, but its the simplest Rundeck plugin for Hubot I found. It looks like 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:

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