right_publish 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format=nested
3
+ --backtrace
4
+ --debugger
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "builder"
4
+ gem "fog", "~> 1.9"
5
+ gem "trollop", "~> 2.0"
6
+
7
+ # Gems used to test and develop RightPublish
8
+ group :development do
9
+ gem "rake", "~> 0.9"
10
+ gem "jeweler", "~> 1.8.3"
11
+ gem "right_develop", "~> 1.0",
12
+ :git => "git@github.com:rightscale/right_develop.git",
13
+ :branch => "master"
14
+ gem "rdoc", ">= 2.4.2"
15
+ gem "rspec", "~> 2.0"
16
+ gem "flexmock", "~> 0.9"
17
+ gem "simplecov"
18
+ gem "ruby-debug", ">= 0.10", :platforms => :ruby_18
19
+ gem "ruby-debug19", ">= 0.11.6", :platforms => :ruby_19
20
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,114 @@
1
+ GIT
2
+ remote: git@github.com:rightscale/right_develop.git
3
+ revision: 90a8fde986145756f341088130c6e1aac4759d68
4
+ branch: master
5
+ specs:
6
+ right_develop (1.1.0)
7
+ actionpack (>= 2.3.0, < 4.0)
8
+ builder (~> 3.0)
9
+ cucumber (~> 1.0)
10
+ rake (>= 0.8.7, < 0.10)
11
+ right_support (~> 2.0)
12
+ rspec (>= 1.3, < 3.0)
13
+ trollop (>= 1.0, < 3.0)
14
+
15
+ GEM
16
+ remote: http://rubygems.org/
17
+ specs:
18
+ actionpack (2.3.18)
19
+ activesupport (= 2.3.18)
20
+ rack (~> 1.1.0)
21
+ activesupport (2.3.18)
22
+ archive-tar-minitar (0.5.2)
23
+ builder (3.2.0)
24
+ columnize (0.3.6)
25
+ cucumber (1.2.5)
26
+ builder (>= 2.1.2)
27
+ diff-lcs (>= 1.1.3)
28
+ gherkin (~> 2.11.7)
29
+ multi_json (~> 1.3)
30
+ diff-lcs (1.2.3)
31
+ excon (0.20.1)
32
+ flexmock (0.9.0)
33
+ fog (1.10.1)
34
+ builder
35
+ excon (~> 0.20)
36
+ formatador (~> 0.2.0)
37
+ mime-types
38
+ multi_json (~> 1.0)
39
+ net-scp (~> 1.1)
40
+ net-ssh (>= 2.1.3)
41
+ nokogiri (~> 1.5.0)
42
+ ruby-hmac
43
+ formatador (0.2.4)
44
+ gherkin (2.11.8)
45
+ multi_json (~> 1.3)
46
+ git (1.2.5)
47
+ jeweler (1.8.4)
48
+ bundler (~> 1.0)
49
+ git (>= 1.2.5)
50
+ rake
51
+ rdoc
52
+ json (1.7.7)
53
+ linecache (0.46)
54
+ rbx-require-relative (> 0.0.4)
55
+ linecache19 (0.5.12)
56
+ ruby_core_source (>= 0.1.4)
57
+ mime-types (1.22)
58
+ multi_json (1.7.2)
59
+ net-scp (1.1.0)
60
+ net-ssh (>= 2.6.5)
61
+ net-ssh (2.6.7)
62
+ nokogiri (1.5.9)
63
+ rack (1.1.6)
64
+ rake (0.9.6)
65
+ rbx-require-relative (0.0.9)
66
+ rdoc (4.0.1)
67
+ json (~> 1.4)
68
+ right_support (2.6.17)
69
+ rspec (2.13.0)
70
+ rspec-core (~> 2.13.0)
71
+ rspec-expectations (~> 2.13.0)
72
+ rspec-mocks (~> 2.13.0)
73
+ rspec-core (2.13.1)
74
+ rspec-expectations (2.13.0)
75
+ diff-lcs (>= 1.1.3, < 2.0)
76
+ rspec-mocks (2.13.1)
77
+ ruby-debug (0.10.4)
78
+ columnize (>= 0.1)
79
+ ruby-debug-base (~> 0.10.4.0)
80
+ ruby-debug-base (0.10.4)
81
+ linecache (>= 0.3)
82
+ ruby-debug-base19 (0.11.25)
83
+ columnize (>= 0.3.1)
84
+ linecache19 (>= 0.5.11)
85
+ ruby_core_source (>= 0.1.4)
86
+ ruby-debug19 (0.11.6)
87
+ columnize (>= 0.3.1)
88
+ linecache19 (>= 0.5.11)
89
+ ruby-debug-base19 (>= 0.11.19)
90
+ ruby-hmac (0.4.0)
91
+ ruby_core_source (0.1.5)
92
+ archive-tar-minitar (>= 0.5.2)
93
+ simplecov (0.7.1)
94
+ multi_json (~> 1.0)
95
+ simplecov-html (~> 0.7.1)
96
+ simplecov-html (0.7.1)
97
+ trollop (2.0)
98
+
99
+ PLATFORMS
100
+ ruby
101
+
102
+ DEPENDENCIES
103
+ builder
104
+ flexmock (~> 0.9)
105
+ fog (~> 1.9)
106
+ jeweler (~> 1.8.3)
107
+ rake (~> 0.9)
108
+ rdoc (>= 2.4.2)
109
+ right_develop (~> 1.0)!
110
+ rspec (~> 2.0)
111
+ ruby-debug (>= 0.10)
112
+ ruby-debug19 (>= 0.11.6)
113
+ simplecov
114
+ trollop (~> 2.0)
data/README.rdoc ADDED
@@ -0,0 +1,113 @@
1
+ = RightPublish
2
+
3
+ = DESCRIPTION
4
+
5
+ == Synopsis
6
+
7
+ RightPublish facilitates the management of remote package repositories. It is in
8
+ essense a wrapper around package repository management tools such as YUM and
9
+ RubyGems allowing one to add/remove/replace packages stored in a repository
10
+ residing in a remote storage location, typically Amazon's S3. The current
11
+ incantation of RightPublish can manage RubyGem, YUM (RPM) and APT (DEB) pacakge
12
+ repositories.
13
+
14
+ == Interface
15
+
16
+ RightPublish is used as a command line tool and controlled via command line options
17
+ and a YAML configuration file known as a RightPublish <i>profile</i>.
18
+
19
+ === Profile Format
20
+
21
+ ###########################################################
22
+ # RightPublish Profile
23
+ #
24
+ # + Attribute is required, example shown
25
+ # * Attribute is optional, no default; example shown
26
+ # - Attribute is optional, default shown
27
+ #
28
+ # Values of the form "@@...@@" indicate an environment
29
+ # variable should be substituted in place of the value.
30
+ # These substitution sequences can appear as the entire
31
+ # value of an attribute, or they can appear anywhere in
32
+ # the attribute with static text surrounding them.
33
+ #
34
+ :verbose: # -: false
35
+ :apt_repo:
36
+ :dists: # *: [ woody, sid, etch, jaunty, lucid ]
37
+ :auto: # -: true
38
+ :subdir: # -: apt/
39
+ :gpg_key_id: # *: 9A917D05
40
+ :gpg_password: # *: @@GPG_PASSWORD@@
41
+ :gem_repo:
42
+ :subdir: # -: gems/
43
+ :yum_repo:
44
+ :dists: # *: el: ['6']
45
+ :epel: # -: 1
46
+ :subdir: # -: yum/
47
+ :gpg_key_id: # *: 9A917D05
48
+ :gpg_password: # *: @@GPG_PASSWORD@@
49
+ :local_storage:
50
+ :cache_dir # -: ~/.rp_cache
51
+ :remote_storage:
52
+ :storage_provider: # -: AWS
53
+ :access_id: # *: @@AWS_ACCESS_KEY_ID@@
54
+ :access_key: # *: @@AWS_SECRET_ACCESS_KEY@@
55
+ :remote_path: # +: rightlink-testing
56
+ #
57
+ # End of RightPublish Profile
58
+ ###########################################################
59
+
60
+ === Command Line Usage
61
+
62
+ RightPublish can manage a YUM/APT/RubyGem repository in remote storage, e.g. S3.
63
+ If autosigning packages import the private key beforehand with gpg --import. If
64
+ no gpg_password config key is supplied but a gpg_key_id is it should prompt you
65
+ for a password.
66
+
67
+ Usage:
68
+ right_publish [global_options] <command> [cmd_options]
69
+ commands:
70
+ fetch
71
+ publish
72
+ store
73
+ global options:
74
+ --profile, -p <s>: Publish profile
75
+ --repo-type, -r <s>: Repository type: ["apt", "gem", "yum"]
76
+ --dist, -d: Target dist: ["el/6", "precise"]
77
+ --verbose, -v: Verbose output
78
+ --version, -e: Print version and exit
79
+ --help, -h: Show this message
80
+
81
+
82
+ == Supported Configuration
83
+
84
+ RightPublish has been tested on EL6, Ubuntu 12.10 and ArchLinux (as of March `13).
85
+
86
+ == Work in Progress
87
+
88
+ RightPublish is work in progress, expect more documentation and examples in the near future.
89
+
90
+ = LICENSE
91
+
92
+ <b>RightPublish</b>
93
+
94
+ Copyright:: Copyright (c) 2013 RightScale, Inc.
95
+
96
+ Permission is hereby granted, free of charge, to any person obtaining
97
+ a copy of this software and associated documentation files (the
98
+ 'Software'), to deal in the Software without restriction, including
99
+ without limitation the rights to use, copy, modify, merge, publish,
100
+ distribute, sublicense, and/or sell copies of the Software, and to
101
+ permit persons to whom the Software is furnished to do so, subject to
102
+ the following conditions:
103
+
104
+ The above copyright notice and this permission notice shall be
105
+ included in all copies or substantial portions of the Software.
106
+
107
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
108
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
109
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
110
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
111
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
112
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
113
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ # -*-ruby-*-
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+
5
+ require 'rake'
6
+ require 'rdoc/task'
7
+ require 'rubygems/package_task'
8
+
9
+ require 'rake/clean'
10
+ require 'rspec/core/rake_task'
11
+
12
+ require 'right_develop/ci/rake_task'
13
+
14
+ desc "Run unit tests"
15
+ task :default => :spec
16
+
17
+ desc "Run unit tests"
18
+ RSpec::Core::RakeTask.new do |t|
19
+ t.pattern = Dir['**/*_spec.rb']
20
+ end
21
+
22
+ desc 'Generate documentation for the right_publish gem.'
23
+ Rake::RDocTask.new(:rdoc) do |rdoc|
24
+ rdoc.rdoc_dir = 'doc'
25
+ rdoc.title = 'RightPublish'
26
+ rdoc.options << '--line-numbers' << '--inline-source'
27
+ rdoc.rdoc_files.include('README.rdoc')
28
+ rdoc.rdoc_files.include('lib/**/*.rb')
29
+ rdoc.rdoc_files.exclude('features/**/*')
30
+ rdoc.rdoc_files.exclude('spec/**/*')
31
+ end
32
+
33
+ require 'jeweler'
34
+ Jeweler::Tasks.new do |gem|
35
+ # gem is a Gem::Specification; see http://docs.rubygems.org/read/chapter/20 for more options
36
+ gem.name = "right_publish"
37
+ gem.homepage = "https://github.com/rightscale/right_publish"
38
+ gem.license = "Proprietary"
39
+ gem.summary = %Q{Package publishing and indexing tool}
40
+ gem.description = %Q{A tool for maintaining S3-based DEB, GEM and RPM packages.}
41
+ gem.email = "support@rightscale.com"
42
+ gem.authors = ['Brian Szmyd', 'Tony Spataro']
43
+ end
44
+ Jeweler::RubygemsDotOrgTasks.new
45
+
46
+ CLEAN.include('pkg')
47
+
48
+ RightDevelop::CI::RakeTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/bin/right_publish ADDED
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ #
4
+
5
+ $: << File.expand_path("../../lib",__FILE__)
6
+
7
+ require 'right_publish'
8
+ RightPublish::Main.run()
@@ -0,0 +1,96 @@
1
+ require 'singleton'
2
+ require 'yaml'
3
+
4
+ module RightPublish
5
+
6
+ class Profile
7
+
8
+ # Configuration defaults
9
+ # =============
10
+ DEFAULT_VERBOSE = false
11
+ # =============
12
+
13
+ attr_accessor :settings
14
+ include Singleton
15
+
16
+ def self.config()
17
+ return Profile.instance.settings
18
+ end
19
+
20
+ def self.log(s, level=:info)
21
+ puts(s) if Profile.config[:verbose] || level != :debug
22
+ end
23
+
24
+ def load(path)
25
+ begin
26
+ if path && File.exists?(path)
27
+ begin
28
+ @settings ||= Profile.symbolize_profile(YAML.load_file(path))
29
+ validate_profile
30
+ rescue Exception => e
31
+ raise LoadError.new("Bad Format: #{path}\n#{e}")
32
+ end
33
+ else
34
+ raise LoadError.new("Missing Profile")
35
+ end
36
+ rescue LoadError => e
37
+ raise LoadError.new("#{e}")
38
+ end
39
+ nil
40
+ end
41
+
42
+ def register_section(section_key, section_options)
43
+ @config_map ||= {}
44
+ @config_map[section_key] ||= section_options
45
+ end
46
+
47
+ private
48
+
49
+ def self.symbolize_profile(args)
50
+ if args.is_a? Hash
51
+ Hash[ args.collect do |key, value|
52
+ [key.to_sym, symbolize_profile(value)]
53
+ end ]
54
+ else
55
+ args
56
+ end
57
+ end
58
+
59
+ SUBSTITUTION = /@@(\w+)@@/
60
+
61
+ def check_section(section, attrs)
62
+ provider = (attrs[:provider] && attrs[:provider]) || section
63
+ raise LoadError.new("Unknown provider #{attrs[:provider]} for: #{section}") unless @config_map[provider]
64
+
65
+ @settings[section] ||= {}
66
+ @config_map[provider].each_pair do |attr, default|
67
+ setting = @settings[section][attr]
68
+
69
+ if setting.nil? && (default != :attr_optional)
70
+ if default == :attr_needed
71
+ raise LoadError.new("Missing attribute #{section}:#{attr}")
72
+ end
73
+ @settings[section][attr] = default
74
+ end
75
+
76
+ # Perform environment-variable substitition if the setting is a leaf node
77
+ if setting.is_a?(String)
78
+ while match = SUBSTITUTION.match(setting)
79
+ if ENV.key?(match[1])
80
+ setting = setting.gsub(match[0], ENV[match[1]])
81
+ else
82
+ raise LoadError.new("Missing environment variable #{match[1]}.")
83
+ end
84
+ end
85
+ end
86
+
87
+ @settings[section][attr] = setting
88
+ end
89
+ end
90
+
91
+ def validate_profile()
92
+ @settings[:verbose] = DEFAULT_VERBOSE unless @settings[:verbose]
93
+ @settings.each_pair { |section, attrs| check_section(section, attrs) if attrs.is_a? Hash }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,159 @@
1
+ require 'right_publish/profile'
2
+ require 'right_publish/storage'
3
+ require 'pty'
4
+ require 'expect'
5
+
6
+ module RightPublish
7
+
8
+ class RepoManager
9
+ @@repository_type_regex = /\A(\w+)_repo/
10
+ @@repository_table = {}
11
+
12
+ def self.get_repository(type)
13
+ @@repository_table[type].new(type) if @@repository_table[type]
14
+ end
15
+
16
+ def self.repo_types()
17
+ type_hash = {}
18
+ @@repository_table.each_key { |k| type_hash[@@repository_type_regex.match(k.to_s)[1]] = k }
19
+ type_hash
20
+ end
21
+
22
+ def self.register_repo(repo_type)
23
+ repo_key = repo_type::REPO_KEY
24
+
25
+ if repo_type.respond_to?(:new) && @@repository_type_regex.match(repo_key.to_s)
26
+ @@repository_table[repo_key] = repo_type
27
+ RightPublish::Profile.instance.register_section(repo_key, repo_type::REPO_OPTIONS)
28
+ else
29
+ raise TypeError
30
+ end
31
+
32
+ nil
33
+ end
34
+ end
35
+
36
+ module Repo
37
+ def initialize(option_key)
38
+ @repository_type ||= option_key
39
+ end
40
+
41
+ def fetch()
42
+ Profile.log("Fetching latest #{repo_human_name(@repository_type)} data...")
43
+ begin
44
+ sync_dirs(
45
+ get_storage(Profile.config[:remote_storage][:provider]),
46
+ get_storage(Profile.config[:local_storage][:provider]),
47
+ Profile.config[@repository_type][:subdir] )
48
+ rescue Exception => e
49
+ RightPublish::Profile.log("Could not synchronize storage:\n\t#{e}", :error)
50
+ raise RuntimeError, "fetch from remote failed."
51
+ end
52
+ end
53
+
54
+ def store()
55
+ Profile.log("Commiting local #{repo_human_name(@repository_type)} data...")
56
+ begin
57
+ sync_dirs(
58
+ get_storage(Profile.config[:local_storage][:provider]),
59
+ get_storage(Profile.config[:remote_storage][:provider]),
60
+ Profile.config[@repository_type][:subdir] )
61
+ rescue Exception => e
62
+ RightPublish::Profile.log("Could not sychronize storage:\n\t#{e}", :error)
63
+ raise RuntimeError, "store to remote failed."
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def build_glob(ext)
70
+ # Transform array into glob compliant pattern
71
+ '{'.concat Array(ext).join(',').concat '}'
72
+ end
73
+
74
+ def do_in_subdir(subdir)
75
+ full_path = File.expand_path(File.join(Profile.config[:local_storage][:cache_dir], subdir))
76
+ FileUtils.mkdir_p full_path
77
+
78
+ Dir.chdir(full_path) { yield }
79
+ end
80
+
81
+ def get_pkg_list(file_or_dir, ext=nil)
82
+ pkg_list = []
83
+ file_or_dir = Array(file_or_dir)
84
+ file_or_dir.each do |path|
85
+ path = path.gsub("\\", "/")
86
+ pkg_list << if File.directory?(path)
87
+ glob_filter = (ext && "*.#{build_glob(ext)}") || "*"
88
+ Dir.glob(File.join(path, glob_filter))
89
+ else
90
+ path.split(',')
91
+ end
92
+ end
93
+ pkg_list.flatten!
94
+
95
+ fail("No packages found") if pkg_list.empty?
96
+ pkg_list.each do |pkg_path|
97
+ fail("\"#{pkg_path}\" does not appear to be a valid package for the repository.") \
98
+ unless File.file?(pkg_path) && Array(ext).any? { |e| pkg_path.end_with?(e) }
99
+ yield pkg_path if block_given?
100
+ end if ext
101
+
102
+ pkg_list
103
+ end
104
+
105
+ # For automation, we want to send the password, however gpg uses getpass
106
+ # c function, which interacts directly with /dev/pty instead of stdin/stdout
107
+ # to hide the password as its typed in. So, we need to allocate a pty.
108
+ def shellout_with_password(cmd)
109
+ password = repo_config[:gpg_password]
110
+ raise Exception, ":gpg_password must be supplied when signing packages" unless password
111
+
112
+ begin
113
+ PTY.spawn(cmd) do |stdin,stdout,pid|
114
+ while output = stdin.expect(/pass.?phrase/i, 2)
115
+ stdout.puts(password)
116
+ Profile.log(output, :debug)
117
+ end
118
+ Process.wait(pid)
119
+ end
120
+ status = $?
121
+ rescue PTY::ChildExited => e
122
+ status = e.status
123
+ end
124
+ return status.success?
125
+ end
126
+
127
+ def get_storage(provider)
128
+ type = RightPublish::StorageManager.storage_types[provider]
129
+ RightPublish::StorageManager.get_storage(type)
130
+ end
131
+
132
+ def install_file(file, dest)
133
+ Profile.log("#{file} => #{dest}")
134
+ local_dir = get_storage(Profile.config[:local_storage][:provider]).get_directories
135
+ File.open(file, "rb") { |chunk| local_dir.files.create(:key=>File.join(dest, File.basename(file)), :body=>chunk) }
136
+ end
137
+
138
+ def prune_all(glob_pattern)
139
+ Profile.log("Pruning: #{glob_pattern}")
140
+ Dir.glob(glob_pattern) { |f| File.unlink(f) }
141
+ end
142
+
143
+ def repo_config()
144
+ Profile.config[@repository_type]
145
+ end
146
+
147
+ def repo_human_name(type)
148
+ type.to_s.sub(/_/,' ')
149
+ end
150
+
151
+ def sync_dirs(src, dest, subdir='')
152
+ RightPublish::Storage.sync_dirs(src, dest, :subdir=>subdir, :sweep=>true)
153
+ end
154
+ end
155
+ end
156
+
157
+ require 'right_publish/repos/apt'
158
+ require 'right_publish/repos/gem'
159
+ require 'right_publish/repos/yum'