bosh_cli 0.16

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 (113) hide show
  1. data/README +4 -0
  2. data/Rakefile +55 -0
  3. data/bin/bosh +17 -0
  4. data/lib/cli.rb +76 -0
  5. data/lib/cli/cache.rb +44 -0
  6. data/lib/cli/changeset_helper.rb +142 -0
  7. data/lib/cli/command_definition.rb +52 -0
  8. data/lib/cli/commands/base.rb +245 -0
  9. data/lib/cli/commands/biff.rb +300 -0
  10. data/lib/cli/commands/blob.rb +125 -0
  11. data/lib/cli/commands/cloudcheck.rb +169 -0
  12. data/lib/cli/commands/deployment.rb +147 -0
  13. data/lib/cli/commands/job.rb +42 -0
  14. data/lib/cli/commands/job_management.rb +117 -0
  15. data/lib/cli/commands/log_management.rb +81 -0
  16. data/lib/cli/commands/maintenance.rb +131 -0
  17. data/lib/cli/commands/misc.rb +240 -0
  18. data/lib/cli/commands/package.rb +112 -0
  19. data/lib/cli/commands/property_management.rb +125 -0
  20. data/lib/cli/commands/release.rb +469 -0
  21. data/lib/cli/commands/ssh.rb +271 -0
  22. data/lib/cli/commands/stemcell.rb +184 -0
  23. data/lib/cli/commands/task.rb +213 -0
  24. data/lib/cli/commands/user.rb +28 -0
  25. data/lib/cli/commands/vms.rb +53 -0
  26. data/lib/cli/config.rb +154 -0
  27. data/lib/cli/core_ext.rb +145 -0
  28. data/lib/cli/dependency_helper.rb +62 -0
  29. data/lib/cli/deployment_helper.rb +263 -0
  30. data/lib/cli/deployment_manifest_compiler.rb +28 -0
  31. data/lib/cli/director.rb +633 -0
  32. data/lib/cli/director_task.rb +64 -0
  33. data/lib/cli/errors.rb +48 -0
  34. data/lib/cli/event_log_renderer.rb +351 -0
  35. data/lib/cli/job_builder.rb +226 -0
  36. data/lib/cli/package_builder.rb +254 -0
  37. data/lib/cli/packaging_helper.rb +248 -0
  38. data/lib/cli/release.rb +176 -0
  39. data/lib/cli/release_builder.rb +215 -0
  40. data/lib/cli/release_compiler.rb +178 -0
  41. data/lib/cli/release_tarball.rb +272 -0
  42. data/lib/cli/runner.rb +771 -0
  43. data/lib/cli/stemcell.rb +83 -0
  44. data/lib/cli/task_log_renderer.rb +40 -0
  45. data/lib/cli/templates/help_message.erb +75 -0
  46. data/lib/cli/validation.rb +42 -0
  47. data/lib/cli/version.rb +7 -0
  48. data/lib/cli/version_calc.rb +48 -0
  49. data/lib/cli/versions_index.rb +126 -0
  50. data/lib/cli/yaml_helper.rb +62 -0
  51. data/spec/assets/biff/bad_gateway_config.yml +28 -0
  52. data/spec/assets/biff/good_simple_config.yml +63 -0
  53. data/spec/assets/biff/good_simple_golden_config.yml +63 -0
  54. data/spec/assets/biff/good_simple_template.erb +69 -0
  55. data/spec/assets/biff/multiple_subnets_config.yml +40 -0
  56. data/spec/assets/biff/network_only_template.erb +34 -0
  57. data/spec/assets/biff/no_cc_config.yml +27 -0
  58. data/spec/assets/biff/no_range_config.yml +27 -0
  59. data/spec/assets/biff/no_subnet_config.yml +16 -0
  60. data/spec/assets/biff/ok_network_config.yml +30 -0
  61. data/spec/assets/biff/properties_template.erb +6 -0
  62. data/spec/assets/deployment.MF +0 -0
  63. data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
  64. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
  65. data/spec/assets/release/jobs/cacher.tgz +0 -0
  66. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  67. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  68. data/spec/assets/release/jobs/cacher/job.MF +6 -0
  69. data/spec/assets/release/jobs/cacher/monit +1 -0
  70. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  71. data/spec/assets/release/jobs/cleaner/job.MF +4 -0
  72. data/spec/assets/release/jobs/cleaner/monit +1 -0
  73. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  74. data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
  75. data/spec/assets/release/jobs/sweeper/job.MF +5 -0
  76. data/spec/assets/release/jobs/sweeper/monit +1 -0
  77. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  78. data/spec/assets/release/packages/stuff.tgz +0 -0
  79. data/spec/assets/release/release.MF +17 -0
  80. data/spec/assets/release_invalid_checksum.tgz +0 -0
  81. data/spec/assets/release_invalid_jobs.tgz +0 -0
  82. data/spec/assets/release_no_name.tgz +0 -0
  83. data/spec/assets/release_no_version.tgz +0 -0
  84. data/spec/assets/stemcell/image +1 -0
  85. data/spec/assets/stemcell/stemcell.MF +6 -0
  86. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  87. data/spec/assets/stemcell_no_image.tgz +0 -0
  88. data/spec/assets/valid_release.tgz +0 -0
  89. data/spec/assets/valid_stemcell.tgz +0 -0
  90. data/spec/spec_helper.rb +25 -0
  91. data/spec/unit/base_command_spec.rb +66 -0
  92. data/spec/unit/biff_spec.rb +135 -0
  93. data/spec/unit/cache_spec.rb +36 -0
  94. data/spec/unit/cli_commands_spec.rb +481 -0
  95. data/spec/unit/config_spec.rb +139 -0
  96. data/spec/unit/core_ext_spec.rb +77 -0
  97. data/spec/unit/dependency_helper_spec.rb +52 -0
  98. data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
  99. data/spec/unit/director_spec.rb +511 -0
  100. data/spec/unit/director_task_spec.rb +48 -0
  101. data/spec/unit/event_log_renderer_spec.rb +171 -0
  102. data/spec/unit/hash_changeset_spec.rb +73 -0
  103. data/spec/unit/job_builder_spec.rb +454 -0
  104. data/spec/unit/package_builder_spec.rb +567 -0
  105. data/spec/unit/release_builder_spec.rb +65 -0
  106. data/spec/unit/release_spec.rb +66 -0
  107. data/spec/unit/release_tarball_spec.rb +33 -0
  108. data/spec/unit/runner_spec.rb +140 -0
  109. data/spec/unit/ssh_spec.rb +78 -0
  110. data/spec/unit/stemcell_spec.rb +17 -0
  111. data/spec/unit/version_calc_spec.rb +27 -0
  112. data/spec/unit/versions_index_spec.rb +132 -0
  113. metadata +338 -0
@@ -0,0 +1,64 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh
4
+ module Cli
5
+ class DirectorTask
6
+
7
+ attr_accessor :offset
8
+
9
+ def initialize(director, task_id, log_type = nil)
10
+ @director = director
11
+ @task_id = task_id
12
+ @offset = 0
13
+ @log_type = log_type
14
+ @buf = ""
15
+ end
16
+
17
+ def state
18
+ @director.get_task_state(@task_id)
19
+ end
20
+
21
+ def result
22
+ @director.get_task_result(@task_id)
23
+ end
24
+
25
+ def output
26
+ body, new_offset = @director.get_task_output(@task_id, @offset,
27
+ @log_type)
28
+
29
+ @buf << body if body
30
+
31
+ if new_offset
32
+ @offset = new_offset
33
+ else
34
+ return flush_output
35
+ end
36
+
37
+ last_nl = @buf.rindex("\n")
38
+
39
+ if !last_nl
40
+ result = nil
41
+ elsif last_nl != @buf.size - 1
42
+ result = @buf[0..last_nl]
43
+ @buf = @buf[last_nl+1..-1]
44
+ else
45
+ result = @buf
46
+ @buf = ""
47
+ end
48
+
49
+ result
50
+ end
51
+
52
+ def flush_output
53
+ out = @buf
54
+ @buf = ""
55
+ out.blank? ? nil : "#{out}\n"
56
+ end
57
+
58
+ def cancel
59
+ @director.cancel_task(@task_id)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ class CliError < StandardError
5
+ attr_reader :exit_code
6
+
7
+ def initialize(*args)
8
+ @exit_code = 1
9
+ super(*args)
10
+ end
11
+
12
+ def self.error_code(code = nil)
13
+ define_method(:error_code) { code }
14
+ end
15
+
16
+ def self.exit_code(code = nil)
17
+ define_method(:exit_code) { code }
18
+ end
19
+ end
20
+
21
+ class UnknownCommand < CliError; error_code(100); end
22
+ class ConfigError < CliError; error_code(101); end
23
+ class DirectorMissing < CliError; error_code(102); end
24
+ class DirectorInaccessible < CliError; error_code(103); end
25
+
26
+ class DirectorError < CliError; error_code(201); end
27
+ class AuthError < DirectorError; error_code(202); end
28
+ class MissingTask < DirectorError; error_code(203); end
29
+ class TaskTrackError < DirectorError; error_code(204); end
30
+ class DeploymentNotFound < DirectorError; error_code(205); end
31
+
32
+ class CliExit < CliError; error_code(400); end
33
+ class GracefulExit < CliExit; error_code(401); end
34
+
35
+ class CacheDirectoryError < CliError; error_code(301); end
36
+
37
+ class InvalidPackage < CliError; error_code(500); end
38
+ class InvalidJob < CliError; error_code(501); end
39
+ class InvalidRelease < CliError; error_code(503); end
40
+ class MissingDependency < CliError; error_code(504); end
41
+ class CircularDependency < CliError; error_code(505); end
42
+ class InvalidIndex < CliError; error_code(506); end
43
+ class BlobstoreError < CliError; error_code(507); end
44
+ class PackagingError < CliError; error_code(508); end
45
+ class UndefinedProperty < CliError; error_code(509); end
46
+ class MalformedManifest < CliError; error_code(511); end
47
+ class MissingTarget < CliError; error_code(512); end
48
+ end
@@ -0,0 +1,351 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ class EventLogRenderer < TaskLogRenderer
5
+
6
+ class InvalidEvent < StandardError; end
7
+
8
+ class Task
9
+ attr_accessor :name
10
+ attr_accessor :progress
11
+ attr_accessor :start_time
12
+ attr_accessor :finish_time
13
+
14
+ def initialize(name)
15
+ @name = name
16
+ @progress = 0
17
+ end
18
+ end
19
+
20
+ attr_reader :current_stage
21
+ attr_reader :events_count
22
+ attr_reader :started_at, :finished_at
23
+
24
+ def initialize
25
+ @lock = Monitor.new
26
+ @events_count = 0
27
+ @seen_stages = Set.new
28
+ @out = Bosh::Cli::Config.output || $stdout
29
+ @out.sync = true
30
+ @buffer = StringIO.new
31
+ @progress_bars = { }
32
+ @pos = 0
33
+ @time_adjustment = 0
34
+ end
35
+
36
+ def add_output(output)
37
+ output.to_s.split("\n").each do |line|
38
+ add_event(line)
39
+ end
40
+ end
41
+
42
+ def add_event(event)
43
+ event = parse_event(event)
44
+
45
+ @lock.synchronize do
46
+ # One way to handle old stages is to prevent them
47
+ # from appearing on screen altogether. That means
48
+ # that we can always render the current stage only
49
+ # and that simplifies housekeeping around progress
50
+ # bars and messages. However we could always support
51
+ # resuming the older stages rendering if we feel
52
+ # that it's valuable.
53
+
54
+ tags = event["tags"].is_a?(Array) ? event["tags"] : []
55
+ stage_header = event["stage"]
56
+ if tags.size > 0
57
+ stage_header += " " + tags.sort.join(", ").green
58
+ end
59
+
60
+ unless @seen_stages.include?(stage_header)
61
+ done_with_stage if @current_stage
62
+ begin_stage(event, stage_header)
63
+ end
64
+
65
+ if @current_stage == stage_header
66
+ append_event(event)
67
+ end
68
+ end
69
+
70
+ rescue InvalidEvent => e
71
+ # Swallow for the moment
72
+ end
73
+
74
+ def begin_stage(event, header)
75
+ @current_stage = header
76
+ @seen_stages << @current_stage
77
+
78
+ @stage_start_time = Time.at(event["time"]) rescue Time.now
79
+ @local_start_time = adjusted_time(@stage_start_time)
80
+
81
+ @tasks = {}
82
+ @done_tasks = []
83
+
84
+ @eta = nil
85
+ # Tracks max_in_flight best guess
86
+ @tasks_batch_size = 0
87
+ @batches_count = 0
88
+
89
+ # Running average of task completion time
90
+ @running_avg = 0
91
+
92
+ append_stage_header
93
+ end
94
+
95
+ def render
96
+ @lock.synchronize do
97
+ @buffer.seek(@pos)
98
+ output = @buffer.read
99
+ @out.print output
100
+ @pos = @buffer.tell
101
+ output
102
+ end
103
+ end
104
+
105
+ def refresh
106
+ # This is primarily used to refresh timer
107
+ # without advancing rendering buffer
108
+ @lock.synchronize do
109
+ if @in_progress
110
+ progress_bar.label = time_with_eta(Time.now - @local_start_time, @eta)
111
+ progress_bar.refresh
112
+ end
113
+ render
114
+ end
115
+ end
116
+
117
+ def finish(state)
118
+ return if @events_count == 0
119
+
120
+ @lock.synchronize do
121
+ @done = true
122
+ done_with_stage(state)
123
+ render
124
+ end
125
+ end
126
+
127
+ def duration
128
+ return nil if @started_at.nil? || @finished_at.nil?
129
+ @finished_at - @started_at
130
+ end
131
+
132
+ private
133
+
134
+ def append_stage_header
135
+ @buffer.print "\n#{@current_stage}\n"
136
+ end
137
+
138
+ def done_with_stage(state = "done")
139
+ if @last_event
140
+ completion_time = Time.at(@last_event["time"]) rescue Time.now
141
+ else
142
+ completion_time = Time.now
143
+ end
144
+
145
+ case state.to_s
146
+ when "done"
147
+ progress_bar.title = "Done".green
148
+ progress_bar.finished_steps = progress_bar.total
149
+ when "error"
150
+ progress_bar.title = "Error".red
151
+ else
152
+ progress_bar.title = "Not done".yellow
153
+ end
154
+
155
+ progress_bar.bar_visible = false
156
+ progress_bar.label = format_time(completion_time - @stage_start_time)
157
+ progress_bar.refresh
158
+ @buffer.print "\n"
159
+ @in_progress = false
160
+ end
161
+
162
+ def progress_bar
163
+ @progress_bars[@current_stage] ||= StageProgressBar.new(@buffer)
164
+ end
165
+
166
+ # We have to trust the first event in each stage
167
+ # to have correct "total" and "current" fields.
168
+ def append_event(event)
169
+ progress = 0
170
+ total = event["total"].to_i
171
+
172
+ if event["state"] == "started"
173
+ task = Task.new(event["task"])
174
+ else
175
+ task = @tasks[event["index"]]
176
+ end
177
+
178
+ event_data = event["data"] || {}
179
+ # Ignoring out-of-order events
180
+ return if task.nil?
181
+
182
+ @events_count += 1
183
+ @last_event = event
184
+
185
+ case event["state"]
186
+ when "started"
187
+ begin
188
+ task.start_time = Time.at(event["time"])
189
+ # Treat first "started" event as task start time
190
+ @started_at = task.start_time if @started_at.nil?
191
+ rescue
192
+ task.start_time = Time.now
193
+ end
194
+
195
+ task.progress = 0
196
+
197
+ @tasks[event["index"]] = task
198
+
199
+ if @tasks.size > @tasks_batch_size
200
+ # Heuristics here: we assume that local maximum of
201
+ # tasks number is a "max_in_flight" value and batches count
202
+ # should only be recalculated once we refresh this maximum.
203
+ # It's unlikely that the first task in a batch will be finished
204
+ # before the last one is started so @done_tasks is expected
205
+ # to only have canaries.
206
+ @tasks_batch_size = @tasks.size
207
+ @non_canary_event_start_time = task.start_time
208
+ @batches_count = ((total - @done_tasks.size) /
209
+ @tasks_batch_size.to_f).ceil
210
+ end
211
+ when "finished", "failed"
212
+ @tasks.delete(event["index"])
213
+ @done_tasks << task
214
+
215
+ begin
216
+ task.finish_time = @finished_at = Time.at(event["time"])
217
+ rescue
218
+ task.finish_time = Time.now
219
+ end
220
+
221
+ task_time = task.finish_time - task.start_time
222
+
223
+ n_done_tasks = @done_tasks.size.to_f
224
+ @running_avg = @running_avg * (n_done_tasks - 1) / n_done_tasks +
225
+ task_time.to_f / n_done_tasks
226
+
227
+ progress = 1
228
+ progress_bar.finished_steps += 1
229
+ progress_bar.label = time_with_eta(task_time, @eta)
230
+
231
+ progress_bar.clear_line
232
+
233
+ task_name = task.name.to_s
234
+ if task_name !~ /^[A-Z]{2}/
235
+ task_name = task_name[0..0].to_s.downcase + task_name[1..-1].to_s
236
+ end
237
+
238
+ if event["state"] == "failed"
239
+ # TODO: truncate?
240
+ status = [task_name.red, event_data["error"]].compact.join(": ")
241
+ else
242
+ status = task_name.yellow
243
+ end
244
+ @buffer.puts(" #{status} (#{format_time(task_time)})")
245
+ when "in_progress"
246
+ progress = [event["progress"].to_f / 100, 1].min
247
+ end
248
+
249
+ if @batches_count > 0 && @non_canary_event_start_time
250
+ @eta = adjusted_time(@non_canary_event_start_time +
251
+ @running_avg * @batches_count)
252
+ end
253
+
254
+ progress_bar_gain = progress - task.progress
255
+ task.progress = progress
256
+
257
+ progress_bar.total = total
258
+ progress_bar.title = @tasks.values.map {|t| t.name }.sort.join(", ")
259
+
260
+ progress_bar.current += progress_bar_gain
261
+ progress_bar.refresh
262
+
263
+ @in_progress = true
264
+ end
265
+
266
+ def parse_event(event_line)
267
+ event = JSON.parse(event_line)
268
+
269
+ if event["time"] && event["stage"] && event["task"] &&
270
+ event["index"] && event["total"] && event["state"]
271
+ event
272
+ else
273
+ raise InvalidEvent, "Invalid event structure: stage, time, task, " +
274
+ "index, total, state are all required"
275
+ end
276
+
277
+ rescue JSON::JSONError
278
+ raise InvalidEvent, "Cannot parse event, invalid JSON"
279
+ end
280
+
281
+ # Expects time and eta to be adjusted
282
+ def time_with_eta(time, eta)
283
+ time_fmt = format_time(time)
284
+ eta_fmt = eta && eta > Time.now ? format_time(eta - Time.now) : "--:--:--"
285
+
286
+ "#{time_fmt} ETA: #{eta_fmt}"
287
+ end
288
+
289
+ def adjusted_time(time)
290
+ time + @time_adjustment.to_f
291
+ end
292
+ end
293
+
294
+ class StageProgressBar
295
+ attr_accessor :total
296
+ attr_accessor :title
297
+ attr_accessor :current
298
+ attr_accessor :label
299
+ attr_accessor :bar_visible
300
+ attr_accessor :finished_steps
301
+ attr_accessor :terminal_width
302
+
303
+ def initialize(output)
304
+ @output = output
305
+ @current = 0
306
+ @total = 100
307
+ @bar_visible = true
308
+ @finished_steps = 0
309
+ @filler = "o"
310
+ @terminal_width = calculate_terminal_width
311
+ @bar_width = (0.24 * @terminal_width).to_i # characters
312
+ end
313
+
314
+ def refresh
315
+ clear_line
316
+ bar_repr = @bar_visible ? bar : ""
317
+ title_width = (0.35 * @terminal_width).to_i
318
+ title = @title.truncate(title_width).ljust(title_width)
319
+ @output.print "#{title} #{bar_repr} #{@finished_steps}/#{@total}"
320
+ @output.print " #{@label}" if @label
321
+ end
322
+
323
+ def bar
324
+ n_fillers = @total == 0 ? 0 : [(@bar_width *
325
+ (@current.to_f / @total.to_f)).floor, 0].max
326
+
327
+ fillers = "#{@filler}" * n_fillers
328
+ spaces = " " * [(@bar_width - n_fillers), 0].max
329
+ "|#{fillers}#{spaces}|"
330
+ end
331
+
332
+ def clear_line
333
+ @output.print("\r")
334
+ @output.print(" " * @terminal_width)
335
+ @output.print("\r")
336
+ end
337
+
338
+ def calculate_terminal_width
339
+ if !ENV["TERM"].blank?
340
+ width = `tput cols`
341
+ $?.exitstatus == 0 ? [width.to_i, 100].min : 80
342
+ else
343
+ 80
344
+ end
345
+ rescue
346
+ 80
347
+ end
348
+
349
+ end
350
+
351
+ end