knife-wip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2014 Daniel Schauenberg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # knife-wip
2
+
3
+ ## Overview
4
+ Chef should be the authoritative source of how your infrastructure looks like.
5
+ Ideally what's in Chef is also what's running on the servers. However
6
+ sometimes you have to go into a server, stop Chef and debug something by hand.
7
+ This has all sorts of implications - depending on the length of the work - the
8
+ server won't get updates, drifts out of sync with app configuration and worst
9
+ of all nobody might know. This is why knife-wip provides a tag based way to
10
+ track work in progress in your infrastructure. Knife-wip uses Chef [node
11
+ tags][tags] to track who is working on which servers. This way it supports all
12
+ the great ways of searching for tags and testing for them in Chef recipe and
13
+ doesn't rely on any external setup.
14
+
15
+
16
+ ## Tag format
17
+ In order to provide an easy and standardized way to retrieve information and
18
+ provide everyone with the maximum amount of information the tag for WIP looks
19
+ like this:
20
+
21
+ ```
22
+ wip:[USERNAME]:[provided description]
23
+ ```
24
+
25
+ This should make it easy to immediately see who has added the WIP tag and what
26
+ they are working on.
27
+
28
+
29
+ ## Usage
30
+
31
+ ```
32
+ # mark a node as WIP
33
+ % knife node wip web01.example.com testing php build
34
+ Created WIP "wip:dschauenberg:testing php build" for node web01.example.com.
35
+
36
+ # show all nodes that are marked as WIP
37
+ % knife wip list
38
+ 1 nodes found with work in progress
39
+
40
+ web01.example.com: dschauenberg:testing php build
41
+
42
+ # remove the WIP tag
43
+ % knife node unwip web01.example.com testing php build
44
+ Deleted WIP description "wip:dschauenberg:testing php build" for node web01.example.com.
45
+ ```
46
+
47
+ ## Plugins
48
+ knife-wip has a plugin system that makes it possible to perform custom actions
49
+ when work is started or stopped.
50
+
51
+
52
+ ### Configuration format
53
+ knife-wip reads its configuration from different files in the following order:
54
+
55
+ - `$COOKBOOKPATH/config/knife-wip-config.yml`
56
+ - `config/knife-wip-config.yml`
57
+ - `/etc/knife-wip-config.yml`
58
+ - `~/.chef/knife-wip-config.yml`
59
+
60
+ And the file should be YAML and look something like this:
61
+
62
+ ```
63
+ plugins:
64
+ irccat:
65
+ server: irccat.example.com
66
+ port: 12345
67
+ channels:
68
+ - "#chef"
69
+ ```
70
+
71
+ The key of the plugin configuration is what knife-wip uses to try and load the
72
+ corresponding ruby file under `lib/knife-wip/plugins`. So it needs to exist
73
+ there first.
74
+
75
+ ### Plugin format
76
+ A simple plugin just needs to inherit from `KnifeWip::Plugin` and implement
77
+ the two methods `wip_start` and `wip_stop`. Those methods get passed in the
78
+ user, tag and node for the command that was performed. When the plugin gets
79
+ instantiated it also gets its configuration from the config file passed in and
80
+ it's available as the `@config` instance variable in there.
81
+
82
+ ```
83
+ module KnifeWip
84
+ module Plugins
85
+ class LolFormatter < KnifeWip::Plugin
86
+
87
+ def wip_start(user, tag, node)
88
+ end
89
+
90
+ def wip_stop(user, tag, node)
91
+ end
92
+
93
+
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+
100
+
101
+ ## Installation
102
+
103
+ ```
104
+ gem install knife-wip
105
+ ```
106
+
107
+
108
+ ## Bugs
109
+ Probably. Please file a ticket [here][issues]
110
+
111
+
112
+
113
+ [tags]: https://docs.getchef.com/knife_tag.html
114
+ [issues]: https://github.com/mrtazz/knife-wip/issues
115
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
data/knife-wip.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+ require 'knife-wip'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'knife-wip'
6
+ gem.version = KnifeWip::VERSION
7
+ gem.authors = ["Daniel Schauenberg"]
8
+ gem.email = 'd@unwiredcouch.com'
9
+ gem.homepage = 'https://github.com/mrtazz/knife-wip'
10
+ gem.summary = "A workflow plugin to track WIP nodes on a chef server"
11
+ gem.description = "A workflow plugin to track WIP nodes on a chef server"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.require_paths = ["lib"]
17
+
18
+ gem.add_runtime_dependency 'chef', '>= 0.10.4'
19
+ gem.add_runtime_dependency 'app_conf', '~> 0.4', '>= 0.4.2'
20
+ end
@@ -0,0 +1,227 @@
1
+ require 'chef/knife'
2
+ require 'chef/knife/core/node_presenter'
3
+ require 'app_conf'
4
+
5
+ module KnifeWip
6
+
7
+ # Public: collection namespace for some helper functions and methods
8
+ module Helpers
9
+
10
+ # Public: load all plugins that are configured
11
+ #
12
+ # this method merely tries to require the plugin files, instantiates an
13
+ # object from them and then stores it in @plugins. That way the knife
14
+ # commands can just call all plugins however they want from there.
15
+ #
16
+ # Returns nothing
17
+ def load_plugins
18
+
19
+ @plugins ||= []
20
+
21
+ app_config[:plugins].to_hash.each do |plugin, plugin_config|
22
+ begin
23
+ require "knife-wip/plugins/#{plugin.downcase}"
24
+ # apparently this is the way to dynamically instantiate ruby objects
25
+ @plugins << KnifeWip::Plugins.const_get(plugin.capitalize).new(plugin_config)
26
+ rescue LoadError
27
+ ui.warn "Configured plugin '#{plugin}' doesn't exist."
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+
34
+ # Public: get the configuration loaded from the yaml files
35
+ #
36
+ # Returns the appconf object with config loaded
37
+ def app_config
38
+ return @app_config unless @app_config.nil?
39
+
40
+ @app_config = ::AppConf.new
41
+ load_paths = []
42
+ # first look for configuration in the cookbooks folder
43
+ load_paths << File.expand_path("#{cookbook_path.gsub('cookbooks','')}/config/knife-wip-config.yml")
44
+ # or see if we are in the cookbooks repo
45
+ load_paths << File.expand_path('config/knife-wip-config.yml')
46
+ # global config in /etc has higher priority
47
+ load_paths << '/etc/knife-wip-config.yml'
48
+ # finally the user supplied config file is loaded
49
+ load_paths << File.expand_path('~/.chef/knife-wip-config.yml')
50
+
51
+ # load all the paths
52
+ load_paths.each do |load_path|
53
+ if File.exists?(load_path)
54
+ @app_config.load(load_path)
55
+ end
56
+ end
57
+
58
+ @app_config
59
+ end
60
+
61
+ def ensure_cookbook_path!
62
+ if config[:cookbook_path].nil?
63
+ ui.fatal "No default cookbook_path; Specify with -o or fix your knife.rb."
64
+ show_usage
65
+ exit(1)
66
+ end
67
+ end
68
+
69
+ def cookbook_path
70
+ ensure_cookbook_path!
71
+ [config[:cookbook_path] ||= ::Chef::Config.cookbook_path].flatten[0]
72
+ end
73
+
74
+ end
75
+
76
+ # Public: class to inherit from for plugins. Basically a cheap hack to get
77
+ # some control over whether or not a plugin implements the correct methods
78
+ class Plugin
79
+ include ::KnifeWip::Helpers
80
+
81
+ def initialize(config)
82
+ @config = config
83
+ end
84
+
85
+ def wip_start(user, tag, node)
86
+ ui.warn "Warning: #{self.class} doesn't implement the 'wip_start' method."
87
+ ui.warn "No announcements performed for #{self.class}"
88
+ end
89
+
90
+ def wip_stop(user, tag, node)
91
+ ui.warn "Warning: #{self.class} doesn't implement the 'wip_stop' method."
92
+ ui.warn "No announcements performed for #{self.class}"
93
+ end
94
+
95
+ end
96
+
97
+
98
+ class NodeWip < Chef::Knife
99
+ include KnifeWip::Helpers
100
+
101
+ deps do
102
+ require 'chef/node'
103
+ end
104
+
105
+ banner "knife node wip NODE DESCRIPTION"
106
+
107
+ def run
108
+ name = @name_args[0]
109
+ description = @name_args[1..-1]
110
+
111
+ if name.nil? || description.nil? || description.empty?
112
+ show_usage
113
+ ui.fatal("You must specify a node name and a description of what you are working on.")
114
+ exit 1
115
+ end
116
+
117
+ self.config = Chef::Config.merge!(config)
118
+
119
+ load_plugins
120
+
121
+ wip_tag = "wip:#{ENV["USER"]}:#{description.join(" ")}"
122
+
123
+ node = Chef::Node.load name
124
+ (node.tags << wip_tag).uniq!
125
+ node.save
126
+ ui.info("Created WIP \"#{wip_tag}\" for node #{name}.")
127
+ @plugins.each do |plugin|
128
+ plugin.wip_start(ENV["USER"], description.join(" "), name)
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ class NodeUnwip < Chef::Knife
135
+ include KnifeWip::Helpers
136
+
137
+ deps do
138
+ require 'chef/node'
139
+ end
140
+
141
+ banner "knife node unwip NODE DESCRIPTION"
142
+
143
+ def run
144
+ name = @name_args[0]
145
+ description = @name_args[1..-1].join(" ")
146
+
147
+ if name.nil? || description.nil? || description.empty?
148
+ show_usage
149
+ ui.fatal("You must specify a node name and a WIP description.")
150
+ exit 1
151
+ end
152
+
153
+ self.config = Chef::Config.merge!(config)
154
+
155
+ load_plugins
156
+
157
+ node = Chef::Node.load name
158
+ tag = "wip:#{ENV["USER"]}:#{description}"
159
+ success = node.tags.delete(tag).nil? ? false : true
160
+
161
+ node.save
162
+ if success == false
163
+ message = "Nothing has changed. The WIP description requested to be deleted does not exist."
164
+ else
165
+ message = "Deleted WIP description \"#{tag}\" for node #{name}."
166
+ @plugins.each do |plugin|
167
+ plugin.wip_stop(ENV["USER"], description, name)
168
+ end
169
+ end
170
+ ui.info(message)
171
+ end
172
+
173
+ end
174
+
175
+ class WipList < Chef::Knife
176
+
177
+ deps do
178
+ require 'chef/node'
179
+ require 'chef/environment'
180
+ require 'chef/api_client'
181
+ require 'chef/search/query'
182
+ end
183
+
184
+ include Chef::Knife::Core::NodeFormattingOptions
185
+
186
+ banner "knife wip list"
187
+
188
+ def run
189
+ q = Chef::Search::Query.new
190
+
191
+ query = URI.escape("tags:wip*", Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
192
+
193
+ result_items = []
194
+ result_count = 0
195
+
196
+ begin
197
+ q.search('node', query) do |item|
198
+ formatted_item = format_for_display(item)
199
+ result_items << formatted_item
200
+ result_count += 1
201
+ end
202
+ rescue Net::HTTPServerException => e
203
+ msg = Chef::JSONCompat.from_json(e.response.body)["error"].first
204
+ ui.error("knife wip list failed: #{msg}")
205
+ exit 1
206
+ end
207
+
208
+ if ui.interchange?
209
+ output({:results => result_count, :rows => result_items})
210
+ else
211
+ ui.msg "#{result_count} nodes found with work in progress"
212
+ ui.msg("\n")
213
+ result_items.each do |item|
214
+ normalized_tags = []
215
+ item.tags.each do |tag|
216
+ next unless tag.start_with? "wip:"
217
+ tag = tag.split(":")
218
+ normalized_tags << tag[1, tag.length].join(":")
219
+ end
220
+ output("#{item.name}: #{normalized_tags.join(", ")}")
221
+ end
222
+ end
223
+ end
224
+
225
+ end
226
+
227
+ end
data/lib/knife-wip.rb ADDED
@@ -0,0 +1,3 @@
1
+ module KnifeWip
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,62 @@
1
+ require 'chef/knife'
2
+
3
+ module KnifeWip
4
+ module Plugins
5
+ class Eventinator < KnifeWip::Plugin
6
+
7
+ def wip_start(user, tag, node)
8
+ event_data = {
9
+ :tag => 'knife',
10
+ :username => user,
11
+ :status => "#{user} started work '#{tag}' on #{node}",
12
+ :metadata => {
13
+ :node_name => node,
14
+ :tag => tag
15
+ }.to_json
16
+ }
17
+ eventinate(event_data)
18
+ end
19
+
20
+ def wip_stop(user, tag, node)
21
+ event_data = {
22
+ :tag => 'knife',
23
+ :username => user,
24
+ :status => "#{user} stopped work '#{tag}' on #{node}",
25
+ :metadata => {
26
+ :node_name => node,
27
+ :tag => tag
28
+ }.to_json
29
+ }
30
+ eventinate(event_data)
31
+ end
32
+
33
+ private
34
+ def eventinate(event_data)
35
+ begin
36
+ uri = URI.parse(@config["url"])
37
+ rescue Exception => e
38
+ ui.error 'Could not parse URI for Eventinator.'
39
+ ui.error e.to_s
40
+ return
41
+ end
42
+
43
+ http = Net::HTTP.new(uri.host, uri.port)
44
+ http.read_timeout = @config["read_timeout"] || 5
45
+
46
+ request = Net::HTTP::Post.new(uri.request_uri)
47
+ request.set_form_data(event_data)
48
+
49
+ begin
50
+ response = http.request(request)
51
+ ui.error "Eventinator at #{@config["url"]} did not receive a good response from the server" if response.code != '200'
52
+ rescue Timeout::Error
53
+ ui.error "Eventinator timed out connecting to #{@config["url"]}. Is that URL accessible?"
54
+ rescue Exception => e
55
+ ui.error 'Eventinator error.'
56
+ ui.error e.to_s
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,44 @@
1
+ module KnifeWip
2
+ module Plugins
3
+ class Irccat < KnifeWip::Plugin
4
+
5
+ TEMPLATES = {
6
+ :start => '#BOLD#PURPLECHEF:#NORMAL %{user} started work #TEAL%{tag}#NORMAL on #RED%{node}#NORMAL',
7
+ :stop => '#BOLD#PURPLECHEF:#NORMAL %{user} stopped work #TEAL%{tag}#NORMAL on #RED%{node}#NORMAL'
8
+ }
9
+
10
+ def wip_start(user, tag, node)
11
+ send_to_irccat(TEMPLATES[:start] % {
12
+ :user => user,
13
+ :tag => tag,
14
+ :node => node,
15
+ })
16
+ end
17
+
18
+ def wip_stop(user, tag, node)
19
+ send_to_irccat(TEMPLATES[:stop] % {
20
+ :user => user,
21
+ :tag => tag,
22
+ :node => node,
23
+ })
24
+ end
25
+
26
+ private
27
+ def send_to_irccat(message)
28
+ @config["channels"].each do |channel|
29
+ begin
30
+ # Write the message using a TCP Socket
31
+ socket = TCPSocket.open(@config["server"], @config["port"])
32
+ socket.write("#{channel} #{message}")
33
+ rescue Exception => e
34
+ ui.error 'Failed to post message with irccat.'
35
+ ui.error e.to_s
36
+ ensure
37
+ socket.close unless socket.nil?
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-wip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Schauenberg
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-10-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: chef
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.10.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.10.4
30
+ - !ruby/object:Gem::Dependency
31
+ name: app_conf
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.4'
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.4.2
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: '0.4'
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: 0.4.2
52
+ description: A workflow plugin to track WIP nodes on a chef server
53
+ email: d@unwiredcouch.com
54
+ executables: []
55
+ extensions: []
56
+ extra_rdoc_files: []
57
+ files:
58
+ - Gemfile
59
+ - LICENSE
60
+ - README.md
61
+ - Rakefile
62
+ - knife-wip.gemspec
63
+ - lib/chef/knife/knife-wip.rb
64
+ - lib/knife-wip.rb
65
+ - lib/knife-wip/plugins/eventinator.rb
66
+ - lib/knife-wip/plugins/irccat.rb
67
+ homepage: https://github.com/mrtazz/knife-wip
68
+ licenses: []
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.23.2
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: A workflow plugin to track WIP nodes on a chef server
91
+ test_files: []