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
You can’t perform that action at this time.