bosh_cli 0.19.1 → 0.19.2

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.
@@ -14,6 +14,8 @@ module Bosh::Cli
14
14
  def initialize(name)
15
15
  @name = name
16
16
  @progress = 0
17
+ @start_time = nil
18
+ @finish_time = nil
17
19
  end
18
20
  end
19
21
 
@@ -28,7 +30,7 @@ module Bosh::Cli
28
30
  @out = Bosh::Cli::Config.output || $stdout
29
31
  @out.sync = true
30
32
  @buffer = StringIO.new
31
- @progress_bars = { }
33
+ @progress_bars = {}
32
34
  @pos = 0
33
35
  @time_adjustment = 0
34
36
  end
@@ -39,10 +41,17 @@ module Bosh::Cli
39
41
  end
40
42
  end
41
43
 
42
- def add_event(event)
43
- event = parse_event(event)
44
+ def add_event(event_line)
45
+ event = parse_event(event_line)
44
46
 
45
47
  @lock.synchronize do
48
+ # Handling the special "error" event
49
+ if event["error"]
50
+ done_with_stage if @current_stage
51
+ add_error(event)
52
+ return
53
+ end
54
+
46
55
  # One way to handle old stages is to prevent them
47
56
  # from appearing on screen altogether. That means
48
57
  # that we can always render the current stage only
@@ -53,6 +62,7 @@ module Bosh::Cli
53
62
 
54
63
  tags = event["tags"].is_a?(Array) ? event["tags"] : []
55
64
  stage_header = event["stage"]
65
+
56
66
  if tags.size > 0
57
67
  stage_header += " " + tags.sort.join(", ").green
58
68
  end
@@ -82,6 +92,7 @@ module Bosh::Cli
82
92
  @done_tasks = []
83
93
 
84
94
  @eta = nil
95
+ @stage_has_error = false # Error flag
85
96
  # Tracks max_in_flight best guess
86
97
  @tasks_batch_size = 0
87
98
  @batches_count = 0
@@ -102,6 +113,19 @@ module Bosh::Cli
102
113
  end
103
114
  end
104
115
 
116
+ def add_error(event)
117
+ error = event["error"] || {}
118
+ code = error["code"]
119
+ message = error["message"]
120
+
121
+ error = "Error"
122
+ error += " #{code}" if code
123
+ error += ": #{message}" if message
124
+
125
+ # TODO: add KB article link and maybe cck reference?
126
+ @buffer.puts("\n" + error.red)
127
+ end
128
+
105
129
  def refresh
106
130
  # This is primarily used to refresh timer
107
131
  # without advancing rendering buffer
@@ -139,13 +163,19 @@ module Bosh::Cli
139
163
  @buffer.print "\n#{@current_stage}\n"
140
164
  end
141
165
 
142
- def done_with_stage(state = "done")
166
+ def done_with_stage(state = nil)
167
+ return unless @in_progress
168
+
143
169
  if @last_event
144
170
  completion_time = Time.at(@last_event["time"]) rescue Time.now
145
171
  else
146
172
  completion_time = Time.now
147
173
  end
148
174
 
175
+ if state.nil?
176
+ state = @stage_has_error ? "error" : "done"
177
+ end
178
+
149
179
  case state.to_s
150
180
  when "done"
151
181
  progress_bar.title = "Done".green
@@ -170,6 +200,8 @@ module Bosh::Cli
170
200
  # We have to trust the first event in each stage
171
201
  # to have correct "total" and "current" fields.
172
202
  def append_event(event)
203
+ validate_event(event)
204
+
173
205
  progress = 0
174
206
  total = event["total"].to_i
175
207
 
@@ -242,6 +274,7 @@ module Bosh::Cli
242
274
  if event["state"] == "failed"
243
275
  # TODO: truncate?
244
276
  status = [task_name.red, event_data["error"]].compact.join(": ")
277
+ @stage_has_error = true
245
278
  else
246
279
  status = task_name.yellow
247
280
  end
@@ -259,7 +292,7 @@ module Bosh::Cli
259
292
  task.progress = progress
260
293
 
261
294
  progress_bar.total = total
262
- progress_bar.title = @tasks.values.map {|t| t.name }.sort.join(", ")
295
+ progress_bar.title = @tasks.values.map { |t| t.name }.sort.join(", ")
263
296
 
264
297
  progress_bar.current += progress_bar_gain
265
298
  progress_bar.refresh
@@ -269,19 +302,22 @@ module Bosh::Cli
269
302
 
270
303
  def parse_event(event_line)
271
304
  event = JSON.parse(event_line)
272
-
273
- if event["time"] && event["stage"] && event["task"] &&
274
- event["index"] && event["total"] && event["state"]
275
- event
276
- else
277
- raise InvalidEvent, "Invalid event structure: stage, time, task, " +
278
- "index, total, state are all required"
305
+ unless event.kind_of?(Hash)
306
+ raise InvalidEvent, "Hash expected, #{event.class} given"
279
307
  end
280
-
308
+ event
281
309
  rescue JSON::JSONError
282
310
  raise InvalidEvent, "Cannot parse event, invalid JSON"
283
311
  end
284
312
 
313
+ def validate_event(event)
314
+ unless event["time"] && event["stage"] && event["task"] &&
315
+ event["index"] && event["total"] && event["state"]
316
+ raise InvalidEvent, "Invalid event structure: stage, time, task, " +
317
+ "index, total, state are all required"
318
+ end
319
+ end
320
+
285
321
  # Expects time and eta to be adjusted
286
322
  def time_with_eta(time, eta)
287
323
  time_fmt = format_time(time)
@@ -10,13 +10,14 @@ module Bosh::Cli
10
10
  new(manifest_file, blobstore).compile
11
11
  end
12
12
 
13
- def initialize(manifest_file, blobstore,
14
- remote_release = nil, release_dir = nil)
13
+ def initialize(manifest_file, blobstore, remote_jobs = nil,
14
+ remote_packages_sha1 = [], release_dir = nil)
15
15
  @build_dir = Dir.mktmpdir
16
16
  @jobs_dir = File.join(@build_dir, "jobs")
17
17
  @packages_dir = File.join(@build_dir, "packages")
18
18
  @blobstore = blobstore
19
19
  @release_dir = release_dir || Dir.pwd
20
+ @remote_packages_sha1 = remote_packages_sha1
20
21
 
21
22
  at_exit { FileUtils.rm_rf(@build_dir) }
22
23
 
@@ -26,15 +27,11 @@ module Bosh::Cli
26
27
  @manifest_file = File.expand_path(manifest_file, @release_dir)
27
28
  @manifest = load_yaml_file(manifest_file)
28
29
 
29
- if remote_release
30
- @remote_packages = remote_release["packages"].map do |pkg|
31
- OpenStruct.new(pkg)
32
- end
33
- @remote_jobs = remote_release["jobs"].map do |job|
30
+ if remote_jobs
31
+ @remote_jobs = remote_jobs.map do |job|
34
32
  OpenStruct.new(job)
35
33
  end
36
34
  else
37
- @remote_packages = []
38
35
  @remote_jobs = []
39
36
  end
40
37
 
@@ -56,7 +53,7 @@ module Bosh::Cli
56
53
  header("Copying packages")
57
54
  @packages.each do |package|
58
55
  say("#{package.name} (#{package.version})".ljust(30), " ")
59
- if remote_object_exists?(@remote_packages, package)
56
+ if @remote_packages_sha1.any? { |sha1| sha1 == package.sha1 }
60
57
  say("SKIP".yellow)
61
58
  next
62
59
  end
@@ -24,11 +24,19 @@ module Bosh::Cli
24
24
  File.exists?(@tarball_path) && File.readable?(@tarball_path)
25
25
  end
26
26
 
27
+ def manifest
28
+ return nil unless valid?
29
+ unpack
30
+ File.read(File.join(@unpack_dir, "release.MF"))
31
+ end
32
+
27
33
  # Repacks tarball according to the structure of remote release
28
34
  # Return path to repackaged tarball or nil if repack has failed
29
- def repack(remote_release)
35
+ def repack(remote_jobs = nil, remote_packages_sha1 = nil)
30
36
  return nil unless valid?
31
37
  unpack
38
+ remote_jobs ||= []
39
+ remote_packages_sha1 ||= []
32
40
 
33
41
  tmpdir = Dir.mktmpdir
34
42
  repacked_path = File.join(tmpdir, "release-repack.tgz")
@@ -39,16 +47,13 @@ module Bosh::Cli
39
47
 
40
48
  local_packages = manifest["packages"]
41
49
  local_jobs = manifest["jobs"]
42
- remote_packages = remote_release["packages"]
43
- remote_jobs = remote_release["jobs"]
44
50
 
45
51
  @skipped = 0
46
52
 
47
53
  Dir.chdir(@unpack_dir) do
48
54
  local_packages.each do |package|
49
55
  say("#{package['name']} (#{package['version']})".ljust(30), " ")
50
- if remote_packages.any? { |rp| package["name"] == rp["name"] &&
51
- package["version"].to_s == rp["version"].to_s }
56
+ if remote_packages_sha1.any? { |sha1| sha1 == package["sha1"] }
52
57
  say("SKIP".green)
53
58
  @skipped += 1
54
59
  FileUtils.rm_rf(File.join("packages", "#{package['name']}.tgz"))
@@ -25,7 +25,6 @@ module Bosh::Cli
25
25
  end
26
26
 
27
27
  def initialize(args)
28
- define_commands
29
28
  @args = args
30
29
  @options = {
31
30
  :director_checks => true,
@@ -68,9 +67,18 @@ module Bosh::Cli
68
67
  end
69
68
 
70
69
  runner.send(@action.to_sym, *@args)
70
+ exit(runner.exit_code)
71
71
  elsif @args.empty? || @args == %w(help)
72
72
  say(help_message)
73
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")
81
+ exit(0)
74
82
  elsif @args[0] == "help"
75
83
  cmd_args = @args[1..-1]
76
84
  suggestions = command_suggestions(cmd_args).map do |cmd|
@@ -116,6 +124,33 @@ module Bosh::Cli
116
124
  end
117
125
  end
118
126
 
127
+ # looks for command completion in the parse tree
128
+ def parse_tree_completion(node, words, index)
129
+ word = words[index]
130
+
131
+ # exact match and not on the last word
132
+ if node[word] && words.length != index
133
+ parse_tree_completion(node[word], words, index + 1)
134
+
135
+ # exact match at the last word
136
+ elsif node[word]
137
+ node[word].values
138
+
139
+ # find all partial matches
140
+ else
141
+ node.keys.grep(/^#{word}/)
142
+ end
143
+ end
144
+
145
+ # for use with:
146
+ # complete -C 'bosh complete' bosh
147
+ # @param [String] 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
+
119
154
  def command(name, &block)
120
155
  cmd_def = CommandDefinition.new
121
156
  cmd_def.instance_eval(&block)
@@ -6,7 +6,7 @@ module Bosh::Cli
6
6
  def self.create_for_log_type(log_type)
7
7
  if log_type == "event"
8
8
  EventLogRenderer.new
9
- elsif log_type == "result"
9
+ elsif log_type == "result" || log_type == "none"
10
10
  # Null renderer doesn't output anything to screen, so it fits well
11
11
  # in case we need to fetch task result log only, without rendering it
12
12
  NullRenderer.new
@@ -18,7 +18,10 @@ module Bosh
18
18
  @task_id = task_id
19
19
  @options = options
20
20
 
21
- @log_type = options[:log_type] || "event"
21
+ @quiet = !!options[:quiet]
22
+ default_log_type = @quiet ? "none" : "event"
23
+
24
+ @log_type = options[:log_type] || default_log_type
22
25
  @use_cache = options.key?(:use_cache) ? @options[:use_cache] : true
23
26
 
24
27
  @output = nil
@@ -51,11 +54,7 @@ module Bosh
51
54
  task_status = poll
52
55
  end
53
56
 
54
- if task_status == :error && interactive? && @log_type != "debug"
55
- prompt_for_debug_log
56
- else
57
- print_task_summary(task_status)
58
- end
57
+ print_task_summary(task_status)
59
58
 
60
59
  save_task_output unless cached_output
61
60
  task_status
@@ -133,6 +132,14 @@ module Bosh
133
132
 
134
133
  private
135
134
 
135
+ def nl
136
+ super unless @quiet
137
+ end
138
+
139
+ def say(*args)
140
+ super unless @quiet
141
+ end
142
+
136
143
  # @param [String] output Output received from director task
137
144
  def output_received(output)
138
145
  return if output.nil?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bosh
4
4
  module Cli
5
- VERSION = "0.19.1"
5
+ VERSION = "0.19.2"
6
6
  end
7
7
  end
@@ -24,7 +24,7 @@ describe String do
24
24
  end
25
25
  end
26
26
 
27
- it "has colorization helpers" do
27
+ it "has colorization helpers (but only if tty)" do
28
28
  Bosh::Cli::Config.colorize = false
29
29
  "string".red.should == "string"
30
30
  "string".green.should == "string"
@@ -32,10 +32,14 @@ describe String do
32
32
  "string".colorize(:green).should == "string"
33
33
 
34
34
  Bosh::Cli::Config.colorize = true
35
+ Bosh::Cli::Config.output.stub(:tty?).and_return(true)
35
36
  "string".red.should == "\e[0m\e[31mstring\e[0m"
36
37
  "string".green.should == "\e[0m\e[32mstring\e[0m"
37
38
  "string".colorize("a").should == "string"
38
39
  "string".colorize(:green).should == "\e[0m\e[32mstring\e[0m"
40
+
41
+ Bosh::Cli::Config.output.stub(:tty?).and_return(false)
42
+ "string".green.should == "string"
39
43
  end
40
44
  end
41
45
 
@@ -61,7 +61,8 @@ describe Bosh::Cli::Director do
61
61
 
62
62
  it "uploads stemcell" do
63
63
  @director.should_receive(:upload_and_track).
64
- with("/stemcells", "application/x-compressed", "/path").
64
+ with(:post, "/stemcells", "/path",
65
+ {:content_type => "application/x-compressed"}).
65
66
  and_return(true)
66
67
  @director.upload_stemcell("/path")
67
68
  end
@@ -117,7 +118,8 @@ describe Bosh::Cli::Director do
117
118
 
118
119
  it "uploads release" do
119
120
  @director.should_receive(:upload_and_track).
120
- with("/releases", "application/x-compressed", "/path").
121
+ with(:post, "/releases", "/path",
122
+ {:content_type => "application/x-compressed"}).
121
123
  and_return(true)
122
124
  @director.upload_release("/path")
123
125
  end
@@ -138,45 +140,49 @@ describe Bosh::Cli::Director do
138
140
 
139
141
  it "deletes stemcell" do
140
142
  @director.should_receive(:request_and_track).
141
- with(:delete, "/stemcells/ubuntu/123").and_return(true)
143
+ with(:delete, "/stemcells/ubuntu/123", {}).and_return(true)
142
144
  @director.delete_stemcell("ubuntu", "123")
143
145
  end
144
146
 
145
147
  it "deletes deployment" do
146
148
  @director.should_receive(:request_and_track).
147
- with(:delete, "/deployments/foo").and_return(true)
149
+ with(:delete, "/deployments/foo", {}).and_return(true)
148
150
  @director.delete_deployment("foo")
149
151
  end
150
152
 
151
153
  it "deletes release (non-force)" do
152
154
  @director.should_receive(:request_and_track).
153
- with(:delete, "/releases/za").and_return(true)
155
+ with(:delete, "/releases/za", {}).and_return(true)
154
156
  @director.delete_release("za")
155
157
  end
156
158
 
157
159
  it "deletes release (force)" do
158
160
  @director.should_receive(:request_and_track).
159
- with(:delete, "/releases/zb?force=true").and_return(true)
161
+ with(:delete, "/releases/zb?force=true", {}).and_return(true)
160
162
  @director.delete_release("zb", :force => true)
161
163
  end
162
164
 
163
165
  it "deploys" do
164
166
  @director.should_receive(:request_and_track).
165
- with(:post, "/deployments", "text/yaml", "manifest").and_return(true)
167
+ with(:post, "/deployments",
168
+ {:content_type => "text/yaml", :payload => "manifest"}).
169
+ and_return(true)
166
170
  @director.deploy("manifest")
167
171
  end
168
172
 
169
173
  it "changes job state" do
170
174
  @director.should_receive(:request_and_track).
171
175
  with(:put, "/deployments/foo/jobs/dea?state=stopped",
172
- "text/yaml", "manifest").and_return(true)
176
+ {:content_type => "text/yaml", :payload =>"manifest"}).
177
+ and_return(true)
173
178
  @director.change_job_state("foo", "manifest", "dea", nil, "stopped")
174
179
  end
175
180
 
176
181
  it "changes job instance state" do
177
182
  @director.should_receive(:request_and_track).
178
183
  with(:put, "/deployments/foo/jobs/dea/0?state=detached",
179
- "text/yaml", "manifest").and_return(true)
184
+ {:content_type => "text/yaml", :payload => "manifest"}).
185
+ and_return(true)
180
186
  @director.change_job_state("foo", "manifest", "dea", 0, "detached")
181
187
  end
182
188
 
@@ -255,9 +261,12 @@ describe Bosh::Cli::Director do
255
261
  with(@director, "502", options).
256
262
  and_return(tracker)
257
263
 
258
- @director.request_and_track(:get, "/stuff", "text/plain",
259
- "abc", options).
260
- should == ["polling result", "502", "foo"]
264
+ @director.request_and_track(:get, "/stuff",
265
+ {:content_type => "text/plain",
266
+ :payload => "abc",
267
+ :arg1 => 1, :arg2 => 2
268
+ }).
269
+ should == ["polling result", "502"]
261
270
  end
262
271
 
263
272
  it "considers all responses but 302 a failure" do
@@ -265,9 +274,12 @@ describe Bosh::Cli::Director do
265
274
  @director.should_receive(:request).
266
275
  with(:get, "/stuff", "text/plain", "abc").
267
276
  and_return([code, "body", {}])
268
- @director.request_and_track(:get, "/stuff", "text/plain",
269
- "abc", :arg1 => 1, :arg2 => 2).
270
- should == [:failed, nil, nil]
277
+ @director.request_and_track(:get, "/stuff",
278
+ {:content_type => "text/plain",
279
+ :payload => "abc",
280
+ :arg1 => 1, :arg2 => 2
281
+ }).
282
+ should == [:failed, nil]
271
283
  end
272
284
  end
273
285
 
@@ -275,9 +287,12 @@ describe Bosh::Cli::Director do
275
287
  @director.should_receive(:request).
276
288
  with(:get, "/stuff", "text/plain", "abc").
277
289
  and_return([302, "body", { :location => "/track-task/502" }])
278
- @director.request_and_track(:get, "/stuff", "text/plain",
279
- "abc", :arg1 => 1, :arg2 => 2).
280
- should == [:non_trackable, nil, nil]
290
+ @director.request_and_track(:get, "/stuff",
291
+ {:content_type => "text/plain",
292
+ :payload => "abc",
293
+ :arg1 => 1, :arg2 => 2
294
+ }).
295
+ should == [:non_trackable, nil]
281
296
  end
282
297
 
283
298
  it "supports uploading with progress bar" do
@@ -286,8 +301,10 @@ describe Bosh::Cli::Director do
286
301
 
287
302
  Bosh::Cli::FileWithProgressBar.stub!(:open).with(file, "r").and_return(f)
288
303
  @director.should_receive(:request_and_track).
289
- with(:post, "/stuff", "application/x-compressed", f, { })
290
- @director.upload_and_track("/stuff", "application/x-compressed", file)
304
+ with(:put, "/stuff", {:content_type => "application/x-compressed",
305
+ :payload => f})
306
+ @director.upload_and_track(:put, "/stuff", file,
307
+ :content_type => "application/x-compressed")
291
308
  f.progress_bar.finished?.should be_true
292
309
  end
293
310
  end
@@ -348,7 +365,7 @@ describe Bosh::Cli::Director do
348
365
  @director.request(:get, "/stuff", "application/octet-stream",
349
366
  "payload", { :hdr1 => "a", :hdr2 => "b"})
350
367
  }.should raise_error(Bosh::Cli::DirectorError,
351
- "Director error 40422: Weird stuff happened")
368
+ "Error 40422: Weird stuff happened")
352
369
 
353
370
  lambda {
354
371
  # Not JSON
@@ -360,7 +377,7 @@ describe Bosh::Cli::Director do
360
377
  @director.request(:get, "/stuff", "application/octet-stream",
361
378
  "payload", { :hdr1 => "a", :hdr2 => "b"})
362
379
  }.should raise_error(Bosh::Cli::DirectorError,
363
- "Director error (HTTP #{code}): " +
380
+ "HTTP #{code}: " +
364
381
  "error message goes here")
365
382
 
366
383
  lambda {
@@ -373,7 +390,7 @@ describe Bosh::Cli::Director do
373
390
  @director.request(:get, "/stuff", "application/octet-stream",
374
391
  "payload", { :hdr1 => "a", :hdr2 => "b"})
375
392
  }.should raise_error(Bosh::Cli::DirectorError,
376
- "Director error (HTTP #{code}): " +
393
+ "HTTP #{code}: " +
377
394
  %Q[{"c":"d","a":"b"}])
378
395
  end
379
396
  end