Create a gist now

Instantly share code, notes, and snippets.

@steeef / forked from lusis/
Last active Feb 4, 2017

What would you like to do?

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 (per the hubot-auth plugin).

HUBOT_RUNDECK_URL should be set to the root URL of your Rundeck server, not including the path to the current api version. NOTE: Currently relying on Rundeck API version 12.

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"
# "hubot-auth"
# Configuration:
# HUBOT_RUNDECK_URL - root URL for Rundeck, not including api path
# 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:
# REQUIRES Rundeck API version 12
# 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
class Rundeck
constructor: (@robot) ->
@logger = @robot.logger
@baseUrl = "#{process.env.HUBOT_RUNDECK_URL}/api/12"
@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)
@logger.debug body
parser.parseString body, (e, result) ->
cb result
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}"
formatList: ->
"#{@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)
# hubot rundeck list
robot.respond /(?:rd|rundeck) (?:list|jobs)$/i, (msg) ->
if robot.auth.hasRole(msg.envelope.user, rundeck.adminRole) (jobs) ->
if jobs.length > 0
for job in jobs
msg.send job.formatList()
msg.send "No Rundeck jobs found."
msg.send "#{msg.envelope.user}: you do not have #{rundeck.adminRole} role."
# hubot rundeck output <job-id>
# sample url:
robot.respond /(?:rd|rundeck) output (.+)/i, (msg) ->
jobid = msg.match[1]
if robot.auth.hasRole(msg.envelope.user, rundeck.adminRole)
rundeck.getOutput "execution/#{jobid}/output", (output) ->
if output
msg.send "```#{output}```"
msg.send "Could not find output for Rundeck job \"#{jobid}\"."
msg.send "#{msg.envelope.user}: you do not have #{rundeck.adminRole} role."
# hubot rundeck show <name>
robot.respond /(?:rd|rundeck) show ([\w -_]+)/i, (msg) ->
name = msg.match[1]
if robot.auth.hasRole(msg.envelope.user, rundeck.adminRole) name, (job) ->
if job
msg.send job.format()
msg.send "Could not find Rundeck job \"#{name}\"."
msg.send "#{msg.envelope.user}: you do not have #{rundeck.adminRole} role."
# hubot rundeck run <name>
robot.respond /(?:rd|rundeck) run ([\w -_]+)/i, (msg) ->
name = msg.match[1]
if robot.auth.hasRole(msg.envelope.user, rundeck.adminRole) name, null, (job, results) ->
if job
robot.logger.debug inspect(results, false, null)
msg.send "Running job #{name}: #{results.executions.execution[0]['$'].href}"
msg.send "Could not execute Rundeck job \"#{name}\"."
msg.send "#{msg.envelope.user}: you do not have #{rundeck.adminRole} role."
# takes all but last word as the name of our job
# hubot rundeck ad-hoc <name> <nodename>
robot.respond /(?:rd|rundeck) (?:ad[ -]?hoc) ([\w -_]+) ([\w-]+)/i, (msg) ->
name = msg.match[1]
params = { argString: "-nodename #{msg.match[2].trim().toLowerCase()}" }
query = "?#{querystring.stringify(params)}"
if robot.auth.hasRole(msg.envelope.user, rundeck.adminRole) name, query, (job, results) ->
if job
msg.send "Running job #{name}: #{results.executions.execution[0]['$'].href}"
msg.send "Could not execute Rundeck job \"#{name}\"."
msg.send "#{msg.envelope.user}: you do not have #{rundeck.adminRole} role."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment