bosh_cli 0.16

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