bosh_cli 0.19.1 → 0.19.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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