bosh_cli 0.19.6 → 1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/bin/bosh +3 -0
  2. data/lib/cli.rb +15 -5
  3. data/lib/cli/{commands/base.rb → base_command.rb} +38 -44
  4. data/lib/cli/command_discovery.rb +40 -0
  5. data/lib/cli/command_handler.rb +135 -0
  6. data/lib/cli/commands/biff.rb +16 -12
  7. data/lib/cli/commands/blob_management.rb +10 -3
  8. data/lib/cli/commands/cloudcheck.rb +13 -11
  9. data/lib/cli/commands/complete.rb +29 -0
  10. data/lib/cli/commands/deployment.rb +137 -28
  11. data/lib/cli/commands/help.rb +96 -0
  12. data/lib/cli/commands/job.rb +4 -1
  13. data/lib/cli/commands/job_management.rb +36 -23
  14. data/lib/cli/commands/job_rename.rb +11 -12
  15. data/lib/cli/commands/log_management.rb +28 -32
  16. data/lib/cli/commands/maintenance.rb +6 -1
  17. data/lib/cli/commands/misc.rb +129 -87
  18. data/lib/cli/commands/package.rb +6 -65
  19. data/lib/cli/commands/property_management.rb +20 -8
  20. data/lib/cli/commands/release.rb +211 -206
  21. data/lib/cli/commands/ssh.rb +178 -188
  22. data/lib/cli/commands/stemcell.rb +114 -51
  23. data/lib/cli/commands/task.rb +74 -56
  24. data/lib/cli/commands/user.rb +6 -3
  25. data/lib/cli/commands/vms.rb +17 -15
  26. data/lib/cli/config.rb +27 -1
  27. data/lib/cli/core_ext.rb +27 -1
  28. data/lib/cli/deployment_helper.rb +47 -0
  29. data/lib/cli/director.rb +18 -9
  30. data/lib/cli/errors.rb +6 -0
  31. data/lib/cli/job_builder.rb +75 -23
  32. data/lib/cli/job_property_collection.rb +87 -0
  33. data/lib/cli/job_property_validator.rb +130 -0
  34. data/lib/cli/package_builder.rb +32 -5
  35. data/lib/cli/release.rb +2 -0
  36. data/lib/cli/release_builder.rb +9 -13
  37. data/lib/cli/release_compiler.rb +5 -34
  38. data/lib/cli/release_tarball.rb +4 -19
  39. data/lib/cli/runner.rb +118 -694
  40. data/lib/cli/version.rb +1 -1
  41. data/spec/assets/config/swift-hp/config/final.yml +6 -0
  42. data/spec/assets/config/swift-hp/config/private.yml +7 -0
  43. data/spec/assets/config/swift-rackspace/config/final.yml +6 -0
  44. data/spec/assets/config/swift-rackspace/config/private.yml +6 -0
  45. data/spec/spec_helper.rb +0 -5
  46. data/spec/unit/base_command_spec.rb +32 -37
  47. data/spec/unit/biff_spec.rb +11 -10
  48. data/spec/unit/cli_commands_spec.rb +96 -88
  49. data/spec/unit/core_ext_spec.rb +1 -1
  50. data/spec/unit/deployment_manifest_spec.rb +36 -0
  51. data/spec/unit/director_spec.rb +17 -3
  52. data/spec/unit/job_builder_spec.rb +2 -2
  53. data/spec/unit/job_property_collection_spec.rb +111 -0
  54. data/spec/unit/job_property_validator_spec.rb +7 -0
  55. data/spec/unit/job_rename_spec.rb +7 -6
  56. data/spec/unit/package_builder_spec.rb +2 -2
  57. data/spec/unit/release_builder_spec.rb +33 -0
  58. data/spec/unit/release_spec.rb +54 -0
  59. data/spec/unit/release_tarball_spec.rb +2 -7
  60. data/spec/unit/runner_spec.rb +1 -151
  61. data/spec/unit/ssh_spec.rb +15 -9
  62. metadata +41 -12
  63. data/lib/cli/command_definition.rb +0 -52
  64. data/lib/cli/templates/help_message.erb +0 -80
data/lib/cli/release.rb CHANGED
@@ -64,6 +64,8 @@ module Bosh::Cli
64
64
  has_legacy_secret? ||
65
65
  has_blobstore_secrets?(bs, "atmos", "secret") ||
66
66
  has_blobstore_secrets?(bs, "simple", "user", "password") ||
67
+ has_blobstore_secrets?(bs, "swift", "rackspace") ||
68
+ has_blobstore_secrets?(bs, "swift", "hp") ||
67
69
  has_blobstore_secrets?(bs, "s3", "access_key_id", "secret_access_key")
68
70
  end
69
71
 
@@ -7,7 +7,7 @@ module Bosh::Cli
7
7
 
8
8
  DEFAULT_RELEASE_NAME = "bosh_release"
9
9
 
10
- attr_reader :release, :packages, :jobs, :version
10
+ attr_reader :release, :packages, :jobs, :version, :build_dir
11
11
 
12
12
  # @param [Bosh::Cli::Release] release Current release
13
13
  # @param [Array<Bosh::Cli::PackageBuilder>] packages Built packages
@@ -23,7 +23,12 @@ module Bosh::Cli
23
23
  @dev_index = VersionsIndex.new(dev_releases_dir, release_name)
24
24
  @index = @final ? @final_index : @dev_index
25
25
 
26
- create_release_build_dir
26
+ @build_dir = Dir.mktmpdir
27
+
28
+ in_build_dir do
29
+ FileUtils.mkdir("packages")
30
+ FileUtils.mkdir("jobs")
31
+ end
27
32
  end
28
33
 
29
34
  # @return [String] Release name
@@ -107,6 +112,7 @@ module Bosh::Cli
107
112
  "name" => package.name,
108
113
  "version" => package.version,
109
114
  "sha1" => package.checksum,
115
+ "fingerprint" => package.fingerprint,
110
116
  "dependencies" => package.dependencies
111
117
  }
112
118
  end
@@ -115,6 +121,7 @@ module Bosh::Cli
115
121
  {
116
122
  "name" => job.name,
117
123
  "version" => job.version,
124
+ "fingerprint" => job.fingerprint,
118
125
  "sha1" => job.checksum,
119
126
  }
120
127
  end
@@ -229,17 +236,6 @@ module Bosh::Cli
229
236
  end
230
237
  end
231
238
 
232
- def build_dir
233
- @build_dir ||= Dir.mktmpdir
234
- end
235
-
236
- def create_release_build_dir
237
- in_build_dir do
238
- FileUtils.mkdir("packages")
239
- FileUtils.mkdir("jobs")
240
- end
241
- end
242
-
243
239
  def in_build_dir(&block)
244
240
  Dir.chdir(build_dir) { yield }
245
241
  end
@@ -12,11 +12,10 @@ module Bosh::Cli
12
12
 
13
13
  # @param [String] manifest_file Release manifest path
14
14
  # @param [Bosh::Blobstore::Client] blobstore Blobstore client
15
- # @param [Hash] remote_release Remote release info from director
16
15
  # @param [Array] package_matches List of package checksums that director
17
16
  # can match
18
17
  # @param [String] release_dir Release directory
19
- def initialize(manifest_file, blobstore, remote_release = nil,
18
+ def initialize(manifest_file, blobstore,
20
19
  package_matches = [], release_dir = nil)
21
20
 
22
21
  @blobstore = blobstore
@@ -30,28 +29,11 @@ module Bosh::Cli
30
29
 
31
30
  @package_matches = Set.new(package_matches)
32
31
 
33
- at_exit { FileUtils.rm_rf(@build_dir) }
34
-
35
32
  FileUtils.mkdir_p(@jobs_dir)
36
33
  FileUtils.mkdir_p(@packages_dir)
37
34
 
38
35
  @manifest = load_yaml_file(manifest_file)
39
36
 
40
- if remote_release
41
- # TODO: instead of OpenStruct conversion we should probably
42
- # introduce proper abstractions for things below
43
- @remote_packages = remote_release["packages"].map do |pkg|
44
- OpenStruct.new(pkg)
45
- end
46
-
47
- @remote_jobs = remote_release["jobs"].map do |job|
48
- OpenStruct.new(job)
49
- end
50
- else
51
- @remote_packages = []
52
- @remote_jobs = []
53
- end
54
-
55
37
  @name = @manifest["name"]
56
38
  @version = @manifest["version"]
57
39
  @packages = @manifest["packages"].map { |pkg| OpenStruct.new(pkg) }
@@ -185,28 +167,17 @@ module Bosh::Cli
185
167
  # @return [Boolean]
186
168
  def remote_package_exists?(local_package)
187
169
  # If checksum is known to director we can always match it
188
- return true if @package_matches.include?(local_package.sha1)
189
-
190
- remote_object_exists?(@remote_packages, local_package)
170
+ @package_matches.include?(local_package.sha1) ||
171
+ (local_package.fingerprint &&
172
+ @package_matches.include?(local_package.fingerprint))
191
173
  end
192
174
 
193
175
  # Checks if local job is already known remotely
194
176
  # @param [#name, #version] local_job
195
177
  # @return [Boolean]
196
178
  def remote_job_exists?(local_job)
197
- remote_object_exists?(@remote_jobs, local_job)
198
- end
199
-
200
- # @param [Enumerable] remote_objects Remote object collection
201
- # @param [#name, #version] local_object
202
- # @return [Boolean]
203
- def remote_object_exists?(remote_objects, local_object)
204
- remote_objects.any? do |remote_object|
205
- remote_object.name == local_object.name &&
206
- remote_object.version.to_s == local_object.version.to_s
207
- end
179
+ false
208
180
  end
209
-
210
181
  end
211
182
 
212
183
  end
@@ -32,25 +32,17 @@ module Bosh::Cli
32
32
 
33
33
  # Repacks tarball according to the structure of remote release
34
34
  # Return path to repackaged tarball or nil if repack has failed
35
- def repack(remote_release = nil, package_matches = [])
35
+ def repack(package_matches = [])
36
36
  return nil unless valid?
37
37
  unpack
38
38
 
39
39
  tmpdir = Dir.mktmpdir
40
40
  repacked_path = File.join(tmpdir, "release-repack.tgz")
41
41
 
42
- at_exit { FileUtils.rm_rf(tmpdir) }
43
-
44
42
  manifest = load_yaml_file(File.join(@unpack_dir, "release.MF"))
45
43
 
46
- # Remote release could be not-existent, then package matches are supposed
47
- # to satisfy everything
48
- remote_release ||= {"jobs" => [], "packages" => []}
49
-
50
44
  local_packages = manifest["packages"]
51
45
  local_jobs = manifest["jobs"]
52
- remote_packages = remote_release["packages"]
53
- remote_jobs = remote_release["jobs"]
54
46
 
55
47
  @skipped = 0
56
48
 
@@ -61,8 +53,8 @@ module Bosh::Cli
61
53
  local_packages.each do |package|
62
54
  say("#{package["name"]} (#{package["version"]})".ljust(30), " ")
63
55
  if package_matches.include?(package["sha1"]) ||
64
- remote_packages.any? { |rp| package["name"] == rp["name"] &&
65
- package["version"].to_s == rp["version"].to_s }
56
+ (package["fingerprint"] &&
57
+ package_matches.include?(package["fingerprint"]))
66
58
  say("SKIP".green)
67
59
  @skipped += 1
68
60
  FileUtils.rm_rf(File.join("packages", "#{package["name"]}.tgz"))
@@ -73,14 +65,7 @@ module Bosh::Cli
73
65
 
74
66
  local_jobs.each do |job|
75
67
  say("#{job["name"]} (#{job["version"]})".ljust(30), " ")
76
- if remote_jobs.any? { |rj| job["name"] == rj["name"] &&
77
- job["version"].to_s == rj["version"].to_s }
78
- say("SKIP".green)
79
- @skipped += 1
80
- FileUtils.rm_rf(File.join("jobs", "#{job["name"]}.tgz"))
81
- else
82
- say("UPLOAD".red)
83
- end
68
+ say("UPLOAD".red)
84
69
  end
85
70
 
86
71
  return nil if @skipped == 0
data/lib/cli/runner.rb CHANGED
@@ -6,667 +6,186 @@ module Bosh::Cli
6
6
  end
7
7
 
8
8
  class Runner
9
- COMMANDS = { }
10
- ALL_KEYWORDS = []
11
9
 
12
- attr_reader :usage
13
- attr_reader :namespace
14
- attr_reader :action
10
+ # @return [Array]
15
11
  attr_reader :args
16
- attr_reader :options
17
12
 
18
- # The runner is an instance of the command type that the user issued,
19
- # such as a Deployment instance. This is an accessor for testing.
20
- # @return [Bosh::Cli::Command::<type>] Instance of the command instance.
21
- attr_accessor :runner
13
+ # @return [Hash]
14
+ attr_reader :options
22
15
 
16
+ # @param [Array] args
23
17
  def self.run(args)
24
18
  new(args).run
25
19
  end
26
20
 
27
- def initialize(args)
21
+ # @param [Array] args
22
+ def initialize(args, options = {})
28
23
  @args = args
29
- @options = {
30
- :director_checks => true,
31
- :colorize => true,
32
- }
33
- end
24
+ @options = options.dup
34
25
 
35
- def prepare
36
- define_commands
37
- parse_options!
38
- Config.output ||= STDOUT unless @options[:quiet]
39
- Config.interactive = !@options[:non_interactive]
40
- Config.colorize = @options.delete(:colorize)
41
- Config.cache = Bosh::Cli::Cache.new(@options[:cache_dir] ||
42
- Bosh::Cli::DEFAULT_CACHE_DIR)
26
+ banner = "Usage: bosh [<options>] <command> [<args>]"
27
+ @option_parser = OptionParser.new(banner)
43
28
 
44
- define_plugin_commands
45
- build_parse_tree
46
- add_shortcuts
29
+ Config.colorize = true
30
+ Config.output ||= STDOUT
47
31
  end
48
32
 
33
+ # Find and run CLI command
34
+ # @return [void]
49
35
  def run
50
- prepare
51
- dispatch unless @namespace && @action
52
-
53
- if @namespace && @action
54
- ns_class_name = @namespace.to_s.gsub(/(?:_|^)(.)/) { $1.upcase }
55
- klass = eval("Bosh::Cli::Command::#{ns_class_name}")
56
- runner = klass.new(@options)
57
- runner.usage = @usage
36
+ parse_global_options
58
37
 
59
- action_arity = runner.method(@action.to_sym).arity
60
- n_required_args = action_arity >= 0 ? action_arity : -action_arity - 1
38
+ Config.interactive = !@options[:non_interactive]
39
+ Config.cache = Bosh::Cli::Cache.new(@options[:cache_dir])
61
40
 
62
- if n_required_args > @args.size
63
- err("Not enough arguments, correct usage is: bosh #{@usage}")
64
- end
65
- if action_arity >= 0 && n_required_args < @args.size
66
- err("Too many arguments, correct usage is: bosh #{@usage}")
67
- end
41
+ build_parse_tree
42
+ load_plugins
43
+ add_shortcuts
68
44
 
69
- runner.send(@action.to_sym, *@args)
70
- exit(runner.exit_code)
71
- elsif @args.empty? || @args == %w(help)
72
- say(help_message)
73
- say(plugin_help_message) if @plugins
74
- elsif @args[0] == "complete"
75
- unless ENV.has_key?('COMP_LINE')
76
- $stderr.puts "COMP_LINE must be set when calling bosh complete"
77
- exit(1)
78
- end
79
- line = ENV['COMP_LINE'].gsub(/^\S*bosh\s*/, '')
80
- puts complete(line).join("\n")
45
+ if @args.empty?
46
+ say(usage)
81
47
  exit(0)
82
- elsif @args[0] == "help"
83
- cmd_args = @args[1..-1]
84
- suggestions = command_suggestions(cmd_args).map do |cmd|
85
- command_usage(cmd, 0)
86
- end
87
- if suggestions.empty?
88
- unknown_command(cmd_args.join(" "))
89
- else
90
- say(suggestions.uniq.join("\n"))
91
- end
92
- else
93
- unknown_command(@args.join(" "))
48
+ end
94
49
 
95
- suggestions = command_suggestions(@args).map do |cmd|
96
- "bosh #{cmd.usage}"
97
- end
50
+ command = search_parse_tree(@parse_tree)
51
+ if command.nil? && Config.interactive
52
+ command = try_alias
53
+ end
54
+
55
+ if command.nil?
56
+ err("Unknown command: #{@args.join(" ")}")
57
+ end
98
58
 
99
- if suggestions.size > 0
100
- say("Did you mean any of these?")
101
- say("\n" + suggestions.uniq.join("\n"))
59
+ command.runner = self
60
+ begin
61
+ exit_code = command.run(@args, @options)
62
+ exit(exit_code)
63
+ rescue OptionParser::ParseError => e
64
+ say(e.message.red)
65
+ say("Usage: bosh #{command.usage_with_params.columnize(60, 7)}")
66
+ if command.has_options?
67
+ say(command.options_summary.indent(7))
102
68
  end
103
- exit(1)
104
69
  end
105
70
 
106
- rescue OptionParser::InvalidOption => e
107
- say(e.message.red + "\n" + basic_usage)
108
- exit(1)
109
- rescue Bosh::Cli::GracefulExit => e
110
- # Redirected bosh commands end up
111
- # generating this exception (kind of goto)
112
- rescue Bosh::Cli::CliExit, Bosh::Cli::DirectorError => e
71
+ rescue OptionParser::ParseError => e
113
72
  say(e.message.red)
114
- exit(e.exit_code)
73
+ say(@option_parser.to_s)
74
+ exit(1)
115
75
  rescue Bosh::Cli::CliError => e
116
- say("Error #{e.error_code}: #{e.message}".red)
76
+ say(e.message.red)
117
77
  exit(e.exit_code)
118
- rescue => e
119
- if @options[:debug]
120
- raise e
121
- else
122
- save_exception(e)
123
- exit(1)
124
- end
125
78
  end
126
79
 
127
- # looks for command completion in the parse tree
128
- def parse_tree_completion(node, words, index)
80
+ # Finds command completions in the parse tree
81
+ # @param [Array] words Completion prefix
82
+ # @param [Bosh::Cli::ParseTreeNode] node Current parse tree node
83
+ def find_completions(words, node = @parse_tree, index = 0)
129
84
  word = words[index]
130
85
 
131
86
  # exact match and not on the last word
132
87
  if node[word] && words.length != index
133
- parse_tree_completion(node[word], words, index + 1)
88
+ find_completions(words, node[word], index + 1)
134
89
 
135
- # exact match at the last word
90
+ # exact match at the last word
136
91
  elsif node[word]
137
92
  node[word].values
138
93
 
139
- # find all partial matches
94
+ # find all partial matches
140
95
  else
141
96
  node.keys.grep(/^#{word}/)
142
97
  end
143
98
  end
144
99
 
145
- # for use with:
146
- # complete -C 'bosh complete' bosh
147
- # @param [String] line command line (minus "bosh")
148
- # @return [Array]
149
- def complete(line)
150
- words = line.split(/\s+/)
151
- parse_tree_completion(@parse_tree, words, 0)
152
- end
153
-
154
- def command(name, &block)
155
- cmd_def = CommandDefinition.new
156
- cmd_def.instance_eval(&block)
157
- COMMANDS[name] = cmd_def
158
- ALL_KEYWORDS.push(*cmd_def.keywords)
159
- end
160
-
161
- def find_command(name)
162
- COMMANDS[name] || raise("Unknown command definition: #{name}")
163
- end
164
-
165
- def dispatch(command = nil)
166
- command ||= search_parse_tree(@parse_tree)
167
- command = try_alias if command.nil? && Config.interactive
168
- return if command.nil?
169
- @usage = command.usage
170
-
171
- case command.route
172
- when Array
173
- @namespace, @action = command.route
174
- when Proc
175
- @namespace, @action = command.route.call(@args)
176
- else
177
- raise "Command definition is invalid, " +
178
- "route should be an Array or Proc"
179
- end
180
- end
181
-
182
- def define_commands
183
- command :version do
184
- usage "version"
185
- desc "Show version"
186
- route :misc, :version
187
- end
188
-
189
- command :alias do
190
- usage "alias <name> <command>"
191
- desc "Create an alias <name> for command <command>"
192
- route :misc, :set_alias
193
- end
194
-
195
- command :list_aliases do
196
- usage "aliases"
197
- desc "Show the list of available command aliases"
198
- route :misc, :list_aliases
199
- end
200
-
201
- command :target do
202
- usage "target [<name>] [<alias>]"
203
- desc "Choose director to talk to (optionally creating an alias). " +
204
- "If no arguments given, show currently targeted director"
205
- route do |args|
206
- (args.size > 0) ? [:misc, :set_target] : [:misc, :show_target]
207
- end
208
- end
209
-
210
- command :list_targets do
211
- usage "targets"
212
- desc "Show the list of available targets"
213
- route :misc, :list_targets
214
- end
215
-
216
- command :deployment do
217
- usage "deployment [<name>]"
218
- desc "Choose deployment to work with " +
219
- "(it also updates current target)"
220
- route do |args|
221
- if args.size > 0
222
- [:deployment, :set_current]
223
- else
224
- [:deployment, :show_current]
225
- end
226
- end
227
- end
228
-
229
- command :deploy do
230
- usage "deploy"
231
- desc "Deploy according to the currently selected " +
232
- "deployment manifest"
233
- option "--recreate", "recreate all VMs in deployment"
234
- route :deployment, :perform
235
- end
236
-
237
- command :edit_deployment do
238
- usage "edit deployment"
239
- desc "Edit current deployment manifest"
240
- route :deployment, :edit
241
- end
242
-
243
- command :ssh do
244
- usage "ssh <job> [index] [<options>] [command]"
245
- desc "Given a job, execute the given command or " +
246
- "start an interactive session"
247
- option "--public_key <file>"
248
- option "--gateway_host <host>"
249
- option "--gateway_user <user>"
250
- option "--default_password", "Use default ssh password. Not recommended."
251
- route :ssh, :shell
252
- end
253
-
254
- command :ssh_cleanup do
255
- usage "ssh_cleanup <job> [index]"
256
- desc "Cleanup SSH artifacts"
257
- route :ssh, :cleanup
258
- end
259
-
260
- command :scp do
261
- usage "scp <job> [index] (--upload|--download) [options]" +
262
- "/path/to/source /path/to/destination"
263
- desc "upload/download the source file to the given job. " +
264
- "Note: for dowload /path/to/destination is a directory"
265
- option "--public_key <file>"
266
- option "--gateway_host <host>"
267
- option "--gateway_user <user>"
268
- route :ssh, :scp
269
- end
270
-
271
- command :scp do
272
- usage "scp <job> <--upload | --download> [options] " +
273
- "/path/to/source /path/to/destination"
274
- desc "upload/download the source file to the given job. " +
275
- "Note: for download /path/to/destination is a directory"
276
- option "--index <job_index>"
277
- option "--public_key <file>"
278
- option "--gateway_host <host>"
279
- option "--gateway_user <user>"
280
- route :ssh, :scp
281
- end
282
-
283
- command :status do
284
- usage "status"
285
- desc "Show current status (current target, " +
286
- "user, deployment info etc.)"
287
- route :misc, :status
288
- end
289
-
290
- command :login do
291
- usage "login [<name>] [<password>]"
292
- desc "Provide credentials for the subsequent interactions " +
293
- "with targeted director"
294
- route :misc, :login
295
- end
296
-
297
- command :logout do
298
- usage "logout"
299
- desc "Forget saved credentials for targeted director"
300
- route :misc, :logout
301
- end
302
-
303
- command :purge do
304
- usage "purge"
305
- desc "Purge local manifest cache"
306
- route :misc, :purge_cache
307
- end
308
-
309
- command :create_release do
310
- usage "create release"
311
- desc "Create release (assumes current directory " +
312
- "to be a release repository)"
313
- route :release, :create
314
- option "--force", "bypass git dirty state check"
315
- option "--final", "create production-ready release " +
316
- "(stores artefacts in blobstore, bumps final version)"
317
- option "--with-tarball", "create full release tarball" +
318
- "(by default only manifest is created)"
319
- option "--dry-run", "stop before writing release " +
320
- "manifest (for diagnostics)"
321
- end
322
-
323
- command :create_user do
324
- usage "create user [<name>] [<password>]"
325
- desc "Create user"
326
- route :user, :create
327
- end
328
-
329
- command :create_package do
330
- usage "create package <name>|<path>"
331
- desc "Build a single package"
332
- route :package, :create
333
- end
334
-
335
- command :start_job do
336
- usage "start <job> [<index>]"
337
- desc "Start job/instance"
338
- route :job_management, :start_job
339
-
340
- power_option "--force"
341
- end
342
-
343
- command :stop_job do
344
- usage "stop <job> [<index>]"
345
- desc "Stop job/instance"
346
- route :job_management, :stop_job
347
- option "--soft", "stop process only"
348
- option "--hard", "power off VM"
349
-
350
- power_option "--force"
351
- end
352
-
353
- command :restart_job do
354
- usage "restart <job> [<index>]"
355
- desc "Restart job/instance (soft stop + start)"
356
- route :job_management, :restart_job
357
-
358
- power_option "--force"
359
- end
360
-
361
- command :recreate_job do
362
- usage "recreate <job> [<index>]"
363
- desc "Recreate job/instance (hard stop + start)"
364
- route :job_management, :recreate_job
365
-
366
- power_option "--force"
367
- end
368
-
369
- command :rename_job do
370
- usage "rename <old_job_name> <new_job_name>"
371
- desc "renames a job. NOTE, your deployment manifest must also be " +
372
- "updated to reflect the new job name."
373
- power_option "--force"
374
-
375
- route :job_rename, :rename
376
- end
377
-
378
- command :fetch_logs do
379
- usage "logs <job> <index>"
380
- desc "Fetch job (default) or agent (if option provided) logs"
381
- route :log_management, :fetch_logs
382
- option "--agent", "fetch agent logs"
383
- option "--only <filter1>[...]", "only fetch logs that satisfy " +
384
- "given filters (defined in job spec)"
385
- option "--all", "fetch all files in the job or agent log directory"
386
- end
387
-
388
- command :set_property do
389
- usage "set property <name> <value>"
390
- desc "Set deployment property"
391
- route :property_management, :set
392
- end
393
-
394
- command :get_property do
395
- usage "get property <name>"
396
- desc "Get deployment property"
397
- route :property_management, :get
398
- end
399
-
400
- command :unset_property do
401
- usage "unset property <name>"
402
- desc "Unset deployment property"
403
- route :property_management, :unset
404
- end
405
-
406
- command :list_properties do
407
- usage "properties"
408
- desc "List current deployment properties"
409
- route :property_management, :list
410
- option "--terse", "easy to parse output"
411
- end
412
-
413
- command :init_release do
414
- usage "init release [<path>]"
415
- desc "Initialize release directory"
416
- route :release, :init
417
- option "--git", "initialize git repository"
418
- end
419
-
420
- command :generate_package do
421
- usage "generate package <name>"
422
- desc "Generate package template"
423
- route :package, :generate
424
- end
425
-
426
- command :generate_job do
427
- usage "generate job <name>"
428
- desc "Generate job template"
429
- route :job, :generate
430
- end
431
-
432
- command :upload_stemcell do
433
- usage "upload stemcell <path>"
434
- desc "Upload the stemcell"
435
- route :stemcell, :upload
436
- end
437
-
438
- command :upload_release do
439
- usage "upload release [<path>]"
440
- desc "Upload release (<path> can point to tarball or manifest, " +
441
- "defaults to the most recently created release)"
442
- route :release, :upload
443
- end
444
-
445
- command :verify_stemcell do
446
- usage "verify stemcell <path>"
447
- desc "Verify stemcell"
448
- route :stemcell, :verify
449
- end
450
-
451
- command :verify_release do
452
- usage "verify release <path>"
453
- desc "Verify release"
454
- route :release, :verify
455
- end
456
-
457
- command :delete_deployment do
458
- usage "delete deployment <name>"
459
- desc "Delete deployment"
460
- route :deployment, :delete
461
- option "--force", "ignore all errors while deleting parts " +
462
- "of the deployment"
463
- end
464
-
465
- command :delete_stemcell do
466
- usage "delete stemcell <name> <version>"
467
- desc "Delete the stemcell"
468
- route :stemcell, :delete
469
- end
470
-
471
- command :delete_release do
472
- usage "delete release <name> [<version>]"
473
- desc "Delete release (or a particular release version)"
474
- route :release, :delete
475
- option "--force", "ignore errors during deletion"
476
- end
477
-
478
- command :reset_release do
479
- usage "reset release"
480
- desc "Reset release development environment " +
481
- "(deletes all dev artifacts)"
482
- route :release, :reset
483
- end
484
-
485
- command :cancel_task do
486
- usage "cancel task <id>"
487
- desc "Cancel task once it reaches the next cancel checkpoint"
488
- route :task, :cancel
489
- end
490
-
491
- command :track_task do
492
- usage "task [<task_id>|last]"
493
- desc "Show task status and start tracking its output"
494
- route :task, :track
495
- option "--no-cache", "don't cache output locally"
496
- option "--event|--soap|--debug", "different log types to track"
497
- option "--raw", "don't beautify log"
498
- end
499
-
500
- command :list_stemcells do
501
- usage "stemcells"
502
- desc "Show the list of available stemcells"
503
- route :stemcell, :list
504
- end
505
-
506
- command :list_public_stemcells do
507
- usage "public stemcells"
508
- desc "Show the list of publicly available stemcells for download."
509
- route :stemcell, :list_public
510
- option "--full", "show the full download url"
100
+ def parse_global_options
101
+ # -v is reserved for verbose but having 'bosh -v' is handy,
102
+ # hence the little hack
103
+ if @args.size == 1 && (@args[0] == "-v" || @args[0] == "--version")
104
+ @args = %w(version)
105
+ return
511
106
  end
512
107
 
513
- command :download_public_stemcell do
514
- usage "download public stemcell <stemcell_name>"
515
- desc "Downloads a stemcell from the public blobstore."
516
- route :stemcell, :download_public
108
+ opts = @option_parser
109
+ opts.on("-c", "--config FILE", "Override configuration file") do |file|
110
+ @options[:config] = file
517
111
  end
518
-
519
- command :list_releases do
520
- usage "releases"
521
- desc "Show the list of available releases"
522
- route :release, :list
112
+ opts.on("-C", "--cache-dir DIR", "Override cache directory") do |dir|
113
+ @options[:cache_dir] = dir
523
114
  end
524
-
525
- command :list_deployments do
526
- usage "deployments"
527
- desc "Show the list of available deployments"
528
- route :deployment, :list
115
+ opts.on("--[no-]color", "Toggle colorized output") do |v|
116
+ Config.colorize = v
529
117
  end
530
118
 
531
- command :diff do
532
- usage "diff [<template_file>]"
533
- desc "Diffs your current BOSH deployment configuration against " +
534
- "the specified BOSH deployment configuration template so that " +
535
- "you can keep your deployment configuration file up to date. " +
536
- "A dev template can be found in deployments repos."
537
- route :biff, :biff
119
+ opts.on("-v", "--verbose", "Show additional output") do
120
+ @options[:verbose] = true
538
121
  end
539
-
540
- command :list_running_tasks do
541
- usage "tasks"
542
- desc "Show the list of running tasks"
543
- route :task, :list_running
122
+ opts.on("-q", "--quiet", "Suppress all output") do
123
+ Config.output = nil
544
124
  end
545
-
546
- command :list_recent_tasks do
547
- usage "tasks recent [<number>]"
548
- desc "Show <number> recent tasks"
549
- route :task, :list_recent
125
+ opts.on("-n", "--non-interactive", "Don't ask for user input") do
126
+ @options[:non_interactive] = true
127
+ Config.colorize = false
550
128
  end
551
-
552
- command :list_vms do
553
- usage "vms [<deployment>]"
554
- desc "List all VMs that supposed to be in a deployment"
555
- route :vms, :list
129
+ opts.on("-t", "--target URL", "Override target") do |target|
130
+ @options[:target] = target
556
131
  end
557
-
558
- command :cleanup do
559
- usage "cleanup"
560
- desc "Remove all but several recent stemcells and releases " +
561
- "from current director " +
562
- "(stemcells and releases currently in use are NOT deleted)"
563
- route :maintenance, :cleanup
132
+ opts.on("-u", "--user USER", "Override username") do |user|
133
+ @options[:username] = user
564
134
  end
565
-
566
- command :cloudcheck do
567
- usage "cloudcheck [<deployment>]"
568
- desc "Cloud consistency check and interactive repair"
569
- option "--auto", "resolve problems automatically " +
570
- "(not recommended for production)"
571
- option "--report", "generate report only, " +
572
- "don't attempt to resolve problems"
573
- route :cloud_check, :perform
135
+ opts.on("-p", "--password PASSWORD", "Override password") do |pass|
136
+ @options[:password] = pass
574
137
  end
575
-
576
- command :add_blob do
577
- usage "add blob <local_path> [<blob_dir>]"
578
- desc "Add a local file as BOSH blob"
579
- route :blob_management, :add
138
+ opts.on("-d", "--deployment FILE", "Override deployment") do |file|
139
+ @options[:deployment] = file
580
140
  end
581
141
 
582
- command :upload_blobs do
583
- usage "upload blobs"
584
- desc "Upload new and updated blobs to the blobstore"
585
- route :blob_management, :upload
586
- end
142
+ @args = @option_parser.order!(@args)
143
+ end
587
144
 
588
- command :sync_blobs do
589
- usage "sync blobs"
590
- desc "Sync blob with the blobstore"
591
- route :blob_management, :sync
592
- end
145
+ # Discover and load CLI plugins from all available gems
146
+ # @return [void]
147
+ def load_plugins
148
+ plugins_glob = "bosh/cli/commands/*.rb"
593
149
 
594
- command :blobs_status do
595
- usage "blobs"
596
- desc "Print current blobs status"
597
- route :blob_management, :status
150
+ unless Gem.respond_to?(:find_files)
151
+ say("Cannot load plugins, ".yellow +
152
+ "please run `gem update --system' to ".yellow +
153
+ "update your RubyGems".yellow)
154
+ return
598
155
  end
599
156
 
600
- def define_plugin_commands
601
- plugins_glob = "bosh/cli/commands/*.rb"
602
-
603
- unless Gem.respond_to?(:find_files)
604
- say("Cannot load plugins, ".yellow +
605
- "please run `gem update --system' to ".yellow +
606
- "update your RubyGems".yellow)
607
- return
608
- end
609
-
610
- plugins = begin
611
- Gem.find_files(plugins_glob, true)
612
- rescue ArgumentError
613
- # Handling rubygems compatibility issue
614
- Gem.find_files(plugins_glob)
615
- end
616
-
617
- plugins.each do |file|
618
- class_name = File.basename(file, ".rb").capitalize
619
-
620
- next if Bosh::Cli::Command.const_defined?(class_name)
621
-
622
- load file
623
-
624
- plugin = Bosh::Cli::Command.const_get(class_name)
625
-
626
- plugin.commands.each do |name, block|
627
- command(name, &block)
628
- end
629
-
630
- @plugins ||= {}
631
- @plugins[class_name] = plugin
632
- end
157
+ plugins = begin
158
+ Gem.find_files(plugins_glob, true)
159
+ rescue ArgumentError
160
+ # Handling rubygems compatibility issue
161
+ Gem.find_files(plugins_glob)
633
162
  end
634
163
 
635
- end
636
-
637
- def parse_options!
638
- opts_parser = OptionParser.new do |opts|
639
- opts.on("-c", "--config FILE") { |file| @options[:config] = file }
640
- opts.on("--cache-dir DIR") { |dir| @options[:cache_dir] = dir }
641
- opts.on("--verbose") { @options[:verbose] = true }
642
- opts.on("--no-color") { @options[:colorize] = false }
643
- opts.on("-q", "--quiet") { @options[:quiet] = true }
644
- opts.on("-s", "--skip-director-checks") do
645
- @options[:director_checks] = false
164
+ plugins.each do |plugin|
165
+ n_commands = Config.commands.size
166
+ gem_dir = Pathname.new(Gem.dir)
167
+ plugin_name = Pathname.new(plugin).relative_path_from(gem_dir)
168
+ begin
169
+ require plugin
170
+ rescue Exception => e
171
+ say("Failed to load plugin #{plugin_name}: #{e.message}".red)
646
172
  end
647
- opts.on("-n", "--non-interactive") do
648
- @options[:non_interactive] = true
649
- @options[:colorize] = false
173
+ if Config.commands.size == n_commands
174
+ say(("File #{plugin_name} has been loaded as plugin but it didn't " +
175
+ "contain any commands.\nMake sure this plugin is updated to be " +
176
+ "compatible with BOSH CLI 1.0.").columnize(80).yellow)
650
177
  end
651
- opts.on("-d", "--debug") { @options[:debug] = true }
652
- opts.on("--target URL") { |target| @options[:target] = target }
653
- opts.on("--user USER") { |user| @options[:username] = user }
654
- opts.on("--password PASSWORD") { |pass| @options[:password] = pass }
655
- opts.on("--deployment FILE") { |file| @options[:deployment] = file }
656
- opts.on("-v", "--version") { dispatch(find_command(:version)) }
657
178
  end
658
-
659
- @args = opts_parser.order!(@args)
660
179
  end
661
180
 
662
181
  def build_parse_tree
663
182
  @parse_tree = ParseTreeNode.new
664
183
 
665
- COMMANDS.each_pair do |id, command|
184
+ Config.commands.each_value do |command|
666
185
  p = @parse_tree
667
186
  n_kw = command.keywords.size
668
187
 
669
- keywords = command.keywords.each_with_index do |kw, i|
188
+ command.keywords.each_with_index do |kw, i|
670
189
  p[kw] ||= ParseTreeNode.new
671
190
  p = p[kw]
672
191
  p.command = command if i == n_kw - 1
@@ -675,81 +194,17 @@ module Bosh::Cli
675
194
  end
676
195
 
677
196
  def add_shortcuts
678
- { "st" => "status",
197
+ {
198
+ "st" => "status",
679
199
  "props" => "properties",
680
- "cck" => "cloudcheck" }.each do |short, long|
200
+ "cck" => "cloudcheck"
201
+ }.each do |short, long|
681
202
  @parse_tree[short] = @parse_tree[long]
682
203
  end
683
204
  end
684
205
 
685
- def basic_usage
686
- <<-OUT.gsub(/^\s{10}/, "")
687
- usage: bosh [--verbose] [--config|-c <FILE>] [--cache-dir <DIR]
688
- [--force] [--no-color] [--skip-director-checks] [--quiet]
689
- [--non-interactive]
690
- command [<args>]
691
- OUT
692
- end
693
-
694
- def command_usage(cmd, margin = nil)
695
- command = cmd.is_a?(Symbol) ? find_command(cmd) : cmd
696
- usage = command.usage
697
-
698
- margin ||= 2
699
- usage_width = 25
700
- desc_width = 43
701
- option_width = 10
702
-
703
- output = " " * margin
704
- output << usage.ljust(usage_width) + " "
705
- char_count = usage.size > usage_width ? 100 : 0
706
-
707
- command.description.to_s.split(/\s+/).each do |word|
708
- if char_count + word.size + 1 > desc_width # +1 accounts for space
709
- char_count = 0
710
- output << "\n" + " " * (margin + usage_width + 1)
711
- end
712
- char_count += word.size
713
- output << word << " "
714
- end
715
-
716
- command.options.each do |name, value|
717
- output << "\n" + " " * (margin + usage_width + 1)
718
- output << name.ljust(option_width) + " "
719
- # Long option name eats the whole line,
720
- # short one gives space to description
721
- char_count = name.size > option_width ? 100 : 0
722
-
723
- value.to_s.split(/\s+/).each do |word|
724
- if char_count + word.size + 1 > desc_width - option_width
725
- char_count = 0
726
- output << "\n" + " " * (margin + usage_width + option_width + 2)
727
- end
728
- char_count += word.size
729
- output << word << " "
730
- end
731
- end
732
-
733
- output
734
- end
735
-
736
- def help_message
737
- template = File.join(File.dirname(__FILE__),
738
- "templates", "help_message.erb")
739
- ERB.new(File.read(template), 4).result(binding.taint)
740
- end
741
-
742
- def plugin_help_message
743
- help = ['']
744
-
745
- @plugins.each do |class_name, plugin|
746
- help << class_name
747
- plugin.commands.keys.each do |name|
748
- help << command_usage(name)
749
- end
750
- end
751
-
752
- help.join("\n")
206
+ def usage
207
+ @option_parser.to_s
753
208
  end
754
209
 
755
210
  def search_parse_tree(node)
@@ -770,13 +225,12 @@ module Bosh::Cli
770
225
  # Tries to find best match among aliases (possibly multiple words),
771
226
  # then unwinds it onto the remaining args and searches parse tree again.
772
227
  # Not the most effective algorithm but does the job.
773
- config = Bosh::Cli::Config.new(
774
- @options[:config] || Bosh::Cli::DEFAULT_CONFIG_PATH)
228
+ config = Bosh::Cli::Config.new(@options[:config])
775
229
  candidate = []
776
230
  best_match = nil
777
231
  save_args = @args.dup
778
232
 
779
- while arg = @args.shift
233
+ while (arg = @args.shift)
780
234
  candidate << arg
781
235
  resolved = config.resolve_alias(:cli, candidate.join(" "))
782
236
  if best_match && resolved.nil?
@@ -791,41 +245,11 @@ module Bosh::Cli
791
245
  return
792
246
  end
793
247
 
794
- best_match.split(/\s+/).reverse.each do |arg|
795
- @args.unshift(arg)
248
+ best_match.split(/\s+/).reverse.each do |keyword|
249
+ @args.unshift(keyword)
796
250
  end
797
251
 
798
252
  search_parse_tree(@parse_tree)
799
253
  end
800
-
801
- def command_suggestions(args)
802
- non_keywords = args - ALL_KEYWORDS
803
-
804
- COMMANDS.values.select do |cmd|
805
- (args & cmd.keywords).size > 0 && args - cmd.keywords == non_keywords
806
- end
807
- end
808
-
809
- def unknown_command(cmd)
810
- say("Command `#{cmd}' not found.")
811
- say("Please use `bosh help' to get the list of bosh commands.")
812
- end
813
-
814
- def save_exception(e)
815
- say("BOSH CLI Error: #{e.message}".red)
816
- begin
817
- errfile = File.expand_path("~/.bosh_error")
818
- File.open(errfile, "w") do |f|
819
- f.write(e.message)
820
- f.write("\n")
821
- f.write(e.backtrace.join("\n"))
822
- end
823
- say("Error information saved in #{errfile}")
824
- rescue => e
825
- say("Error information couldn't be saved: #{e.message}")
826
- end
827
- end
828
-
829
254
  end
830
-
831
255
  end