reaper-man 0.0.1 → 0.1.0

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.
@@ -0,0 +1,150 @@
1
+ require 'reaper-man'
2
+
3
+ module ReaperMan
4
+ class PackageList
5
+ class Processor
6
+ # Package list process for debian packages
7
+ class Deb < Processor
8
+
9
+ # @return [String]
10
+ attr_reader :origin
11
+ # @return [String]
12
+ attr_reader :dist
13
+ # @return [String]
14
+ attr_reader :component
15
+ # @return [Array<String>] architectures
16
+ attr_reader :all_map
17
+ # @return [String] prefix for package file location
18
+ attr_reader :package_root
19
+ # @return [String] namespace for packages
20
+ attr_reader :package_bucket
21
+
22
+ # default package root prefix
23
+ DEFAULT_ROOT = 'pool'
24
+ # default namespace for packages
25
+ DEFAULT_BUCKET = 'public'
26
+ # default architectures to define
27
+ DEFAULT_ALL_MAP = ['amd64', 'i386']
28
+
29
+ # Create new instance
30
+ #
31
+ # @param args [Hash]
32
+ # @option args [String] :origin
33
+ # @option args [String] :codename
34
+ # @option args [String] :component
35
+ # @option args [String] :package_root
36
+ # @option args [String] :package_bucket
37
+ # @option args [Array<String>] :all_map
38
+ def initialize(args={})
39
+ @origin = args[:origin].to_s
40
+ @dist = args[:codename].to_s
41
+ @component = args[:component].to_s
42
+ @package_root = args.fetch(:package_root, DEFAULT_ROOT)
43
+ @package_bucket = args.fetch(:package_bucket, DEFAULT_BUCKET)
44
+ if(dist.empty? || component.empty?)
45
+ raise 'Both `codename` and `component` must contain valid values'
46
+ end
47
+ @all_map = args.fetch(:all_map, DEFAULT_ALL_MAP)
48
+ end
49
+
50
+ # Add a package to the list
51
+ #
52
+ # @param conf [Hash]
53
+ # @param package [String] path to package
54
+ def add(hash, package)
55
+ info = extract_fields(package)
56
+ info.merge!(generate_checksums(package))
57
+ filenames = inject_package(hash, info, package)
58
+ filenames
59
+ end
60
+
61
+ # Remove package from the list
62
+ #
63
+ # @param conf [Hash] configuration hash
64
+ # @param package_name [String] name
65
+ # @param version [String]
66
+ def remove(hash, package_name, version, args={})
67
+ hash = hash.to_smash
68
+ arch = [args.fetch(:arch, all_map)].flatten.compact
69
+ deleted = false
70
+ arch.each do |arch_name|
71
+ arch_name = "binary-#{arch_name}"
72
+ if(hash.get(:apt, origin, dist, :components, component, arch_name, package_name))
73
+ if(version)
74
+ deleted = hash[:apt][origin][dist][:components][component][arch_name][package_name].delete(version)
75
+ else
76
+ deleted = hash[:apt][origin][dist][:components][component][arch_name].delete(package_name)
77
+ end
78
+ end
79
+ end
80
+ !!deleted
81
+ end
82
+
83
+ # Extract package metadata
84
+ #
85
+ # @param package [String] path to package
86
+ # @return [Hash]
87
+ def extract_fields(package)
88
+ content = shellout("dpkg-deb -f '#{package}'")
89
+ Smash[content.stdout.scan(/([^\s][^:]+):\s+(([^\n]|\n\s)+)/).map{|a| a.slice(0,2)}]
90
+ end
91
+
92
+ # Insert package information into package list
93
+ #
94
+ # @param hash [Hash] package list contents
95
+ # @param info [Hash] package information
96
+ # @param package [String] path to package file
97
+ # @return [Array<String>] package paths within package list contents
98
+ def inject_package(hash, info, package)
99
+ arch = info['Architecture']
100
+ arch = arch == 'all' ? all_map : [arch]
101
+ arch.map do |arch|
102
+ package_file_name = File.join(
103
+ package_root, package_bucket, origin,
104
+ dist, component, "binary-#{arch}",
105
+ File.basename(package)
106
+ )
107
+ hash.deep_merge!(
108
+ 'apt' => {
109
+ origin => {
110
+ dist => {
111
+ 'components' => {
112
+ component => {
113
+ "binary-#{arch}" => {
114
+ info['Package'] => {
115
+ info['Version'] => info.merge!(
116
+ 'Filename' => package_file_name,
117
+ 'Size' => File.size(package)
118
+ )
119
+ }
120
+ },
121
+ "binary-i386" => {
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ )
129
+ package_file_name
130
+ end
131
+ end
132
+
133
+ # Generate required checksums for given package
134
+ #
135
+ # @param package [String] path to package file
136
+ # @return [Hash] checksums
137
+ def generate_checksums(package)
138
+ File.open(package, 'r') do |pkg|
139
+ {
140
+ 'MD5sum' => checksum(pkg.rewind && pkg, :md5),
141
+ 'SHA1' => checksum(pkg.rewind && pkg, :sha1),
142
+ 'SHA256' => checksum(pkg.rewind && pkg, :sha256)
143
+ }
144
+ end
145
+ end
146
+
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,80 @@
1
+ require 'reaper-man'
2
+ require 'rubygems/package'
3
+
4
+ module ReaperMan
5
+ class PackageList
6
+ class Processor
7
+ class Gem < Processor
8
+
9
+ # Add a package to the list
10
+ #
11
+ # @param conf [Hash]
12
+ # @param package [String] path to package
13
+ def add(hash, package)
14
+ info = extract_fields(package)
15
+ filenames = inject_package(hash, info, package)
16
+ filenames
17
+ end
18
+
19
+ # Remove package from the list
20
+ #
21
+ # @param conf [Hash] configuration hash
22
+ # @param package_name [String] name
23
+ # @param version [String]
24
+ def remove(hash, package_name, version, args={})
25
+ deleted = false
26
+ if(hash['rubygems'][package_name])
27
+ if(version)
28
+ deleted = hash['rubygems'][package_name].delete(version)
29
+ else
30
+ deleted = hash['rubygems'].delete(package_name)
31
+ end
32
+ end
33
+ !!deleted
34
+ end
35
+
36
+ # Extract package metadata
37
+ #
38
+ # @param package [String] path to package
39
+ # @return [Hash]
40
+ def extract_fields(package)
41
+ spec = ::Gem::Package.open(File.open(package)){|pack| pack.metadata}
42
+ fields = Smash[
43
+ spec.to_yaml_properties.map do |var_name|
44
+ [var_name.to_s.tr('@', ''), spec.instance_variable_get(var_name)]
45
+ end
46
+ ]
47
+ fields['dependencies'] = fields['dependencies'].map do |dep|
48
+ [dep.name, dep.requirement.to_s]
49
+ end
50
+ fields
51
+ end
52
+
53
+ # Insert package information into package list
54
+ #
55
+ # @param hash [Hash] package list contents
56
+ # @param info [Hash] package information
57
+ # @param package [String] path to package file
58
+ # @return [Array<String>] package paths within package list contents
59
+ def inject_package(hash, info, package)
60
+ package_path = File.join(
61
+ 'rubygems', 'gems', "#{info['name']}-#{info['version']}.gem"
62
+ )
63
+ classification = info['version'].prerelease? ? 'prerelease' : 'release'
64
+ info['version'] = info['version'].version
65
+ hash.deep_merge!(
66
+ 'rubygem' => {
67
+ classification => {
68
+ info['name'] => {
69
+ info['version'].to_s => info.merge('package_path' => package_path)
70
+ }
71
+ }
72
+ }
73
+ )
74
+ package_path
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,11 @@
1
+ require 'reaper-man'
2
+
3
+ module ReaperMan
4
+ class PackageList
5
+ class Processor
6
+ class Rpm < Processor
7
+
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,127 @@
1
+ require 'multi_json'
2
+ require 'reaper-man'
3
+
4
+ module ReaperMan
5
+ # Package list for repository
6
+ class PackageList
7
+ # Package list modification processor
8
+ class Processor
9
+ autoload :Rpm, 'reaper-man/package_list/rpm'
10
+ autoload :Deb, 'reaper-man/package_list/deb'
11
+ autoload :Gem, 'reaper-man/package_list/gem'
12
+
13
+ include Utils::Process
14
+ include Utils::Checksum
15
+
16
+ # Add a package to the list
17
+ #
18
+ # @param conf [Hash]
19
+ # @param package [String] path to package
20
+ def add(conf, package)
21
+ raise NoMethodError.new 'Not implemented'
22
+ end
23
+
24
+ # Remove package from the list
25
+ #
26
+ # @param conf [Hash] configuration hash
27
+ # @param package_name [String] name
28
+ # @param version [String]
29
+ def remove(conf, package_name, version=nil)
30
+ raise NoMethodError.new 'Not implemented'
31
+ end
32
+ end
33
+
34
+ # @return [String] path to list file
35
+ attr_reader :path
36
+ # @return [Hash] configuration
37
+ attr_reader :options
38
+ # @return [Time] package list mtime
39
+ attr_reader :init_mtime
40
+ # @return [Hash] content of package list
41
+ attr_reader :content
42
+
43
+ # Create new instance
44
+ #
45
+ # @param path [String] path to package list
46
+ # @param args [Hash] configuration
47
+ def initialize(path, args={})
48
+ @path = path
49
+ @options = args.dup
50
+ @content = Smash.new
51
+ init_list!
52
+ end
53
+
54
+ # Add package to package list file
55
+ #
56
+ # @param package [Array<String>] path to package file
57
+ def add_package(package)
58
+ [package_handler(File.extname(package).tr('.', '')).add(content, package)].flatten.compact
59
+ end
60
+
61
+ # Remove package from the package list file
62
+ #
63
+ # @param package [String] name of package
64
+ # @param version [String] version of file
65
+ def remove_package(package, version=nil)
66
+ ext = File.extname(package).tr('.', '')
67
+ if(ext.empty?)
68
+ ext = %w(deb) # rpm)
69
+ else
70
+ ext = [ext]
71
+ end
72
+ ext.each do |ext_name|
73
+ package_handler(ext_name).remove(content, package, version)
74
+ end
75
+ end
76
+
77
+ # @return [String] serialized content
78
+ def serialize
79
+ MultiJson.dump(content)
80
+ end
81
+
82
+ # Write contents to package list file
83
+ #
84
+ # @return [Integer] number of bytes written
85
+ def write!
86
+ new_file = !File.exists?(path)
87
+ File.open(path, File::CREAT|File::RDWR) do |file|
88
+ file.flock(File::LOCK_EX)
89
+ if(!new_file && init_mtime != file.mtime)
90
+ file.rewind
91
+ content.deep_merge!(
92
+ MultiJson.load(
93
+ file.read
94
+ )
95
+ )
96
+ file.rewind
97
+ end
98
+ pos = file.write MultiJson.dump(content, :pretty => true)
99
+ file.truncate(pos)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # @return [Processor] processor for give package type
106
+ def package_handler(pkg_ext)
107
+ Processor.const_get(pkg_ext.capitalize).new(options)
108
+ end
109
+
110
+ # Initialize the package list file
111
+ #
112
+ # @return [Hash] loaded file contents
113
+ def init_list!
114
+ write! unless File.exist?(path)
115
+ @init_mtime = File.mtime(path)
116
+ content.deep_merge!(
117
+ MultiJson.load(
118
+ File.open(path, 'r') do |file|
119
+ file.flock(File::LOCK_SH)
120
+ file.read
121
+ end
122
+ )
123
+ )
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,47 @@
1
+ require 'reaper-man'
2
+
3
+ module ReaperMan
4
+ class Signer
5
+ # Signing methods for deb files
6
+ module Deb
7
+
8
+ # command to use for file signing
9
+ SIGN_COMMAND = File.join(
10
+ File.expand_path(File.join(File.dirname(__FILE__), '..')),
11
+ 'util-scripts/auto-debsigs'
12
+ )
13
+
14
+ # Sign given files
15
+ #
16
+ # @param pkgs [String] list of file paths
17
+ # @return [TrueClass]
18
+ def package(*pkgs)
19
+ pkgs = valid_packages(*pkgs)
20
+ pkgs.each_slice(sign_chunk_size) do |pkgs|
21
+ if(key_password)
22
+ shellout(
23
+ "#{SIGN_COMMAND} #{sign_type} #{key_id} #{pkgs.join(' ')}",
24
+ :environment => {
25
+ 'REAPER_KEY_PASSWORD' => key_password
26
+ }
27
+ )
28
+ else
29
+ shellout(%w(debsigs --sign="#{sign_type}" --default-key="#{key_id}" #{pkgs.join(' ')}))
30
+ end
31
+ end
32
+ true
33
+ end
34
+
35
+ # Filter only valid paths for signing (.deb extensions)
36
+ #
37
+ # @param pkgs [String] list of file paths
38
+ # @return [Array<String>]
39
+ def valid_packages(*pkgs)
40
+ pkgs.find_all do |pkg|
41
+ File.extname(pkg) == '.deb'
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ require 'reaper-man'
2
+
3
+ module ReaperMan
4
+ class Signer
5
+ module Rubygems
6
+
7
+ def package(*pkgs)
8
+ nil
9
+ end
10
+
11
+ def valid_packages(*pkgs)
12
+ nil
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,62 @@
1
+ require 'reaper-man'
2
+
3
+ module ReaperMan
4
+ # File signer
5
+ class Signer
6
+
7
+ autoload :Rpm, 'reaper-man/signer/rpm'
8
+ autoload :Deb, 'reaper-man/signer/deb'
9
+ autoload :Rubygems, 'reaper-man/signer/rubygems'
10
+
11
+ include Utils::Process
12
+
13
+ attr_reader :key_id
14
+ attr_reader :sign_chunk_size
15
+ attr_reader :sign_type
16
+ attr_reader :package_system
17
+ attr_reader :key_password
18
+
19
+ # Create new instance
20
+ #
21
+ # @param args [Hash]
22
+ # @option args [String] :signing_key
23
+ # @option args [String] :signing_chunk_size (defaults to 1)
24
+ # @option args [String] :signing_type (defaults to 'origin')
25
+ # @option args [String] :key_password (defaults to `ENV['REAPER_KEY_PASSWORD']`)
26
+ # @option args [String] :package_system
27
+ def initialize(args={})
28
+ args = args.to_smash
29
+ @key_id = args[:signing_key]
30
+ @sign_chunk_size = args.fetch(:signing_chunk_size, 1)
31
+ @sign_type = args.fetch(:signing_type, 'origin')
32
+ @key_password = args.fetch(:key_password, ENV['REAPER_KEY_PASSWORD'])
33
+ @package_system = args[:package_system]
34
+ case package_system.to_sym
35
+ when :deb, :apt
36
+ extend Deb
37
+ when :rpm, :yum
38
+ extend Rpm
39
+ when :gem, :rubygems
40
+ extend Rubygems
41
+ else
42
+ raise TypeError.new "Unknown packaging type requested (#{package_system})"
43
+ end
44
+ end
45
+
46
+ # Sign the file
47
+ #
48
+ # @param src [String] path to source file
49
+ # @param dst [String] path for destination file
50
+ # @return [String] destination file path
51
+ def file(src, dst=nil)
52
+ opts = ['--detach-sign', '--armor']
53
+ dst ||= src.sub(/#{Regexp.escape(File.extname(src))}$/, '.gpg')
54
+ opts << "--output '#{dst}'"
55
+ cmd = (['gpg'] + opts + [src]).join(' ')
56
+ shellout!(cmd)
57
+ dst
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/expect -f
2
+
3
+ log_user 0
4
+
5
+ set timeout 3
6
+
7
+ set signtype [lindex $argv 0]
8
+ set signkey [lindex $argv 1]
9
+ set package [join [lrange $argv 2 [llength $argv]] " "]
10
+
11
+ spawn /bin/bash
12
+ expect {
13
+ -re ".*\$ *$" {
14
+ send "/usr/bin/debsigs --sign=\"$signtype\" --default-key=\"$signkey\" $package\r"
15
+ }
16
+ timeout { exit 1 }
17
+ }
18
+ expect {
19
+ -re {Enter passphrase: *} {
20
+ send "$env(REAPER_KEY_PASSWORD)\r"
21
+ }
22
+ timeout { exit 2 }
23
+ }
24
+ expect {
25
+ -re {\$ *} {
26
+ send "exit $?\rexit $?\r"
27
+ }
28
+ timeout { exit 3 }
29
+ }
30
+ expect eof
@@ -0,0 +1,114 @@
1
+ require 'reaper-man'
2
+ require 'childprocess'
3
+ require 'shellwords'
4
+ require 'tempfile'
5
+
6
+ module ReaperMan
7
+ module Utils
8
+ # Shellout helper
9
+ module Process
10
+
11
+ # NOTE: This is extracted from the elecksee gem and some
12
+ # features removed that are not required here. Should be
13
+ # wrapped up into standalone gem so it's more reusable.
14
+
15
+ class CommandFailed < StandardError
16
+ attr_accessor :original, :result
17
+ def initialize(orig, result=nil)
18
+ @original = orig
19
+ @result = result
20
+ super(orig.to_s)
21
+ end
22
+ end
23
+
24
+ class Timeout < CommandFailed
25
+ end
26
+
27
+ class CommandResult
28
+ attr_reader :original, :stdout, :stderr
29
+ def initialize(result)
30
+ @original = result
31
+ if(result.class.ancestors.map(&:to_s).include?('ChildProcess::AbstractProcess'))
32
+ extract_childprocess
33
+ elsif(result.class.to_s == 'Mixlib::ShellOut')
34
+ extract_shellout
35
+ else
36
+ raise TypeError.new("Unknown process result type received: #{result.class}")
37
+ end
38
+ end
39
+
40
+ def extract_childprocess
41
+ original.io.stdout.rewind
42
+ original.io.stderr.rewind
43
+ @stdout = original.io.stdout.read
44
+ @stderr = original.io.stderr.read
45
+ original.io.stdout.delete
46
+ original.io.stderr.delete
47
+ end
48
+
49
+ def extract_shellout
50
+ @stdout = original.stdout
51
+ @stderr = original.stderr
52
+ end
53
+ end
54
+
55
+ # Simple helper to shell out
56
+ def shellout(cmd, args={})
57
+ result = nil
58
+ if(defined?(Mixlib))
59
+ cmd_type = :mixlib_shellout
60
+ else
61
+ cmd_type = :childprocess
62
+ end
63
+ com_block = nil
64
+ case cmd_type
65
+ when :childprocess
66
+ com_block = lambda{ child_process_command(cmd, args) }
67
+ when :mixlib_shellout
68
+ require 'mixlib/shellout'
69
+ com_block = lambda{ mixlib_shellout_command(cmd, args) }
70
+ else
71
+ raise ArgumentError.new("Unknown shellout helper provided: #{cmd_type}")
72
+ end
73
+ result = defined?(Bundler) ? Bundler.with_clean_env{ com_block.call } : com_block.call
74
+ result == false ? false : CommandResult.new(result)
75
+ end
76
+
77
+ def child_process_command(cmd, args)
78
+ s_out = Tempfile.new('stdout')
79
+ s_err = Tempfile.new('stderr')
80
+ s_out.sync
81
+ s_err.sync
82
+ c_proc = ChildProcess.build(*Shellwords.split(cmd))
83
+ c_proc.environment.merge(args.fetch(:environment, {}))
84
+ c_proc.io.stdout = s_out
85
+ c_proc.io.stderr = s_err
86
+ c_proc.start
87
+ begin
88
+ c_proc.poll_for_exit(args[:timeout] || 10)
89
+ rescue ChildProcess::TimeoutError
90
+ c_proc.stop
91
+ ensure
92
+ raise CommandFailed.new("Command failed: #{cmd}", CommandResult.new(c_proc)) if c_proc.crashed?
93
+ end
94
+ c_proc
95
+ end
96
+
97
+ def mixlib_shellout_command(cmd, args)
98
+ shlout = nil
99
+ begin
100
+ shlout = Mixlib::ShellOut.new(cmd,
101
+ :timeout => args[:timeout] || 10,
102
+ :environment => args.fetch(:environment, {})
103
+ )
104
+ shlout.run_command
105
+ shlout.error!
106
+ shlout
107
+ rescue Mixlib::ShellOut::ShellCommandFailed, CommandFailed, Mixlib::ShellOut::CommandTimeout => e
108
+ raise CommandFailed.new(e, CommandResult.new(shlout))
109
+ end
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,27 @@
1
+ require 'digest/sha1'
2
+ require 'digest/sha2'
3
+ require 'digest/md5'
4
+
5
+ require 'reaper-man'
6
+
7
+ module ReaperMan
8
+ # Helper utilities
9
+ module Utils
10
+
11
+ autoload :Process, 'reaper-man/utils/process'
12
+
13
+ # Checksum helper
14
+ module Checksum
15
+
16
+ def checksum(io, type)
17
+ digest = Digest.const_get(type.to_s.upcase).new
18
+ while(data = io.read(2048))
19
+ digest << data
20
+ end
21
+ digest.hexdigest
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
data/lib/reaper-man.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'multi_json'
2
+
3
+ module ReaperMan
4
+ autoload :Cli, 'reaper-man/cli'
5
+ autoload :Command, 'reaper-man/command'
6
+ autoload :Config, 'reaper-man/config'
7
+ autoload :Error, 'reaper-man/error'
8
+ autoload :Generator, 'reaper-man/generator'
9
+ autoload :PackageList, 'reaper-man/package_list'
10
+ autoload :Signer, 'reaper-man/signer'
11
+ autoload :Version, 'reaper-man/version'
12
+ autoload :Utils, 'reaper-man/utils'
13
+ end