boshify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/boshify +23 -0
- data/lib/boshify/command_line.rb +81 -0
- data/lib/boshify/downloader.rb +22 -0
- data/lib/boshify/filesystem.rb +44 -0
- data/lib/boshify/package_converter.rb +22 -0
- data/lib/boshify/release_creator.rb +154 -0
- data/lib/boshify/ubuntu_packages.rb +111 -0
- data/lib/boshify.rb +6 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 301facaa8c05504dbec23d0896f196a1714a7551
|
4
|
+
data.tar.gz: c37f62978214b529975c01c8c1ef3a84549ed5a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3fe9bb319022d84106081a18ad7e341c90cb256d3e670b5c9eb9f70d4c56ee7c2012d35eb2e8eb68c8fc702e31ef22b3021c44b764d3ab4f8ed91fa156749cb7
|
7
|
+
data.tar.gz: 813e29e859b7945191cc061298038923ac9f76153610f886b6303a1c684748e2a463237ae001350d6191f957b8ba656f5a955f9a63678897bc9ec7a9fd9af4ed
|
data/bin/boshify
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'boshify'
|
3
|
+
require 'bosh_lite_helpers'
|
4
|
+
|
5
|
+
# Boshify generates BOSH releases
|
6
|
+
module Boshify
|
7
|
+
cr = BoshLiteHelpers::CommandRunner.new
|
8
|
+
fs = Filesystem.new
|
9
|
+
dl = Downloader.new(filesystem: fs)
|
10
|
+
release_creator = ReleaseCreator.new(filesystem: fs,
|
11
|
+
release_dir: Dir.pwd,
|
12
|
+
cmd_runner: cr)
|
13
|
+
pkg_source = UbuntuPackages.new(downloader: dl, cmd_runner: cr)
|
14
|
+
converter = PackageConverter.new(package_source: pkg_source,
|
15
|
+
downloader: dl,
|
16
|
+
release_creator: release_creator)
|
17
|
+
cmd_line = CommandLine.new(program_name: $PROGRAM_NAME,
|
18
|
+
package_converter: converter)
|
19
|
+
result = cmd_line.run(ARGV)
|
20
|
+
puts result[:stdout] unless result[:stdout].empty?
|
21
|
+
STDERR.puts result[:stderr] unless result[:stderr].empty?
|
22
|
+
exit result[:exit_code]
|
23
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Boshify
|
5
|
+
# Command line argument processing
|
6
|
+
class CommandLine
|
7
|
+
def initialize(options)
|
8
|
+
unless options[:program_name]
|
9
|
+
fail ArgumentError, 'Program name must be specified'
|
10
|
+
end
|
11
|
+
unless options[:package_converter]
|
12
|
+
fail ArgumentError, 'Package converter must be specified'
|
13
|
+
end
|
14
|
+
@program_name = Pathname.new(options[:program_name]).basename
|
15
|
+
@package_converter = options[:package_converter]
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(args)
|
19
|
+
with_options(args) do |options|
|
20
|
+
begin
|
21
|
+
use_mirror_if_specified(options[:mirror])
|
22
|
+
@package_converter.create_release_for(name: options[:package])
|
23
|
+
{ exit_code: 0, stdout: "Package #{options[:package]} converted",
|
24
|
+
stderr: '' }
|
25
|
+
rescue => e
|
26
|
+
{ exit_code: 1, stdout: '', stderr: e.message }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def with_options(args)
|
34
|
+
options = parse(args)
|
35
|
+
if options[:package]
|
36
|
+
yield options
|
37
|
+
else
|
38
|
+
{ exit_code: 0, stdout: @parser.help, stderr: '' }
|
39
|
+
end
|
40
|
+
rescue OptionParser::MissingArgument
|
41
|
+
{ exit_code: 1, stdout: @parser.help, stderr: '' }
|
42
|
+
end
|
43
|
+
|
44
|
+
def use_mirror_if_specified(mirror_url)
|
45
|
+
return unless mirror_url
|
46
|
+
@package_converter.package_source.mirror_url = mirror_url
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(args)
|
50
|
+
options = {}
|
51
|
+
@parser = OptionParser.new do |cmd_opts|
|
52
|
+
cmd_opts.banner = "#{@program_name} [options]"
|
53
|
+
add_package_option(cmd_opts, options)
|
54
|
+
add_mirror_option(cmd_opts, options)
|
55
|
+
add_help_option(cmd_opts, options)
|
56
|
+
end
|
57
|
+
@parser.parse!(args)
|
58
|
+
options
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_package_option(cmd_opts, options)
|
62
|
+
cmd_opts.on('-p', '--package PACKAGE', 'Ubuntu source package') do |p|
|
63
|
+
options[:package] = p
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_mirror_option(cmd_opts, options)
|
68
|
+
cmd_opts.on('-m',
|
69
|
+
'--mirror [MIRROR]',
|
70
|
+
'Alternate Ubuntu mirror URL') do |m|
|
71
|
+
options[:mirror] = m
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def add_help_option(cmd_opts, options)
|
76
|
+
cmd_opts.on('-h', '--help', 'Print help') do
|
77
|
+
options[:help] = true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Boshify
|
5
|
+
class DownloadError < StandardError; end
|
6
|
+
|
7
|
+
# Downloads remote resources to disk
|
8
|
+
class Downloader
|
9
|
+
def initialize(options = {})
|
10
|
+
@filesystem = options[:filesystem]
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(url)
|
14
|
+
bn = Pathname.new(URI.parse(url.to_s).path).basename
|
15
|
+
r = HTTParty.get(url)
|
16
|
+
unless r.ok?
|
17
|
+
fail DownloadError, "The resource could not be retrieved: #{url}"
|
18
|
+
end
|
19
|
+
@filesystem.write_file(basename: bn, content: r.body)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Boshify
|
6
|
+
# Wrapper around filesystem operations
|
7
|
+
class Filesystem
|
8
|
+
def copy(from, to)
|
9
|
+
FileUtils.copy(from, to)
|
10
|
+
end
|
11
|
+
|
12
|
+
def mkdir_p(path)
|
13
|
+
path.mkpath
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_file(options)
|
17
|
+
check_file_options!(options)
|
18
|
+
file = determine_file_path(options)
|
19
|
+
File.open(file.cleanpath, 'w') { |f| f.write(options[:content]) }
|
20
|
+
file
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_file_options!(options)
|
26
|
+
unless options[:content]
|
27
|
+
fail ArgumentError, 'File content must be specified'
|
28
|
+
end
|
29
|
+
|
30
|
+
# rubocop:disable GuardClause
|
31
|
+
unless options[:path] || options[:basename]
|
32
|
+
fail ArgumentError, 'Either basename or path must be specified'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def determine_file_path(options)
|
37
|
+
if options[:path]
|
38
|
+
options[:path]
|
39
|
+
else
|
40
|
+
Pathname.new(Dir.mktmpdir) + options[:basename].basename
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Boshify
|
2
|
+
# Converts an operating system package to a BOSH release
|
3
|
+
class PackageConverter
|
4
|
+
attr_reader :package_source
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@package_source = options[:package_source]
|
8
|
+
@downloader = options[:downloader]
|
9
|
+
@release_creator = options[:release_creator]
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_release_for(package = {})
|
13
|
+
@package_source.refresh
|
14
|
+
local_path = @downloader.get(
|
15
|
+
@package_source.source_tarball_url(package[:name]))
|
16
|
+
@release_creator.create_release(name: package[:name], packages: [
|
17
|
+
name: package[:name],
|
18
|
+
source_tarball: local_path
|
19
|
+
])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Boshify
|
5
|
+
# Responsible for generating BOSH releases
|
6
|
+
# rubocop:disable ClassLength
|
7
|
+
class ReleaseCreator
|
8
|
+
def initialize(options)
|
9
|
+
check_options!(options)
|
10
|
+
@fs = options[:filesystem]
|
11
|
+
@release_dir = Pathname.new(options[:release_dir])
|
12
|
+
@cmd_runner = options[:cmd_runner]
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_release(release)
|
16
|
+
create_empty_release
|
17
|
+
create_placeholder_blobstore_config(release)
|
18
|
+
create_job(release)
|
19
|
+
create_packages(release)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def check_options!(options)
|
25
|
+
unless options[:filesystem]
|
26
|
+
fail ArgumentError, 'Filesystem must be provided'
|
27
|
+
end
|
28
|
+
unless options[:release_dir]
|
29
|
+
fail ArgumentError, 'Release directory must be provided'
|
30
|
+
end
|
31
|
+
# rubocop:disable GuardClause
|
32
|
+
unless options[:cmd_runner]
|
33
|
+
fail ArgumentError, 'Command runner must be provided'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_empty_release
|
38
|
+
create_release_dirs
|
39
|
+
generate_empty_blobs_yaml
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_release_dirs
|
43
|
+
%w(blobs config jobs packages src).each do |dir|
|
44
|
+
@fs.mkdir_p(@release_dir + dir)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_empty_blobs_yaml
|
49
|
+
@fs.write_file(path: @release_dir + 'config' + 'blobs.yml',
|
50
|
+
content: YAML.dump({}))
|
51
|
+
end
|
52
|
+
|
53
|
+
# rubocop:disable MethodLength
|
54
|
+
def create_placeholder_blobstore_config(release)
|
55
|
+
@fs.write_file(path: @release_dir + 'config' + 'final.yml',
|
56
|
+
content: YAML.dump(
|
57
|
+
'blobstore' => {
|
58
|
+
'provider' => 's3',
|
59
|
+
'options' => {
|
60
|
+
'bucket_name' => "#{release[:name]}-release",
|
61
|
+
'access_key_id' => 'MY_ACCESS_KEY_ID',
|
62
|
+
'secret_acces_key' => 'MY_SECRET_ACCESS_KEY',
|
63
|
+
'encryption_key' => 'MY_ENCRYPTION_KEY'
|
64
|
+
}
|
65
|
+
},
|
66
|
+
'final_name' => release[:name]
|
67
|
+
))
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_job(release)
|
71
|
+
job_dir = @release_dir + 'jobs' + release[:name]
|
72
|
+
@fs.mkdir_p(job_dir)
|
73
|
+
@fs.write_file(path: job_dir + 'monit', content: '')
|
74
|
+
@fs.write_file(path: job_dir + 'spec', content: job_spec(release))
|
75
|
+
end
|
76
|
+
|
77
|
+
def job_spec(release)
|
78
|
+
YAML.dump(
|
79
|
+
'name' => release[:name],
|
80
|
+
'packages' => release[:packages].map { |p| p[:name] },
|
81
|
+
'templates' => {}
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_path(source_tarball)
|
86
|
+
files = @cmd_runner.run("tar -ztf #{source_tarball}",
|
87
|
+
quiet: true)[:stdout].split("\n")
|
88
|
+
directory_with_configure(files.map { |p| Pathname.new(p) })
|
89
|
+
end
|
90
|
+
|
91
|
+
def directory_with_configure(paths)
|
92
|
+
paths.find { |p| p.basename == Pathname.new('configure') }.dirname
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_packages(release)
|
96
|
+
release[:packages].each do |pkg|
|
97
|
+
pkg_dir = make_package_dir(pkg[:name])
|
98
|
+
bp = blob_path(pkg)
|
99
|
+
|
100
|
+
generate_package_spec(pkg, pkg_dir, bp)
|
101
|
+
copy_blob_into_place(pkg, bp)
|
102
|
+
generate_package_script(pkg_dir, bp, pkg[:source_tarball])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def generate_package_script(pkg_dir, blob_path, source_tarball)
|
107
|
+
pkg_script = packaging_script(blob_path, build_path(source_tarball))
|
108
|
+
@fs.write_file(path: pkg_dir + 'packaging', content: pkg_script)
|
109
|
+
end
|
110
|
+
|
111
|
+
def blob_path(pkg)
|
112
|
+
"#{pkg[:name]}/#{pkg[:source_tarball].basename}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def make_package_dir(pkg_name)
|
116
|
+
pkg_dir = @release_dir + 'packages' + pkg_name
|
117
|
+
@fs.mkdir_p(pkg_dir)
|
118
|
+
pkg_dir
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate_package_spec(pkg, pkg_dir, blob_path)
|
122
|
+
@fs.write_file(path: pkg_dir + 'spec',
|
123
|
+
content: package_spec(pkg, blob_path))
|
124
|
+
end
|
125
|
+
|
126
|
+
def copy_blob_into_place(pkg, blob_path)
|
127
|
+
@fs.mkdir_p(@release_dir + 'blobs' + pkg[:name])
|
128
|
+
@fs.copy(pkg[:source_tarball],
|
129
|
+
Pathname.new(@release_dir + 'blobs' + blob_path))
|
130
|
+
end
|
131
|
+
|
132
|
+
def packaging_script(blob_path, build_dir_path)
|
133
|
+
"#!/bin/bash
|
134
|
+
set -e
|
135
|
+
set -u
|
136
|
+
|
137
|
+
tar zxvf #{blob_path}
|
138
|
+
cd #{build_dir_path}
|
139
|
+
|
140
|
+
./configure --prefix=${BOSH_INSTALL_TARGET}
|
141
|
+
|
142
|
+
make
|
143
|
+
make install"
|
144
|
+
end
|
145
|
+
|
146
|
+
def package_spec(pkg, blob_path)
|
147
|
+
YAML.dump(
|
148
|
+
'name' => pkg[:name],
|
149
|
+
'dependencies' => [],
|
150
|
+
'files' => [blob_path]
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module Boshify
|
3
|
+
class PackageNotFoundError < StandardError; end
|
4
|
+
class InvalidPackageMetadataError < StandardError; end
|
5
|
+
|
6
|
+
# Ubuntu package source
|
7
|
+
class UbuntuPackages
|
8
|
+
attr_reader :mirror_url
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@downloader = options[:downloader]
|
12
|
+
@cmd_runner = options[:cmd_runner]
|
13
|
+
self.mirror_url = options[:mirror_url] || 'http://us.archive.ubuntu.com/ubuntu'
|
14
|
+
end
|
15
|
+
|
16
|
+
def mirror_url=(mirror)
|
17
|
+
@mirror_url = URI.parse("#{mirror}/")
|
18
|
+
end
|
19
|
+
|
20
|
+
def refresh
|
21
|
+
@all_packages = packages_hash(
|
22
|
+
parse(decompress(download_sources_metadata)))
|
23
|
+
end
|
24
|
+
|
25
|
+
def source_tarball_url(package_name)
|
26
|
+
unless @all_packages[package_name]
|
27
|
+
fail PackageNotFoundError, "Package #{package_name} was not found"
|
28
|
+
end
|
29
|
+
pkg = @all_packages[package_name]
|
30
|
+
mirror_url + "#{pkg['Directory']}/#{original_tarball(pkg)}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(input)
|
34
|
+
group_package_values(
|
35
|
+
as_a_hash(
|
36
|
+
parse_multiline_values(
|
37
|
+
group_by_whether_pairs(
|
38
|
+
split_key_value_pairs(input)))))
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def original_tarball(pkg)
|
44
|
+
pkg['Files'].find { |f| f[:name].end_with?('orig.tar.gz') }[:name]
|
45
|
+
end
|
46
|
+
|
47
|
+
def download_sources_metadata
|
48
|
+
@downloader.get(mirror_url + 'dists/lucid/main/source/Sources.bz2')
|
49
|
+
end
|
50
|
+
|
51
|
+
def decompress(local_path)
|
52
|
+
result = @cmd_runner.run("bzcat #{local_path}", quiet: true)
|
53
|
+
if result[:exit_code] != 0
|
54
|
+
fail InvalidPackageMetadataError,
|
55
|
+
"Could not decompress: #{result[:stderr]}"
|
56
|
+
end
|
57
|
+
result[:stdout]
|
58
|
+
end
|
59
|
+
|
60
|
+
FILE_KEYS = %w(Files Checksums-Sha1 Checksums-Sha256)
|
61
|
+
|
62
|
+
def split_key_value_pairs(input)
|
63
|
+
input.lines.map { |line| line.split(':', 2).map { |f| f.strip } }
|
64
|
+
end
|
65
|
+
|
66
|
+
def group_by_whether_pairs(pairs)
|
67
|
+
pairs.chunk { |p| p.size == 2 }.to_a
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_multiline_values(pairs)
|
71
|
+
# rubocop:disable Next
|
72
|
+
pairs.each_with_index do |v, i|
|
73
|
+
if !v[0] && FILE_KEYS.include?(pairs[i - 1][1].last[0])
|
74
|
+
pairs[i - 1][1].last[1] = remove_empty(v[1]).map do |line|
|
75
|
+
file = line[0].split(' ')
|
76
|
+
{ name: file[2], size_bytes: file[1].to_i, checksum: file[0] }
|
77
|
+
end
|
78
|
+
pairs[i] = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
pairs.compact
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove_empty(lines)
|
85
|
+
lines.reject { |line| line[0].empty? }
|
86
|
+
end
|
87
|
+
|
88
|
+
def as_a_hash(pairs)
|
89
|
+
pairs.map { |b, p| Hash[p] if b }.compact
|
90
|
+
end
|
91
|
+
|
92
|
+
def group_package_values(pkgs)
|
93
|
+
last_pkg_index = -1
|
94
|
+
pkgs.each_with_index do |pkg, i|
|
95
|
+
if pkg.key?('Package')
|
96
|
+
last_pkg_index = i
|
97
|
+
else
|
98
|
+
pkgs[last_pkg_index].merge!(pkg)
|
99
|
+
pkgs[i] = nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
pkgs.compact
|
103
|
+
end
|
104
|
+
|
105
|
+
def packages_hash(pkgs)
|
106
|
+
Hash[pkgs.map do |pkg|
|
107
|
+
[pkg['Package'], pkg]
|
108
|
+
end]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/boshify.rb
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: boshify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Crump
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httparty
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.13.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.13.1
|
27
|
+
description: Generates BOSH releases
|
28
|
+
email: andrew@cloudcredo.com
|
29
|
+
executables:
|
30
|
+
- boshify
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- bin/boshify
|
35
|
+
- lib/boshify.rb
|
36
|
+
- lib/boshify/command_line.rb
|
37
|
+
- lib/boshify/downloader.rb
|
38
|
+
- lib/boshify/filesystem.rb
|
39
|
+
- lib/boshify/package_converter.rb
|
40
|
+
- lib/boshify/release_creator.rb
|
41
|
+
- lib/boshify/ubuntu_packages.rb
|
42
|
+
homepage: https://github.com/cloudcredo/boshify
|
43
|
+
licenses:
|
44
|
+
- Apache
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.2.2
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Generates BOSH releases
|
66
|
+
test_files: []
|