Skip to content

Instantly share code, notes, and snippets.

Embed
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:
# 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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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