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,300 @@
1
+ module Bosh::Cli::Command
2
+ class Biff < Base
3
+
4
+ # Takes your current deployment configuration and uses some of its
5
+ # configuration to populate the template file. The Network information is
6
+ # used and then IPs for each job are automatically set. Once the template
7
+ # file has been used to generate a new config, the old config and new config
8
+ # are diff'd and the user can choose to keep the new config.
9
+ # @param [String] template The string path to the template that should be
10
+ # used.
11
+ def biff(template)
12
+ begin
13
+ setup(template)
14
+
15
+ template_to_fill = ERB.new(File.read(@template_file), 0, "%<>-")
16
+ @template_output = template_to_fill.result(binding)
17
+
18
+ if @errors == 0
19
+ print_string_diff(File.read(@deployment_file), @template_output)
20
+ keep_new_file unless @no_differences
21
+ else
22
+ say("There were " + "#{@errors} errors.".red)
23
+ end
24
+ ensure
25
+ delete_temp_diff_files
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # Unified is so that we get the whole file diff not just sections.
32
+ DIFF_COMMAND = "diff --unified=1000"
33
+
34
+ KEEP_NEW_VERSION_TEXT = "Would you like to keep the new version? [yn]"
35
+
36
+ DIFF_FAILED_KEEP_NEW_TEXT =
37
+ "Would you like the new version copied to '%s'? [yn]"
38
+
39
+ # Accessor for testing purposes.
40
+ attr_accessor :ip_helper, :template_output
41
+
42
+ # Deletes the temporary files that were used.
43
+ def delete_temp_diff_files
44
+ # File.exists works for both files and directories. Must use for 1.8
45
+ # compat.
46
+ if @dir_name && File.exists?(@dir_name)
47
+ FileUtils.remove_entry_secure(@dir_name)
48
+ end
49
+ end
50
+
51
+ # Takes two strings and prints the diff of them.
52
+ # @param [String] str1 The first string to diff.
53
+ # @param [String] str2 The string to diff against.
54
+ def print_string_diff(str1, str2)
55
+ File.open(@temp_file_path_1, "w") { |f|
56
+ f.write(str1)
57
+ }
58
+ File.open(@temp_file_path_2, "w") { |f|
59
+ f.write(str2)
60
+ }
61
+
62
+ @diff_works = true
63
+ cmd = "#{DIFF_COMMAND} #{@temp_file_path_1} #{@temp_file_path_2} 2>&1"
64
+ output = `#{cmd}`
65
+ if $?.exitstatus == 2
66
+ say("'#{cmd}' did not work.")
67
+ say("Failed, saying: '#{output}'.")
68
+ @diff_works = false
69
+ return
70
+ end
71
+
72
+ if output.empty?
73
+ output = "No differences."
74
+ @no_differences = true
75
+ end
76
+ say(output)
77
+ end
78
+
79
+ # Alias for find. It is used to find within a given object, not the default
80
+ # deployment_obj
81
+ # @param [String] path The path to the object that the template wants to
82
+ # retrieve from the user's config and substitute in.
83
+ # @param [Object] obj Either a hash or array which is the user's deployment
84
+ # config to be looked through.
85
+ # @return [Object] The found object.
86
+ def find_in(path, obj)
87
+ find(path, obj)
88
+ end
89
+
90
+ # Finds a path in the user's deployment configuration object. The reason we
91
+ # use this is to make the paths used in the template file better on the
92
+ # eyes. Instead of having find('jobs[4].static_ips') we have
93
+ # find('jobs.debian_nfs_server.static_ips'). Find will look through the jobs
94
+ # array and find the object that has name=debian_nfs_server. If jobs were a
95
+ # hash then find would get the hash key debian_nfs_server.
96
+ # @param [String] path The path to the object that the template wants to
97
+ # retrieve from the user's config and substitute in.
98
+ # @param [Object] obj Either a hash or array which is the user's deployment
99
+ # config to be looked through.
100
+ # @return [Object] The found object.
101
+ def find(path, obj = @deployment_obj)
102
+ starting_obj = obj
103
+ path_split = path.split(".")
104
+ found_so_far = []
105
+ path_split.each do |path_part|
106
+ found = false
107
+ if obj.is_a?(Array)
108
+ obj.each do |data_val|
109
+ if data_val["name"] == path_part
110
+ obj = data_val
111
+ found = true
112
+ end
113
+ end
114
+ elsif obj[path_part]
115
+ obj = obj[path_part]
116
+ found = true
117
+ end
118
+
119
+ unless found
120
+ @errors += 1
121
+ say("Could not find #{path.red}.")
122
+ say("'#{@template_file}' has it but '#{@deployment_file}' does not.")
123
+ #say("\nIt should exist in \n#{obj.to_yaml}\n")
124
+ if starting_obj == @deployment_obj
125
+ # To cut down on complexity, we don't print out the section of code
126
+ # from the template YAML that the user needs if the find method was
127
+ # called with any other starting object other than deployment_obj.
128
+ # The reason for this is because we'd have to recursively find the
129
+ # path to the starting object so that it can be found in the
130
+ # template.
131
+ print_the_template_path(path.split('.'), found_so_far)
132
+ end
133
+ break
134
+ end
135
+ found_so_far << path_part
136
+ end
137
+ obj
138
+ end
139
+
140
+ # Used by print_the_template_path so that it can prettily print just the
141
+ # section of the template that the user is missing. E.x. if the user is
142
+ # missing the job 'ccdb' then we want to not just print out 'ccdb' and
143
+ # everything in it -- we also want to print out it's heirarchy, aka the fact
144
+ # that it is under jobs. So, we delete everything else in jobs.
145
+ # @param [Object] obj Either a Hash or Array that is supposed to have
146
+ # everything deleted out of it except for a key or object with
147
+ # name = key depending on if it is a hash or array respectively.
148
+ # @param [String] name They key to keep.
149
+ # @return [Object] The original containing object with only the named object
150
+ # in it.
151
+ def delete_all_except(obj, name)
152
+ each_method = obj.is_a?(Hash) ? "each_key" : "each_index"
153
+ obj.send(each_method) do |key|
154
+ if key == name ||
155
+ (obj[key].is_a?(Hash) && obj[key]["name"] == name)
156
+ return_obj = nil
157
+ if (obj.is_a?(Hash))
158
+ return_obj = {}
159
+ return_obj[name] = obj[key]
160
+ else
161
+ return_obj = [obj[key]]
162
+ end
163
+ return return_obj
164
+ end
165
+ end
166
+ end
167
+
168
+ # Tries to print out some helpful output from the template to let the user
169
+ # know what they're missing. For instance, if the user doesn't have a job
170
+ # and the template needs to pull some data from the job then it will print
171
+ # out what the job looks like in the template. This method can't be used if
172
+ # the path is a relative path. A relative path is when find_in was used.
173
+ # @param [Array] looking_for_path The path that is being looked for in the
174
+ # user's deployment config but does not exist.
175
+ # @param [Array] users_farthest_found_path The farthest that 'find' got in
176
+ # finding the looking_for_path.
177
+ def print_the_template_path(looking_for_path, users_farthest_found_path)
178
+ delete_all_except_name =
179
+ (looking_for_path - users_farthest_found_path).first
180
+ path = users_farthest_found_path.join('.')
181
+ what_we_need = find(path, @template_obj)
182
+ what_we_need = delete_all_except(what_we_need, delete_all_except_name)
183
+ say("Add this to '#{path}':".red + "\n#{what_we_need.to_yaml}\n\n")
184
+ end
185
+
186
+ # Loads the template file as YAML. First, it replaces all of the ruby
187
+ # syntax. This file is used so that when there is an error, biff can report
188
+ # what the user's deployment needs according to this template.
189
+ # @return [String] The loaded template file as a ruby object.
190
+ def load_template_as_yaml
191
+ temp_data = File.read(@template_file)
192
+ temp_data.gsub!(/<%=.*%>/, "INSERT_DATA_HERE")
193
+ temp_data.gsub!(/[ ]*<%.*%>[ ]*\n/, "")
194
+ YAML::load(temp_data)
195
+ end
196
+
197
+ # Gets the network's network/mask for configuring things such as the
198
+ # nfs_server properties. E.x. 192.168.1.0/22
199
+ # @param [String] netw_name The name of the network to get the network/mast
200
+ # from.
201
+ # @return [String] The network/mask.
202
+ def get_network_and_mask(netw_name)
203
+ netw_cidr = @ip_helper[netw_name]
204
+ "#{netw_cidr.network}#{netw_cidr.netmask}"
205
+ end
206
+
207
+ # Used by the template to specify IPs for jobs. It uses the CIDR tool to get
208
+ # them.
209
+ # @param [Integer] ip_num The nth IP number to get.
210
+ # @param [String] netw_name The name of the network to get the IP from.
211
+ # @return [String] An IP in the network.
212
+ def ip(ip_num, netw_name)
213
+ ip_range((ip_num..ip_num), netw_name)
214
+ end
215
+
216
+ # Used by the template to specify IP ranges for jobs. It uses the CIDR tool
217
+ # to get them. Accepts negative ranges.
218
+ # @param [Range] range The range of IPs to return, such as 10..24
219
+ # @param [String] netw_name The name of the network to get the IPs from.
220
+ # @return [String] An IP return in the network.
221
+ def ip_range(range, netw_name)
222
+ netw_cidr = @ip_helper[netw_name]
223
+ first = (range.first >= 0) ? range.first :
224
+ netw_cidr.size + range.first
225
+ last = (range.last >= 0) ? range.last :
226
+ netw_cidr.size + range.last
227
+
228
+ first == last ? "#{netw_cidr.nth(first)}" :
229
+ "#{netw_cidr.nth(first)} - #{netw_cidr.nth(last)}"
230
+ end
231
+
232
+ # Creates the helper hash. Keys are the network name, values are the CIDR
233
+ # tool for generating IPs for jobs in that network.
234
+ # @return [Hash] The helper hash that has a CIDR instance for each network.
235
+ def create_ip_helper
236
+ helper = {}
237
+ netw_arr = find("networks")
238
+ if netw_arr.nil?
239
+ raise "Must have a network section."
240
+ end
241
+ netw_arr.each do |netw|
242
+ subnets = netw["subnets"]
243
+ check_valid_network_config(netw, subnets)
244
+ helper[netw["name"]] = NetAddr::CIDR.create(subnets.first["range"])
245
+ end
246
+ helper
247
+ end
248
+
249
+ # Raises errors if there is something wrong with the user's deployment
250
+ # network configuration, since it's used to populate the rest of the
251
+ # template.
252
+ # @param [Hash] netw The user's network configuration as a ruby hash.
253
+ # @param [Array] subnets The subnets in the network.
254
+ def check_valid_network_config(netw, subnets)
255
+ if subnets.nil?
256
+ raise "You must have subnets in #{netw["name"]}"
257
+ end
258
+ unless subnets.length == 1
259
+ raise "Biff doesn't know how to deal with anything other than one " +
260
+ "subnet in #{netw["name"]}"
261
+ end
262
+ if subnets.first["range"].nil? || subnets.first["dns"].nil?
263
+ raise "Biff requires each network to have range and dns entries."
264
+ end
265
+ if subnets.first["gateway"] && subnets.first["gateway"].match(/.*\.1$/).nil?
266
+ raise "Biff only supports configurations where the gateway is the " +
267
+ "first IP (e.g. 172.31.196.1)."
268
+ end
269
+ end
270
+
271
+ # Asks if the user would like to keep the new template and copies it over
272
+ # their existing template if yes. This is its own function for testing.
273
+ def keep_new_file
274
+ copy_to_file = @diff_works ? @deployment_file : @deployment_file + ".new"
275
+ agree_text = @diff_works ?
276
+ KEEP_NEW_VERSION_TEXT : (DIFF_FAILED_KEEP_NEW_TEXT % copy_to_file)
277
+ if agree(agree_text)
278
+ say("New version copied to '#{copy_to_file}'")
279
+ FileUtils.cp(@temp_file_path_2, copy_to_file)
280
+ end
281
+ end
282
+
283
+ # Sets up a few instance variables.
284
+ # @param [String] template The string path to the template that should be
285
+ # used.
286
+ def setup(template)
287
+ @template_file = template
288
+ @deployment_file = deployment
289
+ raise "Deployment not set." if @deployment_file.nil?
290
+ @deployment_obj = load_yaml_file(@deployment_file)
291
+ @template_obj = load_template_as_yaml
292
+ @ip_helper = create_ip_helper
293
+ @errors = 0
294
+ @dir_name = Dir.mktmpdir
295
+ @temp_file_path_1 = "#{@dir_name}/bosh_biff_1"
296
+ @temp_file_path_2 = "#{@dir_name}/bosh_biff_2"
297
+ end
298
+
299
+ end
300
+ end
@@ -0,0 +1,125 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class Blob < Base
5
+
6
+ def upload_blob(*params)
7
+ check_if_blobs_supported
8
+ force = !params.delete("--force").nil?
9
+
10
+ blobs = params.map{ |param| get_blob_name(param) }
11
+ total = blobs.size
12
+ blob_index = get_blobs_index
13
+
14
+ blobs.each_with_index do |blob_name, idx|
15
+ count = idx + 1
16
+ blob_file = File.join(BLOBS_DIR, blob_name)
17
+ blob_sha = Digest::SHA1.file(blob_file).hexdigest
18
+
19
+ if blob_index[blob_name] && !force
20
+ # We already have this binary on record
21
+ if blob_index[blob_name]["sha"] == blob_sha
22
+ say("[#{count}/#{total}] Skipping #{blob_name}".green)
23
+ next
24
+ end
25
+ # Local copy is different from the remote copy
26
+ if interactive?
27
+ confirm = ask("\nBlob #{blob_name} changed, " +
28
+ "do you want to update the binary [yN]: ")
29
+ if confirm.empty? || !(confirm =~ /y(es)?$/i)
30
+ say("[#{count}/#{total}] Skipping #{blob_name}".green)
31
+ next
32
+ end
33
+ end
34
+ end
35
+
36
+ # TODO: We could use the sha and try to avoid
37
+ # uploading duplicated objects.
38
+ say("[#{count}/#{total}] Uploading #{blob_name}".green)
39
+ blob_id = blobstore.create(File.open(blob_file, "r"))
40
+ blob_index[blob_name] = { "object_id" => blob_id, "sha" => blob_sha }
41
+ end
42
+
43
+ # update the index file
44
+ index_file = Tempfile.new("tmp_blob_index")
45
+ dump_yaml_to_file(blob_index, index_file)
46
+ index_file.close
47
+ FileUtils.mv(index_file.path, File.join(work_dir, BLOBS_INDEX_FILE))
48
+ end
49
+
50
+ def sync_blobs(*options)
51
+ check_if_blobs_supported
52
+ force = options.include?("--force")
53
+
54
+ blob_index = get_blobs_index
55
+ total = blob_index.size
56
+ count = 0
57
+
58
+ blob_index.each_pair do |name, blob_info|
59
+ count += 1
60
+ blob_file = File.join(work_dir, BLOBS_DIR, name)
61
+
62
+ # check if we have conflicting blobs
63
+ if File.file?(blob_file) && !force
64
+ blob_sha = Digest::SHA1.file(blob_file).hexdigest
65
+ if blob_sha == blob_info["sha"]
66
+ say("[#{count}/#{total}] Skipping blob #{name}".green)
67
+ next
68
+ end
69
+
70
+ if interactive?
71
+ confirm = ask("\nLocal blob (#{name}) conflicts with " +
72
+ "remote object, overwrite local copy? [yN]: ")
73
+ if confirm.empty? || !(confirm =~ /y(es)?$/i)
74
+ say("[#{count}/#{total}] Skipping blob #{name}".green)
75
+ next
76
+ end
77
+ end
78
+ end
79
+ say("[#{count}/#{total}] Updating #{blob_file}".green)
80
+ fetch_blob(blob_file, blob_info)
81
+ end
82
+ end
83
+
84
+ def blobs_info
85
+ blob_status(true)
86
+ end
87
+
88
+ private
89
+
90
+ # Sanity check the input file and returns the blob_name
91
+ def get_blob_name(file)
92
+ err("Invalid file #{file}") unless File.file?(file)
93
+ blobs_dir = File.join(realpath(work_dir), "#{BLOBS_DIR}/")
94
+ file_path = realpath(File.expand_path(file))
95
+
96
+ if file_path[0..blobs_dir.length - 1] != blobs_dir
97
+ err("#{file_path} is NOT under #{blobs_dir}")
98
+ end
99
+ file_path[blobs_dir.length..file_path.length]
100
+ end
101
+
102
+ # Download the blob (blob_info) into dst_file
103
+ def fetch_blob(dst_file, blob_info)
104
+ object_id = blob_info["object_id"]
105
+
106
+ # fetch the blob
107
+ new_blob = Tempfile.new("new_blob_file")
108
+ blobstore.get(object_id, new_blob)
109
+ new_blob.close
110
+
111
+ if blob_info["sha"] != Digest::SHA1.file(new_blob.path).hexdigest
112
+ err("Fatal error: " +
113
+ "Inconsistent checksum for object #{blob_info["object_id"]}")
114
+ end
115
+
116
+ FileUtils.mkdir_p(File.dirname(dst_file))
117
+ FileUtils.chmod(0644, new_blob.path)
118
+ FileUtils.mv(new_blob.path, dst_file)
119
+ end
120
+
121
+ def realpath(path)
122
+ Pathname.new(path).realpath.to_s
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,169 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class CloudCheck < Base
5
+ include Bosh::Cli::DeploymentHelper
6
+
7
+ def perform(*options)
8
+ auth_required
9
+
10
+ @auto_mode = options.delete("--auto")
11
+ @report_mode = options.delete("--report")
12
+
13
+ if non_interactive? && !@report_mode
14
+ err ("Cloudcheck cannot be run in non-interactive mode\n" +
15
+ "Please use `--auto' flag if you want automated resolutions")
16
+ end
17
+
18
+ if options.size > 0
19
+ err("Unknown options: #{options.join(", ")}")
20
+ end
21
+
22
+ if @auto_mode && @report_mode
23
+ err("Can't use --auto and --report mode together")
24
+ end
25
+
26
+ say("Performing cloud check...")
27
+
28
+ manifest = prepare_deployment_manifest
29
+ deployment_name = manifest["name"]
30
+
31
+ status, body = director.perform_cloud_scan(deployment_name)
32
+ scan_failed(status, body) if status != :done
33
+
34
+ say("Scan is complete, checking if any problems found...")
35
+ @problems = director.list_problems(deployment_name)
36
+
37
+ verify_problems
38
+ nl
39
+ say("Found #{pluralize(@problems.size, "problem")}".yellow)
40
+ nl
41
+
42
+ @resolutions = {}
43
+
44
+ @problems.each_with_index do |problem, index|
45
+ description = problem["description"].to_s.chomp(".") + "."
46
+ say("Problem #{index+1} of #{@problems.size}: #{description}".yellow)
47
+ next if @report_mode
48
+ if @auto_mode
49
+ @resolutions[problem["id"]] = {
50
+ "name" => nil,
51
+ "plan" => "apply default resolution"
52
+ }
53
+ else
54
+ @resolutions[problem["id"]] = get_resolution(problem)
55
+ end
56
+ nl
57
+ end
58
+
59
+ if @report_mode
60
+ exit(@problems.empty? ? 0 : 1)
61
+ end
62
+
63
+ confirm_resolutions unless @auto_mode
64
+ say("Applying resolutions...")
65
+
66
+ action_map = @resolutions.inject({}) do |hash, (id, resolution)|
67
+ hash[id] = resolution["name"]
68
+ hash
69
+ end
70
+
71
+ status, body = director.apply_resolutions(deployment_name, action_map)
72
+ resolution_failed(status, body) if status != :done
73
+ say("Cloudcheck is finished".green)
74
+ end
75
+
76
+ private
77
+
78
+ def scan_failed(status, response)
79
+ responses = {
80
+ :non_trackable => "Unable to track cloud scan progress, " +
81
+ "please update your director",
82
+ :track_timeout => "Timed out while tracking cloud scan progress",
83
+ :error => "Cloud scan error",
84
+ :invalid => "Invalid cloud scan request"
85
+ }
86
+
87
+ err(responses[status] || "Cloud scan failed: #{response}")
88
+ end
89
+
90
+ def resolution_failed(status, response)
91
+ responses = {
92
+ :non_trackable => "Unable to track problem resolution progress, " +
93
+ "please update your director",
94
+ :track_timeout => "Timed out while tracking problem resolution progress",
95
+ :error => "Problem resolution error",
96
+ :invalid => "Invalid problem resolution request"
97
+ }
98
+
99
+ err(responses[status] || "Problem resolution failed: #{response}")
100
+ end
101
+
102
+ def verify_problems
103
+ err("Invalid problem list format") unless @problems.kind_of?(Enumerable)
104
+
105
+ if @problems.empty?
106
+ say("No problems found".green)
107
+ quit
108
+ end
109
+
110
+ @problems.each do |problem|
111
+ unless problem.is_a?(Hash) && problem["id"] && problem["description"] &&
112
+ problem["resolutions"].kind_of?(Enumerable)
113
+ err("Invalid problem list format received from director")
114
+ end
115
+
116
+ problem["resolutions"].each do |resolution|
117
+ if resolution["name"].blank? || resolution["plan"].blank?
118
+ err("Some problem resolutions received from director " +
119
+ "have an invalid format")
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def get_resolution(problem)
126
+ resolutions = problem["resolutions"]
127
+
128
+ resolutions.each_with_index do |resolution, index|
129
+ say(" #{index+1}. #{resolution["plan"]}")
130
+ end
131
+
132
+ choice = nil
133
+ loop do
134
+ choice = ask("Please choose a resolution [1 - #{resolutions.size}]: ")
135
+ if choice =~ /^\s*\d+\s*$/ &&
136
+ choice.to_i >= 1 &&
137
+ choice.to_i <= resolutions.size
138
+ break
139
+ end
140
+ say("Please enter a number between 1 and #{resolutions.size}".red)
141
+ end
142
+
143
+ resolutions[choice.to_i-1] # -1 accounts for 0-based indexing
144
+ end
145
+
146
+ def confirm_resolutions
147
+ say("Below is the list of resolutions you've provided".yellow)
148
+ say("Please make sure everything is fine and confirm your changes".yellow)
149
+ nl
150
+
151
+ @problems.each_with_index do |problem, index|
152
+ description = problem["description"]
153
+ plan = @resolutions[problem["id"]]["plan"]
154
+ padding = " " * ((index+1).to_s.size + 4)
155
+ say(" #{index+1}. #{problem["description"]}")
156
+ say("#{padding}#{plan.to_s.yellow}")
157
+ nl
158
+ end
159
+
160
+ # TODO: allow editing resolutions?
161
+ cancel unless confirmed?("Apply resolutions?")
162
+ end
163
+
164
+ def cancel
165
+ err("Canceled cloudcheck")
166
+ end
167
+
168
+ end
169
+ end