Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@moutons
Forked from lusis/README.md
Created September 16, 2016 02:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moutons/52c836b1471ff353db8c2c27bca9a9cf to your computer and use it in GitHub Desktop.
Save moutons/52c836b1471ff353db8c2c27bca9a9cf 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}\"."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment