rundeck-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c0b2b8b341f0873b0ca2a5f5d38f75dc2ba99d4e
4
+ data.tar.gz: c03843110188e81e43939b5fe4b737c17c98b095
5
+ SHA512:
6
+ metadata.gz: 92f5845c06eb9fde084af309ce89ca4a7aee5386c2f09848e86afa72df74e7ca7a26505c77a5651b9b3a107281bfe6bc9a91713e6da809fccd2fa758f39efaa9
7
+ data.tar.gz: 789262397ce473570fca1f4821bbcfe581a83295d403771ec3bc432e9573a5bcdb2d00b69c7c0d6a8a3bec879f2962cbd98f16597998912a0a1dfdb900762a81
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rundeck-ruby.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jon Phillips
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,183 @@
1
+ # Rundeck-ruby
2
+
3
+ Like the name says, these are ruby bindings for the rundeck API
4
+
5
+ ## Installation
6
+
7
+ The usual stuff:
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'rundeck-ruby'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install rundeck-ruby
20
+
21
+ ## Getting started
22
+
23
+ So, you're not going to be using your username and password in this
24
+ library. Instead it uses rundeck's token-based authentication for
25
+ everything. See [the API docs](http://rundeck.org/docs/api/#token-authentication) for
26
+ instructions about generating a token.
27
+
28
+ ## Command-line usage
29
+
30
+ This gem installs a binstub named `rundeck`.
31
+ Surprising, huh? Anyway, `rundeck` does one thing: execute jobs. Oh, you
32
+ want more? Well then, send a pull request, buddy.
33
+
34
+ The USAGE line:
35
+ ```
36
+ rundeck exec <url> <token> <job guid> [<exec args>]
37
+ ```
38
+
39
+ For example
40
+ ```
41
+ $ rundeck exec https://my.server afdDSFasdfASD4334fasdfaasWERsW23423
42
+ cd51b400-aad2-0131-c7f8-0438353e293e -arg0 1234 - arg1 blah
43
+ https://my.server/executions/1234
44
+ ```
45
+
46
+ That will connect to your rundeck server `my.server` using your auth
47
+ token, find job cd51b400-aad2-0131-c7f8-0438353e293e and execute it. It
48
+ prints out the url of the resulting execution.
49
+
50
+ "But that's a lot of parameters," you say? Well, if you've got a better idea, submit a pull request. Also, you're probably going to `alias` common executions anyway, so it's really not too bad.
51
+
52
+ ## Library usage
53
+
54
+ ### Connecting
55
+
56
+ Connections to your rundeck server are handled with the `Session` class.
57
+ Like so:
58
+ ```
59
+ require 'rundeck-ruby'
60
+ session = Rundeck::Session('https://my.server', 'my token')
61
+ ```
62
+ That's it. You have a session.
63
+
64
+ From there you can get a hash of some of the server's system information:
65
+ ```
66
+ info_hash = session.system_info
67
+ ```
68
+
69
+ ### Listing Projects
70
+ The other thing the session lets you do get an array of your projects:
71
+ ```
72
+ names = session.projects.map(&:name)
73
+ ```
74
+
75
+ To get a single project, any of these will work:
76
+ ```
77
+ project = session.projects.find{|p| p.name == "ReallyImportantProject"}
78
+ project = session.project("ReallyImportantProject")
79
+ project = Rundeck::Project.find(session, "ReallyImportantProject")
80
+ ```
81
+
82
+ ### Listing Jobs
83
+ From each project, you can get a list of jobs. This code:
84
+ ```
85
+ session.projects.first.jobs.map(&:name)
86
+ ```
87
+ will give you a list of jobs in the project.
88
+
89
+ You can get a single job object from the project in any of these ways:
90
+ ```
91
+ job = project.jobs.find{|j| j.name == "MyJob"} # When you only have the name
92
+ job = project.job_by_id(the_job_guid)
93
+ ```
94
+
95
+ You can skip right over the project and go straight to the job too:
96
+ ```
97
+ job = Rundeck::Job.find(session, the_job_guid)
98
+ ```
99
+
100
+ ### Executing a job
101
+ Once you have a job object, you can work with its executions. To execute
102
+ the job, do this:
103
+ ```
104
+ execution = job.execute!("some fancy job arguments")
105
+ ```
106
+
107
+ ### Finding executions
108
+
109
+ To get all executions from a job, do this:
110
+ ```
111
+ executions = job.executions
112
+ ```
113
+
114
+ If your rundeck is like mine, then there will be a boatload of executions in
115
+ each job, so getting all of them will be a pain. Unnecessary, too. To
116
+ filter down the results, do this:
117
+ ```
118
+ active_executions = job.executions do |query|
119
+ query.status = :failed
120
+ query.max = 2
121
+ query.offset = 100
122
+ end
123
+ ```
124
+ It does what it looks like.
125
+
126
+ ...or, for the whole project:
127
+ ```
128
+ active_executions = Rundeck::Execution.where(project) do |query|
129
+ query.status = :failed
130
+ query.max = 2
131
+ query.offset = 100
132
+ end
133
+ ```
134
+
135
+ To get the valid statuses, ask the query object:
136
+ ```
137
+ statuses = []
138
+ active_executions = job.executions do |query|
139
+ statuses = query.class.valid_statuses
140
+ end
141
+ puts statuses
142
+ ```
143
+ Spoiler: they're one of the following: `succeeded`, `failed`, `aborted`, `running`, or `nil`.
144
+
145
+ ### Execution output
146
+ To get the output from an execution, ask it:
147
+ ```
148
+ output = execution.output
149
+ ```
150
+
151
+ `output` will be a hash containing the following keys: `id`,
152
+ `completed`, `hasFailedNodes`, `log`. `log` will, in turn, be a hash of
153
+ hostnames to log entries, which will be self-explanatory
154
+
155
+ ## Contributing
156
+
157
+ The usual boilerplate:
158
+
159
+ 1. Fork it
160
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
161
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
162
+ 4. Push to the branch (`git push origin my-new-feature`)
163
+ 5. Create new Pull Request
164
+
165
+ ## Wishlist
166
+
167
+ If you want to contribute, here's what I'd like to do sooner rather than
168
+ later:
169
+
170
+ * Add execution tailing to the `rundeck` binstub, à la:
171
+
172
+ ```
173
+ rundeck tail <execution url> <token>
174
+ ```
175
+
176
+ ...or...
177
+
178
+ ```
179
+ rundeck exec -tail ...the other parameters...
180
+ ```
181
+
182
+ * Running ad-hoc commands, both in irb and with the binstub
183
+
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rundeck-ruby'
4
+ action, url, key, job, *exec_args = ARGV
5
+
6
+ abort "I can only execute. Other stuff may be added later." unless action.downcase == "exec"
7
+
8
+ sess = Rundeck::Session.new(url, key)
9
+ j = Rundeck::Job.find(sess, job)
10
+ encoded_args = URI::encode(exec_args.join(' '))
11
+ arg_query = encoded_args && "argString=#{encoded_args}"
12
+ puts j.execute!(arg_query).url
@@ -0,0 +1,6 @@
1
+ require "rundeck-ruby/maybe"
2
+ require "rundeck-ruby/version"
3
+ require "rundeck-ruby/session"
4
+ require "rundeck-ruby/project"
5
+ require "rundeck-ruby/job"
6
+ require "rundeck-ruby/execution"
@@ -0,0 +1,83 @@
1
+ module Rundeck
2
+ class Execution
3
+ def self.from_hash(session, hash)
4
+ job = Job.from_hash(session, hash['job'])
5
+ new(session, hash, job)
6
+ end
7
+
8
+ def initialize(session, hash, job)
9
+ @id = hash['id']
10
+ @url=hash['href']
11
+ @url = URI.join(session.server, URI.split(@url)[5]).to_s if @url # They always return a url of "localhost" for their executions. Replace it with the real URL
12
+ @status=hash['status'].to_sym
13
+ @date_started = hash['date_started']
14
+ @date_ended = hash['date_ended']
15
+ @user = hash['user']
16
+ @args = (hash['argstring'] || "").split
17
+ .each_slice(2)
18
+ .reduce({}){|acc,cur| acc[cur[0]] = cur[1]; acc}
19
+ @job = job
20
+ @session = session
21
+ end
22
+ attr_reader :id, :url, :status, :date_started, :date_ended, :user, :args, :job, :session
23
+
24
+ def self.find(session, id)
25
+ result = session.get("api/1/execution/#{id}", *%w(result executions execution))
26
+ return nil unless result
27
+ job = Job.find(session, result['job']['id'])
28
+ return nil unless job
29
+ Execution.new(session, result, job)
30
+ end
31
+
32
+ def self.where(project)
33
+ qb = QueryBuilder.new
34
+ yield qb if block_given?
35
+
36
+ endpoint = "api/5/executions?project=#{project.name}#{qb.query}"
37
+ pp endpoint
38
+ results = project.session.get(endpoint, 'result', 'executions', 'execution') || []
39
+ results = [results] if results.is_a?(Hash) #Work around an inconsistency in the API
40
+ results.map {|hash| from_hash(project.session, hash)}
41
+ end
42
+
43
+ def output
44
+ ret = session.get("api/9/execution/#{id}/output")
45
+ result = ret['result']
46
+ raise "API call not successful" unless result && result['success']=='true'
47
+
48
+ #sort the output by node
49
+ ret = result['output'].slice(*%w(id completed hasFailedNodes))
50
+ ret['log'] = result['output']['entries']['entry'].group_by{|e| e['node']}
51
+ ret
52
+ end
53
+
54
+ class QueryBuilder
55
+ attr_accessor :status, :max, :offset
56
+
57
+ def self.valid_statuses
58
+ %w(succeeded failed aborted running) << nil
59
+ end
60
+
61
+ def validate
62
+ raise "Invalid requested status: #{status}" unless status.nil? || elf.class.valid_statuses.include?(status.to_s)
63
+ raise "Invalid offset: #{offset}" unless offset.nil? || offset.to_i >= 0
64
+ raise "Invalid max: #{max}" unless max.nil? || max.to_i >= 0
65
+ end
66
+
67
+ def query
68
+ validate
69
+
70
+ [
71
+ "",
72
+ status && "statusFilter=#{status}",
73
+ max && "max=#{max.to_i}",
74
+ offset && "offset=#{offset.to_i}",
75
+ ].compact
76
+ .join("&")
77
+ .chomp("&")
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
@@ -0,0 +1,68 @@
1
+ module Rundeck
2
+ class Job
3
+
4
+ def self.find(session, id)
5
+ result = session.get("api/1/job/#{id}", 'joblist', 'job')
6
+ return nil unless result
7
+ project = Project.find(session, result['context']['project'])
8
+ return nil unless project
9
+ Job.new(session, project, result['id'], result['name'])
10
+ end
11
+
12
+ def self.from_hash(session, hash)
13
+ project = Project.find(session, hash['project'])
14
+ new(session, project, hash['id'], hash['name'])
15
+ end
16
+
17
+ def initialize(session, project, id, name)
18
+ @session = session
19
+ @project = project
20
+ @id = id
21
+ @name = name
22
+ end
23
+
24
+ attr_reader :id, :name, :project, :session
25
+
26
+ def executions
27
+ qb = JobExecutionQueryBuilder.new
28
+ yield qb if block_given?
29
+
30
+ endpoint = "api/1/job/#{id}/executions#{qb.query}"
31
+ results = session.get(endpoint, 'result', 'executions', 'execution') || []
32
+ results = [results] if results.is_a?(Hash) #Work around an inconsistency in the API
33
+ results.map{|hash| Execution.from_hash(session, hash)}
34
+ end
35
+
36
+ def execute!(query_string = '')
37
+ query = "api/1/job/#{id}/run?#{query_string}".chomp("?")
38
+ hash = session.get(query, 'result', 'executions', 'execution') || {}
39
+ Execution.new(session, hash, self)
40
+ end
41
+
42
+ class JobExecutionQueryBuilder
43
+ attr_accessor :status, :max, :offset
44
+
45
+ def self.valid_statuses
46
+ Execution::QueryBuilder.valid_statuses
47
+ end
48
+
49
+ def validate
50
+ raise "Invalid requested status: #{status}" unless status.nil? || self.class.valid_statuses.include?(status.to_s)
51
+ raise "Invalid offset" unless offset.nil? || offset.to_i >= 0
52
+ raise "Invalid max" unless max.nil? || max.to_i >= 0
53
+ end
54
+
55
+ def query
56
+ validate
57
+
58
+ clauses = [
59
+ status && "status=#{status}",
60
+ max && "max=#{max.to_i}",
61
+ offset && "offset=#{offset.to_i}",
62
+ ].compact.join('&')
63
+
64
+ "?#{clauses}".chomp('?')
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,5 @@
1
+ require 'naught'
2
+
3
+ NullObject = Naught.build
4
+
5
+ include NullObject::Conversions
@@ -0,0 +1,30 @@
1
+ module Rundeck
2
+ class Project
3
+ def self.all(session)
4
+ @all ||= begin
5
+ result = session.get('api/3/projects', 'result', 'projects', 'project') || []
6
+ result.map{|hash| Project.new(session, hash)}
7
+ end
8
+ end
9
+
10
+ def self.find(session, name)
11
+ all(session).first{|p| p.name == name}
12
+ end
13
+
14
+ def initialize(session, hash)
15
+ @session = session
16
+ @name = hash['name']
17
+ end
18
+ attr_reader :session, :name
19
+
20
+ def jobs(force_reload = false)
21
+ return @jobs unless @jobs.nil? || force_reload
22
+ result = session.get("api/2/jobs?project=#{name}", 'result', 'jobs', 'job') || []
23
+ @jobs = result.map{|hash| Job.from_hash(session, hash)}
24
+ end
25
+
26
+ def job(id)
27
+ jobs.first{|j| j.id == id}
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+ require 'active_support/all'
3
+ require 'rest_client'
4
+
5
+
6
+ module Rundeck
7
+ class Session
8
+ def initialize(server, token)
9
+ @server = server
10
+ @token = token
11
+ end
12
+
13
+ attr_reader :server, :token
14
+
15
+ def get(url, *keys)
16
+ endpoint = File.join(server, url)
17
+ xml = RestClient.get(endpoint, 'X-Rundeck-Auth-Token'=> token)
18
+ hash = Maybe(Hash.from_xml(xml))
19
+ keys.reduce(hash){|acc, cur| acc && acc[cur]}
20
+ end
21
+
22
+ def system_info
23
+ get('api/1/system/info', 'result', 'system')
24
+ end
25
+
26
+ def projects
27
+ Project.all(self)
28
+ end
29
+
30
+ def project(name)
31
+ Project.find(self, name)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Rundeck
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rundeck-ruby/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rundeck-ruby"
8
+ spec.version = Rundeck::VERSION
9
+ spec.authors = ["Jon Phillips"]
10
+ spec.email = ["jphillips@biaprotect.com"]
11
+ spec.description = %q{Ruby client for Rundeck API}
12
+ spec.summary = %q{For talking to Rundeck}
13
+ spec.homepage = "https://github.com/jonp/rundeck-ruby"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+ spec.bindir = 'exe'
21
+ spec.executables = `git ls-files -- exe/*`.split("\n").map{ |f| File.basename(f) }
22
+ spec.add_runtime_dependency "rest-client", "~> 1.6"
23
+ spec.add_runtime_dependency "json", "~> 1.8"
24
+ spec.add_runtime_dependency "activesupport", "~> 3.0"
25
+ spec.add_runtime_dependency "i18n", "~> 0.6"
26
+ spec.add_runtime_dependency "naught", "~> 1.0"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.3"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rundeck-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jon Phillips
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2014-04-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rest-client
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: '1.6'
25
+ prerelease: false
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.8'
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '1.8'
39
+ prerelease: false
40
+ type: :runtime
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ~>
51
+ - !ruby/object:Gem::Version
52
+ version: '3.0'
53
+ prerelease: false
54
+ type: :runtime
55
+ - !ruby/object:Gem::Dependency
56
+ name: i18n
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.6'
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: '0.6'
67
+ prerelease: false
68
+ type: :runtime
69
+ - !ruby/object:Gem::Dependency
70
+ name: naught
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: '1.0'
81
+ prerelease: false
82
+ type: :runtime
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: '1.3'
95
+ prerelease: false
96
+ type: :development
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ version: '10.0'
109
+ prerelease: false
110
+ type: :development
111
+ description: Ruby client for Rundeck API
112
+ email:
113
+ - jphillips@biaprotect.com
114
+ executables:
115
+ - rundeck
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - .gitignore
120
+ - Gemfile
121
+ - LICENSE.txt
122
+ - README.md
123
+ - Rakefile
124
+ - exe/rundeck
125
+ - lib/rundeck-ruby.rb
126
+ - lib/rundeck-ruby/execution.rb
127
+ - lib/rundeck-ruby/job.rb
128
+ - lib/rundeck-ruby/maybe.rb
129
+ - lib/rundeck-ruby/project.rb
130
+ - lib/rundeck-ruby/session.rb
131
+ - lib/rundeck-ruby/version.rb
132
+ - rundeck-ruby.gemspec
133
+ homepage: https://github.com/jonp/rundeck-ruby
134
+ licenses:
135
+ - MIT
136
+ metadata: {}
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - '>='
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubyforge_project:
153
+ rubygems_version: 2.2.2
154
+ signing_key:
155
+ specification_version: 4
156
+ summary: For talking to Rundeck
157
+ test_files: []