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,248 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ # This relies on having the following instance variables in a host class:
4
+ # @dev_builds_dir, @final_builds_dir, @blobstore,
5
+ # @name, @version, @tarball_path, @final, @artefact_type
6
+
7
+ module Bosh::Cli
8
+ module PackagingHelper
9
+ include Bosh::Cli::VersionCalc
10
+
11
+ attr_accessor :dry_run
12
+
13
+ def init_indices
14
+ @dev_index = VersionsIndex.new(@dev_builds_dir)
15
+ @final_index = VersionsIndex.new(@final_builds_dir)
16
+ end
17
+
18
+ def final?
19
+ @final
20
+ end
21
+
22
+ def dry_run?
23
+ @dry_run
24
+ end
25
+
26
+ def new_version?
27
+ @tarball_generated || @promoted || @will_be_promoted
28
+ end
29
+
30
+ def older_version?
31
+ if @tarball_generated
32
+ false
33
+ elsif @used_final_version
34
+ version_cmp(@version, @final_index.latest_version) < 0
35
+ else
36
+ version_cmp(@version, @dev_index.latest_version) < 0
37
+ end
38
+ end
39
+
40
+ def notes
41
+ notes = []
42
+
43
+ if @will_be_promoted
44
+ new_final_version = @final_index.latest_version.to_i + 1
45
+ notes << "new final version #{new_final_version}"
46
+ elsif new_version?
47
+ notes << "new version"
48
+ end
49
+
50
+ notes << "older than latest" if older_version?
51
+ notes
52
+ end
53
+
54
+ def build
55
+ with_indent(" ") do
56
+ use_final_version || use_dev_version || generate_tarball
57
+ end
58
+ upload_tarball(@tarball_path) if final? && !dry_run?
59
+ @will_be_promoted = true if final? && dry_run? && @used_dev_version
60
+ end
61
+
62
+ def use_final_version
63
+ say("Final version:", " ")
64
+
65
+ item = @final_index[fingerprint]
66
+
67
+ if item.nil?
68
+ say("NOT FOUND".red)
69
+ return nil
70
+ end
71
+
72
+ blobstore_id = item["blobstore_id"]
73
+ version = item["version"]
74
+
75
+ if blobstore_id.nil?
76
+ say("No blobstore id".red)
77
+ return nil
78
+ end
79
+
80
+ filename = @final_index.filename(version)
81
+ need_fetch = true
82
+
83
+ if File.exists?(filename)
84
+ say("FOUND LOCAL".green)
85
+ if file_checksum(filename) == item["sha1"]
86
+ @tarball_path = filename
87
+ need_fetch = false
88
+ else
89
+ say("LOCAL CHECKSUM MISMATCH".red)
90
+ need_fetch = true
91
+ end
92
+ end
93
+
94
+ if need_fetch
95
+ say("Downloading `#{name} (#{version})' (#{blobstore_id})".green)
96
+ payload = @blobstore.get(blobstore_id)
97
+ if Digest::SHA1.hexdigest(payload) == item["sha1"]
98
+ @tarball_path = @final_index.add_version(fingerprint, item, payload)
99
+ else
100
+ err("`#{name}' (#{version}) is corrupted in blobstore " +
101
+ "(id=#{blobstore_id}), " +
102
+ "please remove it manually and re-generate the final release")
103
+ end
104
+ end
105
+
106
+ @version = version
107
+ @used_final_version = true
108
+ true
109
+ rescue Bosh::Blobstore::NotFound => e
110
+ raise BlobstoreError, "Final version of `#{name}' not found in blobstore"
111
+ rescue Bosh::Blobstore::BlobstoreError => e
112
+ raise BlobstoreError, "Blobstore error: #{e}"
113
+ end
114
+
115
+ def use_dev_version
116
+ say("Dev version:", " ")
117
+ item = @dev_index[fingerprint]
118
+
119
+ if item.nil?
120
+ say("NOT FOUND".red)
121
+ return nil
122
+ end
123
+
124
+ version = item["version"]
125
+ filename = @dev_index.filename(version)
126
+
127
+ if File.exists?(filename)
128
+ say("FOUND LOCAL".green)
129
+ else
130
+ say("TARBALL MISSING".red)
131
+ return nil
132
+ end
133
+
134
+ if file_checksum(filename) == item["sha1"]
135
+ @tarball_path = filename
136
+ @version = version
137
+ @used_dev_version = true
138
+ else
139
+ say("`#{name} (#{version})' tarball corrupted".red)
140
+ return nil
141
+ end
142
+ end
143
+
144
+ def generate_tarball
145
+ if final?
146
+ err_message = "No matching build found for " +
147
+ "`#{@name}' #{@artefact_type}.\n" +
148
+ "Please consider creating a dev release first.\n" +
149
+ "The fingerprint is `#{fingerprint}'."
150
+ err(err_message)
151
+ end
152
+
153
+ current_final = @final_index.latest_version.to_i
154
+ new_minor = minor_version(@dev_index.latest_version(current_final)) + 1
155
+
156
+ version = "#{current_final}.#{new_minor}-dev"
157
+ tmp_file = Tempfile.new(name)
158
+
159
+ say("Generating...")
160
+
161
+ copy_files
162
+
163
+ in_build_dir do
164
+ tar_out = `tar -chzf #{tmp_file.path} . 2>&1`
165
+ unless $?.exitstatus == 0
166
+ raise PackagingError, "Cannot create tarball: #{tar_out}"
167
+ end
168
+ end
169
+
170
+ payload = tmp_file.read
171
+
172
+ item = {
173
+ "version" => version
174
+ }
175
+
176
+ unless dry_run?
177
+ @dev_index.add_version(fingerprint, item, payload)
178
+ @tarball_path = @dev_index.filename(version)
179
+ end
180
+
181
+ @version = version
182
+ @tarball_generated = true
183
+ say("Generated version #{version}".green)
184
+ true
185
+ end
186
+
187
+ def upload_tarball(path)
188
+ item = @final_index[fingerprint]
189
+
190
+ say("Uploading final version #{version}...")
191
+
192
+ if !item.nil?
193
+ version = item["version"]
194
+ say("This package has already been uploaded")
195
+ return
196
+ end
197
+
198
+ version = @final_index.latest_version.to_i + 1
199
+ payload = File.read(path)
200
+
201
+ blobstore_id = @blobstore.create(payload)
202
+
203
+ item = {
204
+ "blobstore_id" => blobstore_id,
205
+ "version" => version
206
+ }
207
+
208
+ say("Uploaded, blobstore id #{blobstore_id}")
209
+ @final_index.add_version(fingerprint, item, payload)
210
+ @tarball_path = @final_index.filename(version)
211
+ @version = version
212
+ @promoted = true
213
+ true
214
+ rescue Bosh::Blobstore::BlobstoreError => e
215
+ raise BlobstoreError, "Blobstore error: #{e}"
216
+ end
217
+
218
+ def file_checksum(path)
219
+ Digest::SHA1.file(path).hexdigest
220
+ end
221
+
222
+ def checksum
223
+ if @tarball_path && File.exists?(@tarball_path)
224
+ file_checksum(@tarball_path)
225
+ else
226
+ raise RuntimeError,
227
+ "cannot read checksum for not yet generated package/job"
228
+ end
229
+ end
230
+
231
+ # Git doesn't really track file permissions, it just looks at executable
232
+ # bit and uses 0755 if it's set or 0644 if not. We have to mimic that
233
+ # behavior in the fingerprint calculation to avoid the situation where
234
+ # seemingly clean working copy would trigger new fingerprints for
235
+ # artifacts with changed permissions. Also we don't want current
236
+ # fingerprints to change, hence the exact values below.
237
+ def tracked_permissions(path)
238
+ if File.directory?(path)
239
+ "40755"
240
+ elsif File.executable?(path)
241
+ "100755"
242
+ else
243
+ "100644"
244
+ end
245
+ end
246
+ end
247
+ end
248
+
@@ -0,0 +1,176 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ # This class encapsulates the details of handling dev and final releases:
5
+ # also it partitions release metadata between final config (which is
6
+ # under version control) and user dev config.
7
+ class Release
8
+ attr_reader :dir
9
+
10
+ def initialize(dir)
11
+ @dir = dir
12
+ config_dir = File.join(dir, "config")
13
+ @final_config_file = File.join(config_dir, "final.yml")
14
+ @dev_config_file = File.join(config_dir, "dev.yml")
15
+
16
+ @private_config_file = File.join(config_dir, "private.yml")
17
+
18
+ unless File.directory?(dir)
19
+ err("Cannot find release directory `#{dir}'")
20
+ end
21
+
22
+ unless File.directory?(config_dir)
23
+ err("Cannot find release config directory `#{config_dir}'")
24
+ end
25
+
26
+ @final_config = load_config(@final_config_file)
27
+ @dev_config = load_config(@dev_config_file)
28
+ @private_config = load_config(@private_config_file)
29
+
30
+ migrate_legacy_configs
31
+ end
32
+
33
+ # Devbox-specific attributes, gitignored
34
+ [:dev_name, :latest_release_filename].each do |attr|
35
+ define_method(attr) do
36
+ @dev_config[attr.to_s]
37
+ end
38
+
39
+ define_method("#{attr}=".to_sym) do |value|
40
+ @dev_config[attr.to_s] = value
41
+ end
42
+ end
43
+
44
+ # Shared attributes, present in repo
45
+ [:final_name, :min_cli_version].each do |attr|
46
+ define_method(attr) do
47
+ @final_config[attr.to_s]
48
+ end
49
+
50
+ define_method("#{attr}=".to_sym) do |value|
51
+ @final_config[attr.to_s] = value
52
+ end
53
+ end
54
+
55
+ # Check if the blobstore secret is provided in the private config file
56
+ #
57
+ # @return [Boolean]
58
+ def has_blobstore_secret?
59
+ @private_config.has_key?("blobstore_secret")
60
+ end
61
+
62
+ # Picks blobstore client to use with current release.
63
+ #
64
+ # @return [Bosh::Blobstore::Client] blobstore client
65
+ def blobstore
66
+ return @blobstore if @blobstore
67
+ blobstore_config = Marshal.load(Marshal.dump(@final_config["blobstore"]))
68
+
69
+ if blobstore_config.nil?
70
+ err("Missing blobstore configuration, please update your release")
71
+ end
72
+
73
+ provider = blobstore_config["provider"]
74
+ options = blobstore_config["options"] || {}
75
+
76
+ if has_blobstore_secret?
77
+ options["secret"] = @private_config["blobstore_secret"]
78
+ end
79
+
80
+ @blobstore = Bosh::Blobstore::Client.create(provider,
81
+ symbolize_keys(options))
82
+
83
+ rescue Bosh::Blobstore::BlobstoreError => e
84
+ err("Cannot initialize blobstore: #{e}")
85
+ end
86
+
87
+ def save_config
88
+ # TODO: introduce write_yaml helper
89
+ File.open(@dev_config_file, "w") do |f|
90
+ YAML.dump(@dev_config, f)
91
+ end
92
+
93
+ File.open(@final_config_file, "w") do |f|
94
+ YAML.dump(@final_config, f)
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # Upgrade path for legacy clients that kept release metadata
101
+ # in config/dev.yml and config/final.yml
102
+ #
103
+ def migrate_legacy_configs
104
+ # We're using blobstore_options as old config marker.
105
+ # Unfortunately old CLI won't tell you to upgrade because it checks
106
+ # for valid blobstore options first, so instead of removing
107
+ # blobstore_options we mark it as deprecated, so new CLI proceeds
108
+ # to migrate while the old one tells you to upgrade.
109
+ if @dev_config.has_key?("blobstore_options") &&
110
+ @dev_config["blobstore_options"] != "deprecated"
111
+ say("Found legacy dev config file `#{@dev_config_file}'".yellow)
112
+
113
+ new_dev_config = {
114
+ "dev_name" => @dev_config["name"],
115
+ "latest_release_filename" =>
116
+ @dev_config["latest_release_filename"],
117
+
118
+ # Following two options are only needed for older clients
119
+ # to fail gracefully and never actually read by a new client
120
+ "blobstore_options" => "deprecated",
121
+ "min_cli_version" => "0.12"
122
+ }
123
+
124
+ @dev_config = new_dev_config
125
+
126
+ File.open(@dev_config_file, "w") do |f|
127
+ YAML.dump(@dev_config, f)
128
+ end
129
+ say("Migrated dev config file format".green)
130
+ end
131
+
132
+ if @final_config.has_key?("blobstore_options") &&
133
+ @final_config["blobstore_options"] != "deprecated"
134
+ say("Found legacy config file `#{@final_config_file}'".yellow)
135
+
136
+ unless @final_config["blobstore_options"]["provider"] == "atmos" &&
137
+ @final_config["blobstore_options"].has_key?("atmos_options")
138
+ err("Please update your release to the version " +
139
+ "that uses Atmos blobstore")
140
+ end
141
+
142
+ new_final_config = {
143
+ "final_name" => @final_config["name"],
144
+ "min_cli_version" => @final_config["min_cli_version"],
145
+ "blobstore" => {
146
+ "provider" => "atmos",
147
+ "options" => @final_config["blobstore_options"]["atmos_options"]
148
+ },
149
+ "blobstore_options" => "deprecated"
150
+ }
151
+
152
+ @final_config = new_final_config
153
+
154
+ File.open(@final_config_file, "w") { |f| YAML.dump(@final_config, f) }
155
+ say("Migrated final config file format".green)
156
+ end
157
+ end
158
+
159
+ def symbolize_keys(hash)
160
+ hash.inject({}) do |h, (key, value)|
161
+ h[key.to_sym] = value
162
+ h
163
+ end
164
+ end
165
+
166
+ def load_config(file)
167
+ if File.exists?(file)
168
+ load_yaml_file(file)
169
+ else
170
+ {}
171
+ end
172
+ end
173
+
174
+ end
175
+
176
+ end
@@ -0,0 +1,215 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ class ReleaseBuilder
5
+ include Bosh::Cli::DependencyHelper
6
+
7
+ DEFAULT_RELEASE_NAME = "bosh_release"
8
+
9
+ attr_reader :release, :packages, :jobs, :changed_jobs
10
+
11
+ def initialize(release, packages, jobs, options = { })
12
+ @release = release
13
+ @final = options.has_key?(:final) ? !!options[:final] : false
14
+ @packages = packages
15
+ @jobs = jobs
16
+
17
+ @index = VersionsIndex.new(releases_dir, release_name)
18
+ create_release_build_dir
19
+ end
20
+
21
+ def release_name
22
+ name = @final ? @release.final_name : @release.dev_name
23
+ name.blank? ? DEFAULT_RELEASE_NAME : name
24
+ end
25
+
26
+ def version
27
+ @version ||= assign_version
28
+ end
29
+
30
+ def final?
31
+ @final
32
+ end
33
+
34
+ def affected_jobs
35
+ result = Set.new(@jobs.select { |job| job.new_version? })
36
+ return result if @packages.empty?
37
+
38
+ new_package_names = @packages.inject([]) do |list, package|
39
+ list << package.name if package.new_version?
40
+ list
41
+ end
42
+
43
+ @jobs.each do |job|
44
+ result << job if (new_package_names & job.packages).size > 0
45
+ end
46
+
47
+ result.to_a
48
+ end
49
+
50
+ def build(options = {})
51
+ options = { :generate_tarball => true }.merge(options)
52
+
53
+ header("Generating manifest...")
54
+ generate_manifest
55
+ if options[:generate_tarball]
56
+ header("Generating tarball...")
57
+ generate_tarball
58
+ end
59
+ @build_complete = true
60
+ end
61
+
62
+ def copy_packages
63
+ packages.each do |package|
64
+ say("%-40s %s" % [package.name.green,
65
+ pretty_size(package.tarball_path)])
66
+ FileUtils.cp(package.tarball_path,
67
+ File.join(build_dir, "packages", "#{package.name}.tgz"),
68
+ :preserve => true)
69
+ end
70
+ @packages_copied = true
71
+ end
72
+
73
+ def copy_jobs
74
+ jobs.each do |job|
75
+ say("%-40s %s" % [job.name.green, pretty_size(job.tarball_path)])
76
+ FileUtils.cp(job.tarball_path,
77
+ File.join(build_dir, "jobs", "#{job.name}.tgz"),
78
+ :preserve => true)
79
+ end
80
+ @jobs_copied = true
81
+ end
82
+
83
+ def generate_manifest
84
+ manifest = {}
85
+ manifest["packages"] = []
86
+
87
+ manifest["packages"] = packages.map do |package|
88
+ {
89
+ "name" => package.name,
90
+ "version" => package.version,
91
+ "sha1" => package.checksum,
92
+ "dependencies" => package.dependencies
93
+ }
94
+ end
95
+
96
+ manifest["jobs"] = jobs.map do |job|
97
+ {
98
+ "name" => job.name,
99
+ "version" => job.version,
100
+ "sha1" => job.checksum,
101
+ }
102
+ end
103
+
104
+ manifest["name"] = release_name
105
+
106
+ unless manifest["name"].bosh_valid_id?
107
+ raise InvalidRelease, "Release name `#{manifest["name"]}' " +
108
+ "is not a valid BOSH identifier"
109
+ end
110
+
111
+ fingerprint = make_fingerprint(manifest)
112
+
113
+ if @index[fingerprint]
114
+ old_version = @index[fingerprint]["version"]
115
+ say("This version is no different from version #{old_version}")
116
+ @version = old_version
117
+ else
118
+ @version = assign_version
119
+ @index.add_version(fingerprint, { "version" => @version })
120
+ end
121
+
122
+ manifest["version"] = @version
123
+ manifest_yaml = YAML.dump(manifest)
124
+
125
+ say("Writing manifest...")
126
+ File.open(File.join(build_dir, "release.MF"), "w") do |f|
127
+ f.write(manifest_yaml)
128
+ end
129
+
130
+ File.open(manifest_path, "w") do |f|
131
+ f.write(manifest_yaml)
132
+ end
133
+
134
+ @manifest_generated = true
135
+ end
136
+
137
+ def generate_tarball
138
+ generate_manifest unless @manifest_generated
139
+ return if @index.version_exists?(@version)
140
+
141
+ unless @jobs_copied
142
+ header("Copying jobs...")
143
+ copy_jobs
144
+ nl
145
+ end
146
+ unless @packages_copied
147
+ header("Copying packages...")
148
+ copy_packages
149
+ nl
150
+ end
151
+
152
+ FileUtils.mkdir_p(File.dirname(tarball_path))
153
+
154
+ in_build_dir do
155
+ `tar -czf #{tarball_path} . 2>&1`
156
+ unless $?.exitstatus == 0
157
+ raise InvalidRelease, "Cannot create release tarball"
158
+ end
159
+ say("Generated #{tarball_path}")
160
+ end
161
+ end
162
+
163
+ def releases_dir
164
+ File.join(@release.dir, final? ? "releases" : "dev_releases")
165
+ end
166
+
167
+ def tarball_path
168
+ File.join(releases_dir, "#{release_name}-#{version}.tgz")
169
+ end
170
+
171
+ def manifest_path
172
+ File.join(releases_dir, "#{release_name}-#{version}.yml")
173
+ end
174
+
175
+ def make_fingerprint(item)
176
+ case item
177
+ when Array
178
+ source = item.map { |e| make_fingerprint(e) }.sort.join("")
179
+ when Hash
180
+ source = item.keys.sort.map{ |k| make_fingerprint(item[k]) }.join("")
181
+ else
182
+ source = item.to_s
183
+ end
184
+ Digest::SHA1.hexdigest(source)
185
+ end
186
+
187
+ private
188
+
189
+ def version=(version)
190
+ @version = version
191
+ end
192
+
193
+ def assign_version
194
+ current_version = @index.latest_version.to_i
195
+ current_version + 1
196
+ end
197
+
198
+ def build_dir
199
+ @build_dir ||= Dir.mktmpdir
200
+ end
201
+
202
+ def create_release_build_dir
203
+ in_build_dir do
204
+ FileUtils.mkdir("packages")
205
+ FileUtils.mkdir("jobs")
206
+ end
207
+ end
208
+
209
+ def in_build_dir(&block)
210
+ Dir.chdir(build_dir) { yield }
211
+ end
212
+
213
+ end
214
+
215
+ end