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,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