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,145 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module BoshExtensions
4
+
5
+ def say(message, sep = "\n")
6
+ return unless Bosh::Cli::Config.output && message
7
+ message = message.dup.to_s
8
+ sep = "" if message[-1..-1] == sep
9
+ Bosh::Cli::Config.output.print("#{$indent}#{message}#{sep}")
10
+ end
11
+
12
+ def with_indent(indent)
13
+ old_indent, $indent = $indent, old_indent.to_s + indent.to_s
14
+ yield
15
+ ensure
16
+ $indent = old_indent
17
+ end
18
+
19
+ def header(message, filler = '-')
20
+ say("\n")
21
+ say(message)
22
+ say(filler.to_s * message.size)
23
+ end
24
+
25
+ def nl(count = 1)
26
+ say("\n" * count)
27
+ end
28
+
29
+ def err(message)
30
+ raise Bosh::Cli::CliExit.new message
31
+ end
32
+
33
+ def quit(message = nil)
34
+ say(message)
35
+ raise Bosh::Cli::GracefulExit, message
36
+ end
37
+
38
+ def blank?
39
+ self.to_s.blank?
40
+ end
41
+
42
+ def pretty_size(what, prec=1)
43
+ if what.is_a?(String) && File.exists?(what)
44
+ size = File.size(what)
45
+ else
46
+ size = what.to_i
47
+ end
48
+
49
+ return "NA" unless size
50
+ return "#{size}B" if size < 1024
51
+ return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
52
+ if size < (1024*1024*1024)
53
+ return sprintf("%.#{prec}fM", size/(1024.0*1024.0))
54
+ end
55
+ sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
56
+ end
57
+
58
+ def pluralize(number, singular, plural = nil)
59
+ plural = plural || "#{singular}s"
60
+ number == 1 ? "1 #{singular}" : "#{number} #{plural}"
61
+ end
62
+
63
+ def format_time(time)
64
+ ts = time.to_i
65
+ sprintf("%02d:%02d:%02d", ts / 3600, (ts / 60) % 60, ts % 60);
66
+ end
67
+
68
+ def load_yaml_file(path, expected_type = Hash)
69
+ err("Cannot find file `#{path}'") unless File.exists?(path)
70
+ yaml = YAML.load_file(path)
71
+
72
+ if expected_type && !yaml.is_a?(expected_type)
73
+ err("Incorrect file format in `#{path}', #{expected_type} expected")
74
+ end
75
+
76
+ Bosh::Cli::YamlHelper.check_duplicate_keys(path)
77
+
78
+ yaml
79
+ rescue SystemCallError => e
80
+ err("Cannot load YAML file at `#{path}': #{e}")
81
+ end
82
+
83
+ def dump_yaml_to_file(obj, file)
84
+ yaml = YAML.dump(obj)
85
+ file.write(yaml.gsub(" \n", "\n"))
86
+ file.flush
87
+ end
88
+ end
89
+
90
+ module BoshStringExtensions
91
+
92
+ COLOR_CODES = {
93
+ :red => "\e[0m\e[31m",
94
+ :green => "\e[0m\e[32m",
95
+ :yellow => "\e[0m\e[33m"
96
+ }
97
+
98
+ def red
99
+ colorize(:red)
100
+ end
101
+
102
+ def green
103
+ colorize(:green)
104
+ end
105
+
106
+ def yellow
107
+ colorize(:yellow)
108
+ end
109
+
110
+ def colorize(color_code)
111
+ if Bosh::Cli::Config.colorize && COLOR_CODES[color_code]
112
+ "#{COLOR_CODES[color_code]}#{self}\e[0m"
113
+ else
114
+ self
115
+ end
116
+ end
117
+
118
+ def blank?
119
+ self =~ /^\s*$/
120
+ end
121
+
122
+ def bosh_valid_id?
123
+ self =~ Bosh::Cli::Config::VALID_ID
124
+ end
125
+
126
+ def truncate(limit = 30)
127
+ return "" if self.blank?
128
+ etc = "..."
129
+ stripped = self.strip[0..limit]
130
+ if stripped.length > limit
131
+ stripped.gsub(/\s+?(\S+)?$/, "") + etc
132
+ else
133
+ stripped
134
+ end
135
+ end
136
+
137
+ end
138
+
139
+ class Object
140
+ include BoshExtensions
141
+ end
142
+
143
+ class String
144
+ include BoshStringExtensions
145
+ end
@@ -0,0 +1,62 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ module DependencyHelper
5
+
6
+ # Expects package dependency graph
7
+ # { "A" => ["B", "C"], "B" => ["C", "D"] }
8
+ def tsort_packages(map)
9
+ resolved = Set.new
10
+ in_degree = { }
11
+ graph = { }
12
+
13
+ map.keys.sort.each do |package|
14
+ dependencies = map[package]
15
+ graph[package] ||= Set.new
16
+ in_degree[package] = dependencies.size
17
+
18
+ resolved << package if in_degree[package] == 0
19
+
20
+ # Reverse edges to avoid dfs
21
+ dependencies.each do |dependency|
22
+ unless map.has_key?(dependency)
23
+ raise MissingDependency, ("Package '%s' depends on " +
24
+ "missing package '%s'") % [package, dependency]
25
+ end
26
+
27
+ graph[dependency] ||= Set.new
28
+ graph[dependency] << package
29
+ end
30
+ end
31
+
32
+ sorted = []
33
+
34
+ until resolved.empty?
35
+ p = resolved.first
36
+ resolved.delete(p)
37
+
38
+ sorted << p
39
+
40
+ graph[p].each do |v|
41
+ in_degree[v] -= 1
42
+ resolved << v if in_degree[v] == 0
43
+ end
44
+ graph[p].clear
45
+ end
46
+
47
+ # each_pair gives different (correct) results in 1.8 in 1.9,
48
+ # stabilizing for tests
49
+ graph.keys.sort.each do |v|
50
+ e = graph[v]
51
+ unless e.empty?
52
+ raise CircularDependency, ("Cannot resolve dependencies for '%s': " +
53
+ "circular dependency with '%s'") % [v, e.first]
54
+ end
55
+ end
56
+
57
+ sorted
58
+ end
59
+ end
60
+
61
+ end
62
+
@@ -0,0 +1,263 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ module DeploymentHelper
5
+
6
+ def prepare_deployment_manifest(options = {})
7
+ # TODO: extract to helper class
8
+ deployment_required
9
+ manifest_filename = deployment
10
+
11
+ unless File.exists?(manifest_filename)
12
+ err("Cannot find deployment manifest in `#{manifest_filename}'")
13
+ end
14
+
15
+ manifest = load_yaml_file(manifest_filename)
16
+ manifest_yaml = File.read(manifest_filename)
17
+
18
+ if manifest["name"].blank?
19
+ err("Deployment name not found in the deployment manifest")
20
+ end
21
+
22
+ if manifest["target"]
23
+ err(manifest_target_upgrade_notice)
24
+ end
25
+
26
+ if options[:resolve_properties]
27
+ compiler = DeploymentManifestCompiler.new(manifest_yaml)
28
+ properties = {}
29
+
30
+ begin
31
+ say("Getting deployment properties from director...")
32
+ properties = director.list_properties(manifest["name"])
33
+ rescue Bosh::Cli::DirectorError
34
+ say("Unable to get properties list from director, " +
35
+ "trying without it...")
36
+ end
37
+
38
+ say("Compiling deployment manifest...")
39
+ compiler.properties = properties.inject({}) do |hash, property|
40
+ hash[property["name"]] = property["value"]
41
+ hash
42
+ end
43
+
44
+ manifest_yaml = compiler.result
45
+ manifest = YAML.load(manifest_yaml)
46
+ end
47
+
48
+ if manifest["name"].blank? || manifest["release"].blank? ||
49
+ manifest["director_uuid"].blank?
50
+ err("Invalid manifest `#{File.basename(deployment)}': " +
51
+ "name, release and director UUID are all required")
52
+ end
53
+
54
+ options[:yaml] ? manifest_yaml : manifest
55
+ end
56
+
57
+ # Interactive walkthrough of deployment changes,
58
+ # expected to bail out of CLI using 'cancel_deployment'
59
+ # if something goes wrong, so it doesn't need to have
60
+ # a meaningful return value.
61
+ # @return Boolean Were there any changes in deployment manifest?
62
+ def inspect_deployment_changes(manifest, options = { })
63
+ show_empty_changeset = true
64
+
65
+ if options.has_key?(:show_empty_changeset)
66
+ show_empty_changeset = options[:show_empty_changeset]
67
+ end
68
+
69
+ manifest = manifest.dup
70
+ current_deployment = director.get_deployment(manifest["name"])
71
+
72
+ # We cannot retrieve current manifest until there was at least one
73
+ # successful deployment. There used to be a warning about that
74
+ # but it turned out to be confusing to many users and thus has
75
+ # been removed.
76
+ return if current_deployment["manifest"].nil?
77
+ current_manifest = YAML.load(current_deployment["manifest"])
78
+
79
+ unless current_manifest.is_a?(Hash)
80
+ err("Current deployment manifest format is invalid, " +
81
+ "check if director works properly")
82
+ end
83
+
84
+ # TODO: validate new deployment manifest
85
+ diff = Bosh::Cli::HashChangeset.new
86
+ diff.add_hash(normalize_deployment_manifest(manifest), :new)
87
+ diff.add_hash(normalize_deployment_manifest(current_manifest), :old)
88
+ @_diff_key_visited = { "name" => 1, "director_uuid" => 1 }
89
+
90
+ say("Detecting changes in deployment...".green)
91
+ nl
92
+
93
+ if !diff.changed? && !show_empty_changeset
94
+ return false
95
+ end
96
+
97
+ print_summary(diff, :release)
98
+
99
+ if diff[:release][:name].changed?
100
+ say("Release name has changed: %s -> %s".red % [
101
+ diff[:release][:name].old, diff[:release][:name].new])
102
+ unless confirmed?("This is very serious and potentially destructive " +
103
+ "change. ARE YOU SURE YOU WANT TO DO IT?")
104
+ cancel_deployment
105
+ end
106
+ elsif diff[:release][:version].changed?
107
+ say("Release version has changed: %s -> %s".yellow % [
108
+ diff[:release][:version].old, diff[:release][:version].new])
109
+ unless confirmed?("Are you sure you want to deploy this version?")
110
+ cancel_deployment
111
+ end
112
+ end
113
+ nl
114
+
115
+ print_summary(diff, :compilation)
116
+ nl
117
+
118
+ print_summary(diff, :update)
119
+ nl
120
+
121
+ print_summary(diff, :resource_pools)
122
+
123
+ old_stemcells = Set.new
124
+ new_stemcells = Set.new
125
+
126
+ diff[:resource_pools].each do |pool|
127
+ old_stemcells << {
128
+ :name => pool[:stemcell][:name].old,
129
+ :version => pool[:stemcell][:version].old
130
+ }
131
+ new_stemcells << {
132
+ :name => pool[:stemcell][:name].new,
133
+ :version => pool[:stemcell][:version].new
134
+ }
135
+ end
136
+
137
+ if old_stemcells != new_stemcells
138
+ unless confirmed?("Stemcell update has been detected. " +
139
+ "Are you sure you want to update stemcells?")
140
+ cancel_deployment
141
+ end
142
+ end
143
+
144
+ if old_stemcells.size != new_stemcells.size
145
+ say("Stemcell update seems to be inconsistent with current " +
146
+ "deployment. Please carefully review changes above.".red)
147
+ unless confirmed?("Are you sure this configuration is correct?")
148
+ cancel_deployment
149
+ end
150
+ end
151
+
152
+ nl
153
+ print_summary(diff, :networks)
154
+ nl
155
+ print_summary(diff, :jobs)
156
+ nl
157
+ print_summary(diff, :properties)
158
+ nl
159
+
160
+ diff.keys.each do |key|
161
+ unless @_diff_key_visited[key]
162
+ print_summary(diff, key)
163
+ nl
164
+ end
165
+ end
166
+
167
+ diff.changed?
168
+ rescue Bosh::Cli::DeploymentNotFound
169
+ say("Cannot get current deployment information from director, " +
170
+ "possibly a new deployment".red)
171
+ true
172
+ end
173
+
174
+ private
175
+
176
+ def find_deployment(name)
177
+ if File.exists?(name)
178
+ File.expand_path(name)
179
+ else
180
+ File.expand_path(File.join(work_dir, "deployments", "#{name}.yml"))
181
+ end
182
+ end
183
+
184
+ def cancel_deployment
185
+ quit("Deployment canceled".red)
186
+ end
187
+
188
+ def manifest_error(err)
189
+ err("Deployment manifest error: #{err}")
190
+ end
191
+
192
+ def manifest_target_upgrade_notice
193
+ <<-EOS.gsub(/^\s*/, "").gsub(/\n$/, "")
194
+ Please upgrade your deployment manifest to use director UUID instead
195
+ of target. Just replace 'target' key with 'director_uuid' key in your
196
+ manifest. You can get your director UUID by targeting your director
197
+ with 'bosh target' and running 'bosh status' command afterwards.
198
+ EOS
199
+ end
200
+
201
+ def print_summary(diff, key, title = nil)
202
+ title ||= key.to_s.gsub(/[-_]/, " ").capitalize
203
+
204
+ say(title.green)
205
+ summary = diff[key].summary
206
+ if summary.empty?
207
+ say("No changes")
208
+ else
209
+ say(summary.join("\n"))
210
+ end
211
+ @_diff_key_visited[key.to_s] = 1
212
+ end
213
+
214
+ def normalize_deployment_manifest(manifest)
215
+ normalized = manifest.dup
216
+
217
+ %w(networks jobs resource_pools).each do |section|
218
+ unless normalized[section].kind_of?(Array)
219
+ manifest_error("#{section} is expected to be an array")
220
+ end
221
+
222
+ normalized[section] = normalized[section].inject({}) do |acc, e|
223
+ if e["name"].blank?
224
+ manifest_error("missing name for one of entries in '#{section}'")
225
+ end
226
+ if acc.has_key?(e["name"])
227
+ manifest_error("duplicate entry '#{e['name']}' in '#{section}'")
228
+ end
229
+ acc[e["name"]] = e
230
+ acc
231
+ end
232
+ end
233
+
234
+ normalized["networks"].each do |network_name, network|
235
+ # VIP and dynamic networks do not require subnet,
236
+ # but if it's there we can run some sanity checks
237
+ next unless network.has_key?("subnets")
238
+
239
+ unless network["subnets"].kind_of?(Array)
240
+ manifest_error("network subnets is expected to be an array")
241
+ end
242
+
243
+ subnets = network["subnets"].inject({}) do |acc, e|
244
+ if e["range"].blank?
245
+ manifest_error("missing range for one of subnets " +
246
+ "in '#{network_name}'")
247
+ end
248
+ if acc.has_key?(e["range"])
249
+ manifest_error("duplicate network range '#{e['range']}' " +
250
+ "in '#{network}'")
251
+ end
252
+ acc[e["range"]] = e
253
+ acc
254
+ end
255
+
256
+ normalized["networks"][network_name]["subnets"] = subnets
257
+ end
258
+
259
+ normalized
260
+ end
261
+
262
+ end
263
+ end