boshify 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ require_relative 'boshify/command_line'
2
+ require_relative 'boshify/downloader'
3
+ require_relative 'boshify/filesystem'
4
+ require_relative 'boshify/package_converter'
5
+ require_relative 'boshify/release_creator'
6
+ require_relative 'boshify/ubuntu_packages'
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: []