bosh_cli 0.19.6 → 1.0.rc1

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.
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