pocketknife 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
@@ -0,0 +1,14 @@
1
+ Changes
2
+ =======
3
+
4
+ * 0.2.0
5
+ * [!] Changed default transfer mechanism to `rsync` from `tar` in past versions.
6
+ * Added transfer mechanisms, allowing the choice of `rsync` and `tar` for uploading files to nodes. The new `rsync` option deals gracefully with symlinks in your sources and is faster for making small changes. See "Transfer mechanisms" section in the README for details. Suggested by Roland Moriz.
7
+ * Added data bags support, contributed by Richard Livsey.
8
+ * Added ability to override the runlist. See "Override runlist" in the README for details.
9
+ * Added shellwords to properly escape strings in commands.
10
+ * Added clear success message and timer, suggested by Trip Leonard.
11
+ * Added clearer error message when `nodes` directory is missing.
12
+
13
+ * 0.1.0
14
+ * First release
data/Gemfile CHANGED
@@ -1,13 +1,28 @@
1
- source "http://rubygems.org"
1
+ source 'http://rubygems.org'
2
2
 
3
- gem "archive-tar-minitar", "~> 0.5.0"
4
- gem "rye", "~> 0.9.0"
3
+ gem 'archive-tar-minitar', '~> 0.5.0'
4
+ gem 'rye', '~> 0.9.0'
5
5
 
6
6
  group :development do
7
- gem "bluecloth", "~> 2.1.0"
8
- gem "rspec", "~> 2.3.0"
9
- gem "yard", "~> 0.6.0"
10
- gem "bundler", "~> 1.0.0"
11
- gem "jeweler", "~> 1.6.0"
12
- gem "rcov", ">= 0"
7
+ gem 'rake'
8
+
9
+ gem 'bluecloth', '~> 2.2.0'
10
+ gem 'rspec', '~> 2.10.0'
11
+ gem 'yard', '~> 0.8.0'
12
+ gem 'jeweler', '~> 1.8.0'
13
+
14
+ # OPTIONAL LIBRARIES: These libraries upset travis-ci and may cause Ruby or
15
+ # RVM to hang, so only use them when needed.
16
+ if ENV['DEBUGGER']
17
+ platform :mri_18 do
18
+ gem 'rcov', :require => false
19
+ gem 'ruby-debug'
20
+ end
21
+
22
+ platform :mri_19 do
23
+ gem 'simplecov', :require => false
24
+ gem 'debugger-ruby_core_source'
25
+ gem 'debugger'
26
+ end
27
+ end
13
28
  end
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://secure.travis-ci.org/igal/pocketknife.png)](http://travis-ci.org/igal/pocketknife)
2
+
1
3
  pocketknife
2
4
  ===========
3
5
 
@@ -38,24 +40,6 @@ Go into your new *project* directory:
38
40
 
39
41
  Create cookbooks in the `cookbooks` directory that describe how your computers should be configured. These are standard `chef` cookbooks, like the [opscode/cookbooks](https://github.com/opscode/cookbooks). For example, download a copy of [opscode/cookbooks/ntp](https://github.com/opscode/cookbooks/tree/master/ntp) as `cookbooks/ntp`.
40
42
 
41
- Override cookbooks in the `site-cookbooks` directory. This has the same structure as `cookbooks`, but any files you put here will override the contents of `cookbooks`. This is useful for storing the original code of a third-party cookbook in `cookbooks` and putting your customizations in `site-cookbooks`.
42
-
43
- Optionally define roles in the `roles` directory that describe common behavior and attributes of your computers using JSON syntax using [chef's documentation](http://wiki.opscode.com/display/chef/Roles#Roles-AsJSON). For example, define a role called `ntp_client` by creating a file called `roles/ntp_client.json` with this content:
44
-
45
- {
46
- "name": "ntp_client",
47
- "chef_type": "role",
48
- "json_class": "Chef::Role",
49
- "run_list": [
50
- "recipe[ntp]"
51
- ],
52
- "override_attributes": {
53
- "ntp": {
54
- "servers": ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"]
55
- }
56
- }
57
- }
58
-
59
43
  Define a new node using the `chef` JSON syntax for [runlist](http://wiki.opscode.com/display/chef/Setting+the+run_list+in+JSON+during+run+time) and [attributes](http://wiki.opscode.com/display/chef/Attributes). For example, to define a node with the hostname `henrietta.swa.gov.it` create the `nodes/henrietta.swa.gov.it.json` file, and add the contents below so it uses the `ntp_client` role and overrides its attributes to use a local NTP server:
60
44
 
61
45
  {
@@ -79,6 +63,52 @@ When deploying a configuration to a node, `pocketknife` will check whether Chef
79
63
 
80
64
  To always install Chef and its dependencies when they're needed, without prompts, use the `-i` option, e.g. `pocketknife -i henrietta`. Or to never install Chef and its dependencies, use the `-I` option, which will cause the program to quit with an error rather than prompting if Chef or its dependencies aren't installed.
81
65
 
66
+ Override runlist
67
+ ----------------
68
+
69
+ Specify the runlist by using the `-r` option, which will override the one specified in the node, e.g.:
70
+
71
+ pocketknife -r mycookbook henrietta
72
+
73
+ Transfer mechanisms
74
+ -------------------
75
+
76
+ Files can be uploaded to nodes using different transfer mechanisms:
77
+
78
+ * `tar` - Uses one connection for execution of commands and uploads, and sends a tarball that's then extracted. Pros: Pure Ruby, reuses connection. Cons: Can't cope with symlinks, inefficient for sending a small change.
79
+ * `rsync` - Uses one connection for execution of commands and then runs the `rsync` command to upload files. Pros: Handles symlinks, and is efficient for sending a small change. Cons: Requires `rsync` command, rather than being pure Ruby, and doesn't reuse the connection.
80
+
81
+ You can specify the transfer mechanism with the `-t` option and the name of the mechanism, e.g.:
82
+
83
+ pocketknife -t rsync henrietta
84
+
85
+ Override cookbooks
86
+ ------------------
87
+
88
+ Override cookbooks in the `site-cookbooks` directory. This has the same structure as `cookbooks`, but any files you put here will override the contents of `cookbooks`. This is useful for storing the original code of a third-party cookbook in `cookbooks` and putting your customizations in `site-cookbooks`.
89
+
90
+ Roles
91
+ -----
92
+
93
+ Optionally define roles in the `roles` directory that describe common behavior and attributes of your computers using JSON syntax using [chef's documentation](http://wiki.opscode.com/display/chef/Roles#Roles-AsJSON). For example, define a role called `ntp_client` by creating a file called `roles/ntp_client.json` with this content:
94
+
95
+ {
96
+ "name": "ntp_client",
97
+ "chef_type": "role",
98
+ "json_class": "Chef::Role",
99
+ "run_list": [
100
+ "recipe[ntp]"
101
+ ],
102
+ "override_attributes": {
103
+ "ntp": {
104
+ "servers": ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org"]
105
+ }
106
+ }
107
+ }
108
+
109
+ Debugging
110
+ ---------
111
+
82
112
  If something goes wrong while deploying the configuration, you can display verbose logging from `pocketknife` and Chef by using the `-v` option. For example, deploy the configuration to `henrietta` with verbose logging:
83
113
 
84
114
  pocketknife -v henrietta
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ task :default => :spec
4
+
3
5
  require 'rubygems'
4
6
  require 'bundler'
5
7
  begin
@@ -39,6 +41,7 @@ With pocketknife, all of your cookbooks, roles and nodes are stored in easy-to-u
39
41
  Gemfile
40
42
  LICENSE.txt
41
43
  README.md
44
+ CHANGES.md
42
45
  Rakefile
43
46
  lib/*
44
47
  spec/*
@@ -47,18 +50,55 @@ With pocketknife, all of your cookbooks, roles and nodes are stored in easy-to-u
47
50
  end
48
51
  Jeweler::RubygemsDotOrgTasks.new
49
52
 
53
+ desc "Run coverage report using simplecov."
54
+ task :simplecov do
55
+ ENV['SIMPLECOV'] = 'true'
56
+ Rake::Task['spec'].invoke
57
+ end
58
+
50
59
  require 'rspec/core'
51
60
  require 'rspec/core/rake_task'
52
61
  RSpec::Core::RakeTask.new(:spec) do |spec|
53
62
  spec.pattern = FileList['spec/**/*_spec.rb']
54
63
  end
55
64
 
56
- RSpec::Core::RakeTask.new(:rcov) do |spec|
57
- spec.pattern = 'spec/**/*_spec.rb'
58
- spec.rcov = true
65
+ def rcov(options=[])
66
+ # None of the official ways to invoke Rcov work right now. Sigh.
67
+ cmd = "rcov --exclude osx\/objc,gems\/,spec\/,features\/,lib/shellwords.rb,lib/pocketknife/version.rb #{[options].flatten} $(which rspec) spec/*_spec.rb 2>&1"
68
+ puts cmd
69
+ output = `#{cmd}`
70
+ puts output
71
+ return output
59
72
  end
60
73
 
61
- task :default => :spec
74
+ RCOV_DATA = 'coverage/rcov.data'
75
+ RCOV_LOG = 'coverage/rcov.txt'
76
+
77
+ namespace :rcov do
78
+ desc "Save rcov information for use with rcov:diff"
79
+ task :save do
80
+ rcov "--save=#{RCOV_DATA}"
81
+ end
82
+
83
+ desc "Generate report of what code changed since last rcov:save"
84
+ task :diff do
85
+ output = rcov "--no-color --text-coverage-diff=#{RCOV_DATA}"
86
+ File.open(RCOV_LOG, 'w+') do |h|
87
+ h.write output
88
+ end
89
+ puts "\nSaved coverage report to: #{RCOV_LOG}"
90
+ end
91
+ end
92
+
93
+ desc "Run all specs with rcov"
94
+ task :rcov do
95
+ rcov
96
+ end
62
97
 
63
98
  require 'yard'
64
99
  YARD::Rake::YardocTask.new
100
+
101
+ desc "List undocumented code"
102
+ task :undoc do
103
+ system "yardoc --list-undoc | grep -v 'Unrecognized/invalid option'"
104
+ end
@@ -2,6 +2,20 @@
2
2
  require "pathname"
3
3
  require "fileutils"
4
4
 
5
+ begin
6
+ require "shellwords"
7
+ rescue LoadError
8
+ require "#{File.dirname(__FILE__)}/shellwords"
9
+ end
10
+
11
+ # @!visibility private
12
+ class Pathname
13
+ # @!visibility private
14
+ def shellescape
15
+ self.to_s.shellescape
16
+ end
17
+ end
18
+
5
19
  # Gem libraries
6
20
  require "archive/tar/minitar"
7
21
  require "rye"
@@ -43,8 +57,11 @@ class Pocketknife
43
57
  # Pocketknife.cli('-h')
44
58
  #
45
59
  # @param [Array<String>] args A list of arguments from the command-line, which may include options (e.g. <tt>-h</tt>).
60
+ # @return [void]
61
+ # @raise [SystemExit] Something catastrophic happened, e.g. user passed invalid options to interpreter.
46
62
  def self.cli(args)
47
63
  pocketknife = Pocketknife.new
64
+ timer = Time.now
48
65
 
49
66
  OptionParser.new do |parser|
50
67
  parser.banner = <<-HERE
@@ -96,9 +113,18 @@ OPTIONS:
96
113
  pocketknife.can_install = false
97
114
  end
98
115
 
116
+ parser.on("-r", "--runlist RUNLIST", "Override runlist with a comma-separated list of recipes and roles") do |v|
117
+ pocketknife.runlist = v
118
+ end
119
+
120
+ transfer_mechanisms = %w[rsync tar]
121
+ parser.on("-t", "--transfer MECHANISM", transfer_mechanisms, "Specify transfer mechanism (#{transfer_mechanisms.join(', ')})") do |v|
122
+ pocketknife.transfer_mechanism = v.to_sym
123
+ end
124
+
99
125
  begin
100
126
  arguments = parser.parse!
101
- rescue OptionParser::MissingArgument => e
127
+ rescue OptionParser::ParseError => e
102
128
  puts parser
103
129
  puts
104
130
  puts "ERROR: #{e}"
@@ -126,44 +152,62 @@ OPTIONS:
126
152
  if not options[:upload] and not options[:apply]
127
153
  pocketknife.deploy(nodes)
128
154
  end
155
+
156
+ pocketknife.say("* SUCCESS! Took #{"%0.2f" % [Time.now-timer]} seconds")
129
157
  rescue NodeError => e
130
158
  puts "! #{e.node}: #{e}"
131
159
  exit -1
160
+ rescue Errno::ENOENT => e
161
+ puts "! #{e.message}"
162
+ exit -1
132
163
  end
133
164
  end
134
165
  end
135
166
 
136
167
  # Returns the software's version.
137
168
  #
138
- # @return [String] A version string.
169
+ # @return [String] A version string, e.g. <tt>0.0.1</tt>.
139
170
  def self.version
140
- return "0.0.1"
171
+ return Pocketknife::Version::STRING
141
172
  end
142
173
 
143
- # Amount of detail to display? true means verbose, nil means normal, false means quiet.
174
+ # Amount of detail to display.
175
+ #
176
+ # @return [Nil, Boolean] +true+ means verbose, +nil+ means normal, +false+ means quiet.
144
177
  attr_accessor :verbosity
145
178
 
146
- # Can chef and its dependencies be installed automatically if not found? true means perform installation without prompting, false means quit if chef isn't available, and nil means prompt the user for input.
179
+ # Should Chef and its dependencies be installed automatically if not found on a node?
180
+ #
181
+ # @return [Nil, Boolean] +true+ means perform the installation without prompting, +false+ means quit if Chef isn't found, and +nil+ means prompt the user to decide this interactively.
147
182
  attr_accessor :can_install
148
183
 
149
- # {Pocketknife::NodeManager} instance.
184
+ # @return [Pocketknife::NodeManager] This instance's node manager.
150
185
  attr_accessor :node_manager
151
186
 
152
- # Instantiate a new Pocketknife.
187
+ # @return [Nil, String] Override runlist with a comma-separated list of recipes and roles.
188
+ attr_accessor :runlist
189
+
190
+ # @return [Symbol] Use :rsync or :tar to transfer files.
191
+ attr_accessor :transfer_mechanism
192
+
193
+ # Instantiates a new Pocketknife.
153
194
  #
154
- # @option [Boolean] verbosity Amount of detail to display. +true+ means verbose, +nil+ means normal, +false+ means quiet.
155
- # @option [Boolean] install Install Chef and its dependencies if needed? +true+ means do so automatically, +false+ means don't, and +nil+ means display a prompt to ask the user what to do.
195
+ # @option [Nil, Boolean] verbosity Amount of detail to display. +true+ means verbose, +nil+ means normal, +false+ means quiet.
196
+ # @option [Nil, Boolean] install Install Chef and its dependencies if needed? +true+ means do so automatically, +false+ means don't, and +nil+ means display a prompt to ask the user what to do.
156
197
  def initialize(opts={})
157
198
  self.verbosity = opts[:verbosity]
158
199
  self.can_install = opts[:install]
200
+ self.runlist = opts[:runlist]
201
+ self.transfer_mechanism = opts[:transfer_mechanism] || :rsync
159
202
 
160
203
  self.node_manager = NodeManager.new(self)
161
204
  end
162
205
 
163
- # Display a message, but only if it's important enough
206
+ # Displays a message, but only if it's important enough.
164
207
  #
165
208
  # @param [String] message The message to display.
166
- # @param [Boolean] importance How important is this? +true+ means important, +nil+ means normal, +false+ means unimportant.
209
+ # @param [Nil, Boolean] importance How important is this? +true+ means important, +nil+ means normal, +false+ means unimportant.
210
+ # @return [void]
167
211
  def say(message, importance=nil)
168
212
  display = \
169
213
  case self.verbosity
@@ -183,8 +227,9 @@ OPTIONS:
183
227
  # Creates a new project directory.
184
228
  #
185
229
  # @param [String] project The name of the project directory to create.
186
- # @yield [path] Yields status information to the optionally supplied block.
230
+ # @yield status information to the optionally supplied block.
187
231
  # @yieldparam [String] path The path of the file or directory created.
232
+ # @return [void]
188
233
  def create(project)
189
234
  self.say("* Creating project in directory: #{project}")
190
235
 
@@ -206,16 +251,18 @@ OPTIONS:
206
251
  return true
207
252
  end
208
253
 
209
- # Returns a Node instance.
254
+ # Returns a node.
210
255
  #
211
- # @param[String] name The name of the node.
256
+ # @param [String] name The name of the node.
257
+ # @return [Pocketknife::Node]
212
258
  def node(name)
213
259
  return node_manager.find(name)
214
260
  end
215
261
 
216
262
  # Deploys configuration to the nodes, calls {#upload} and {#apply}.
217
263
  #
218
- # @params[Array<String>] nodes A list of node names.
264
+ # @param [Array<String>] nodes A list of node names.
265
+ # @return [void]
219
266
  def deploy(nodes)
220
267
  node_manager.assert_known(nodes)
221
268
 
@@ -229,6 +276,7 @@ OPTIONS:
229
276
  # Uploads configuration information to remote nodes.
230
277
  #
231
278
  # @param [Array<String>] nodes A list of node names.
279
+ # @return [void]
232
280
  def upload(nodes)
233
281
  node_manager.assert_known(nodes)
234
282
 
@@ -242,6 +290,7 @@ OPTIONS:
242
290
  # Applies configurations to remote nodes.
243
291
  #
244
292
  # @param [Array<String>] nodes A list of node names.
293
+ # @return [void]
245
294
  def apply(nodes)
246
295
  node_manager.assert_known(nodes)
247
296
 
@@ -1,85 +1,125 @@
1
1
  class Pocketknife
2
- # == NodeError
2
+ # == Error
3
3
  #
4
- # An error with a {Pocketknife::Node}. This is meant to be subclassed by a more specific error.
5
- class NodeError < StandardError
6
- # The name of the node.
7
- attr_accessor :node
8
-
9
- # Instantiate a new exception.
4
+ # Superclass of all Pocketknife errors.
5
+ class Error < StandardError
6
+ # == InvalidTransferMechanism
10
7
  #
11
- # @param [String] message The message to display.
12
- # @param [String] node The name of the unknown node.
13
- def initialize(message, node)
14
- self.node = node
15
- super(message)
8
+ # Exception raised when given an invalid transfer mechanism, e.g. not :tar or :rsync.
9
+ class InvalidTransferMechanism < Error
10
+ # @return [Symbol] Transfer mechanism that failed.
11
+ attr_accessor :mechanism
12
+
13
+ def initialize(mechanism)
14
+ super("Invalid transfer mechanism: #{mechanism}")
15
+ end
16
16
  end
17
- end
18
17
 
19
- # == NoSuchNode
20
- #
21
- # Exception raised when asked to perform an operation on an unknown node.
22
- class NoSuchNode < NodeError
23
- end
18
+ # == NodeError
19
+ #
20
+ # An error with a {Pocketknife::Node}. This is meant to be subclassed by a more specific error.
21
+ class NodeError < Error
22
+ # @return [String] The name of the node.
23
+ attr_accessor :node
24
24
 
25
- # == UnsupportedInstallationPlatform
26
- #
27
- # Exception raised when asked to install Chef on a node with an unsupported platform.
28
- class UnsupportedInstallationPlatform < NodeError
29
- end
25
+ # Instantiate a new exception.
26
+ #
27
+ # @param [String] message The message to display.
28
+ # @param [String] node The name of the unknown node.
29
+ def initialize(message, node)
30
+ self.node = node
31
+ super(message)
32
+ end
30
33
 
31
- # == NotInstalling
32
- #
33
- # Exception raised when Chef is not available ohn a node, but user asked not to install it.
34
- class NotInstalling < NodeError
35
- end
34
+ # == NoSuchNode
35
+ #
36
+ # Exception raised when asked to perform an operation on an unknown node.
37
+ class NoSuchNode < NodeError
38
+ end
36
39
 
37
- # == ExecutionError
38
- #
39
- # Exception raised when something goes wrong executing commands against remote host.
40
- class ExecutionError < NodeError
41
- # Command that failed.
42
- attr_accessor :command
40
+ # == UnsupportedInstallationPlatform
41
+ #
42
+ # Exception raised when asked to install Chef on a node with an unsupported platform.
43
+ class UnsupportedInstallationPlatform < NodeError
44
+ end
43
45
 
44
- # Cause of exception, a {Rye:Err}.
45
- attr_accessor :cause
46
+ # == NotInstalling
47
+ #
48
+ # Exception raised when Chef is not available ohn a node, but user asked not to install it.
49
+ class NotInstalling < NodeError
50
+ end
46
51
 
47
- # Was execution's output shown immediately? If so, don't include output in message.
48
- attr_accessor :immediate
52
+ # == RsyncError
53
+ #
54
+ # Exception raised if rsync command failed.
55
+ class RsyncError < NodeError
56
+ # @return [String] Command that failed.
57
+ attr_accessor :command
49
58
 
50
- # Instantiates a new exception.
51
- #
52
- # @param [String] node The name of the unknown node.
53
- # @param [String] command The command that failed.
54
- # @param [Rye::Err] cause The actual exception thrown.
55
- # @param [Boolean] immediate Was execution's output shown immediately? If so, don't include output in message.
56
- def initialize(node, command, cause, immediate)
57
- self.command = command
58
- self.cause = cause
59
- self.immediate = immediate
60
-
61
- message = <<-HERE.chomp
59
+ def initialize(command, node)
60
+ super("Failed while rsyncing: #{command}", node)
61
+ end
62
+ end
63
+
64
+ # == ExecutionError
65
+ #
66
+ # Exception raised when something goes wrong executing commands against remote host.
67
+ class ExecutionError < NodeError
68
+ # @return [String] Command that failed.
69
+ attr_accessor :command
70
+
71
+ # @return [Rye::Err] Cause of exception, a Rye:Err.
72
+ attr_accessor :cause
73
+
74
+ # @return [Boolean] Was execution's output shown immediately? If so, don't include output in message.
75
+ attr_accessor :immediate
76
+
77
+ # Instantiates a new exception.
78
+ #
79
+ # @param [String] node The name of the unknown node.
80
+ # @param [String] command The command that failed.
81
+ # @param [Rye::Err] cause The actual exception thrown.
82
+ # @param [Boolean] immediate Was execution's output shown immediately? If so, don't include output in message.
83
+ def initialize(node, command, cause, immediate)
84
+ self.command = command
85
+ self.cause = cause
86
+ self.immediate = immediate
87
+
88
+ message = <<-HERE.chomp
62
89
  Failed while executing commands on node '#{node}'
63
90
  - COMMAND: #{command}
64
91
  - EXIT STATUS: #{cause.exit_status}
65
- HERE
92
+ HERE
66
93
 
67
- unless immediate
68
- message << <<-HERE.chomp
94
+ unless immediate
95
+ message << <<-HERE.chomp
69
96
 
70
97
  - STDOUT: #{cause.stdout.to_s.strip}
71
98
  - STDERR: #{cause.stderr.to_s.strip}
72
- HERE
99
+ HERE
100
+ end
101
+
102
+ super(message, node)
103
+ end
104
+
105
+ # Returns exit status.
106
+ #
107
+ # @return [Integer] Exit status from execution.
108
+ def exit_status
109
+ return self.cause.exit_status
110
+ end
111
+
73
112
  end
74
113
 
75
- super(message, node)
76
114
  end
77
115
 
78
- # Returns exit status.
79
- #
80
- # @return [Integer] Exit status from execution.
81
- def exit_status
82
- return self.cause.exit_status
83
- end
84
116
  end
117
+
118
+ InvalidTransferMechanism = Pocketknife::Error::InvalidTransferMechanism
119
+ NodeError = Pocketknife::Error::NodeError
120
+ NoSuchNode = Pocketknife::Error::NodeError::NoSuchNode
121
+ UnsupportedInstallationPlatform = Pocketknife::Error::NodeError::UnsupportedInstallationPlatform
122
+ NotInstalling = Pocketknife::Error::NodeError::NotInstalling
123
+ RsyncError = Pocketknife::Error::NodeError::RsyncError
124
+ ExecutionError = Pocketknife::Error::NodeError::ExecutionError
85
125
  end