knife-wip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []