apt_control 0.3.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.
- data/bin/apt_control +5 -0
- data/lib/apt_control.rb +85 -0
- data/lib/apt_control/apt_site.rb +31 -0
- data/lib/apt_control/build_archive.rb +116 -0
- data/lib/apt_control/cli.rb +169 -0
- data/lib/apt_control/cli/include.rb +33 -0
- data/lib/apt_control/cli/status.rb +43 -0
- data/lib/apt_control/cli/watch.rb +95 -0
- data/lib/apt_control/control_file.rb +142 -0
- data/lib/apt_control/exec.rb +121 -0
- data/lib/apt_control/notify.rb +37 -0
- data/lib/apt_control/version.rb +3 -0
- metadata +176 -0
data/bin/apt_control
ADDED
data/lib/apt_control.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'inifile'
|
3
|
+
require 'listen'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module AptControl
|
7
|
+
|
8
|
+
require 'apt_control/exec'
|
9
|
+
require 'apt_control/notify'
|
10
|
+
require 'apt_control/control_file'
|
11
|
+
require 'apt_control/apt_site'
|
12
|
+
require 'apt_control/build_archive'
|
13
|
+
|
14
|
+
class Version
|
15
|
+
include Comparable
|
16
|
+
|
17
|
+
attr_reader :major, :minor, :bugfix, :debian
|
18
|
+
|
19
|
+
def self.parse(string)
|
20
|
+
match = /([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?(?:-(.+))?/.match(string)
|
21
|
+
match && new(*(1..4).map { |i| match[i] }) or raise "could not parse #{string}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(major, minor, bugfix, debian)
|
25
|
+
@major = major && major.to_i
|
26
|
+
@minor = minor && minor.to_i
|
27
|
+
@bugfix = bugfix && bugfix.to_i
|
28
|
+
@debian = debian
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
[@major, @minor, @bugfix, @debian]
|
33
|
+
end
|
34
|
+
|
35
|
+
def <=>(rhs)
|
36
|
+
self.to_a.compact <=> rhs.to_a.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(rhs)
|
40
|
+
self.to_a == rhs.to_a
|
41
|
+
end
|
42
|
+
|
43
|
+
def =~(rhs)
|
44
|
+
self.to_a[0...3] == rhs.to_a[0...3]
|
45
|
+
end
|
46
|
+
|
47
|
+
# = operator
|
48
|
+
# returns true if this version satisfies the given rule and version spec,
|
49
|
+
# where all parts of the version given match our parts. Not commutative,
|
50
|
+
# as 1.3.1.4 satisfies 1.3, but 1.3 does not satisfy 1.3.1.4
|
51
|
+
def satisfies_exactly(rhs)
|
52
|
+
rhs.to_a.compact.zip(self.to_a).each do |rhs_part, lhs_part|
|
53
|
+
return false unless rhs_part == lhs_part
|
54
|
+
end
|
55
|
+
return true
|
56
|
+
end
|
57
|
+
|
58
|
+
# >= operator
|
59
|
+
# returns true if this version is greater than or equal to the given version
|
60
|
+
def satisfies_loosely(rhs)
|
61
|
+
return true if satisfies_exactly(rhs)
|
62
|
+
return true if (self.to_a.compact <=> rhs.to_a.compact) >= 0
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
|
66
|
+
# ~> operator
|
67
|
+
def satisfies_pessimisticly(rhs)
|
68
|
+
|
69
|
+
return false unless self.to_a[0...2] == rhs.to_a[0...2]
|
70
|
+
|
71
|
+
lhs_half = self.to_a[2..-1]
|
72
|
+
rhs_half = rhs.to_a[2..-1]
|
73
|
+
|
74
|
+
(lhs_half.compact <=> rhs_half.compact) >= 0
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
[
|
79
|
+
"#{major}.#{minor}",
|
80
|
+
bugfix && ".#{bugfix}",
|
81
|
+
debian && "-#{debian}"
|
82
|
+
].compact.join
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module AptControl
|
2
|
+
# represents the reprepro apt site that we query and include packages in to
|
3
|
+
class AptSite
|
4
|
+
include Exec::Helpers
|
5
|
+
|
6
|
+
def initialize(apt_site_dir, logger)
|
7
|
+
@apt_site_dir = apt_site_dir
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def reprepro_cmd
|
12
|
+
"reprepro -b #{@apt_site_dir}"
|
13
|
+
end
|
14
|
+
|
15
|
+
# query the apt site for which version of a package is installed for a
|
16
|
+
# particular distribution
|
17
|
+
def included_version(distribution_name, package_name)
|
18
|
+
command = "#{reprepro_cmd} -Tdsc list #{distribution_name} #{package_name}"
|
19
|
+
output = exec(command, :name => 'reprepro')
|
20
|
+
version_string = output.split(' ').last
|
21
|
+
version_string && Version.parse(version_string)
|
22
|
+
end
|
23
|
+
|
24
|
+
# include a particular version in to a distribution. Will likely fail for a
|
25
|
+
# myriad number of reasons, so spits out error messages to sdterr
|
26
|
+
def include!(distribution_name, changes_fname)
|
27
|
+
command = "#{reprepro_cmd} --ignore=wrongdistribution include #{distribution_name} #{changes_fname}"
|
28
|
+
exec(command, :name => 'reprepro')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module AptControl
|
2
|
+
|
3
|
+
# represents a directory containing output from lots of dpkg builds
|
4
|
+
class BuildArchive
|
5
|
+
|
6
|
+
attr_reader :packages
|
7
|
+
attr_reader :dir
|
8
|
+
|
9
|
+
def initialize(dir, logger)
|
10
|
+
@dir = File.expand_path(dir)
|
11
|
+
@logger = logger
|
12
|
+
parse!
|
13
|
+
end
|
14
|
+
|
15
|
+
# get a list of all versions for a particular package
|
16
|
+
def [](name)
|
17
|
+
package = packages.find {|p| p.name == name }
|
18
|
+
package && package.versions
|
19
|
+
end
|
20
|
+
|
21
|
+
# return absolute path to a changes file for a particular package and version
|
22
|
+
def changes_fname(package_name, version)
|
23
|
+
fname = Dir.chdir(@dir) do
|
24
|
+
parsed_changes = Dir["#{package_name}_#{version}_*.changes"].find { |fname|
|
25
|
+
parse_changes_fname(fname)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
fname && File.expand_path(File.join(@dir, fname))
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse!
|
33
|
+
Dir.chdir(@dir) do
|
34
|
+
parsed_changes = Dir['*.changes'].map { |fname|
|
35
|
+
begin
|
36
|
+
parse_changes_fname(fname)
|
37
|
+
rescue => e
|
38
|
+
@logger.error("Unable to parse changes filename: #{fname}")
|
39
|
+
@logger.error(e)
|
40
|
+
end
|
41
|
+
}.compact
|
42
|
+
|
43
|
+
package_names = parsed_changes.map(&:first).sort.uniq
|
44
|
+
@packages = package_names.map do |name|
|
45
|
+
versions = parsed_changes.select {|n, v | name == n }.
|
46
|
+
map(&:last).
|
47
|
+
map {|s|
|
48
|
+
begin
|
49
|
+
Version.parse(s)
|
50
|
+
rescue => e
|
51
|
+
@logger.error("Couldn't parse version string: #{s}")
|
52
|
+
@logger.error(e)
|
53
|
+
end
|
54
|
+
}.compact
|
55
|
+
Package.new(name, versions)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_changes_fname(fname)
|
61
|
+
name, version = fname.split('_')[0...2]
|
62
|
+
raise "bad changes filename #{fname}" unless name and version
|
63
|
+
[name, version]
|
64
|
+
end
|
65
|
+
|
66
|
+
# watch the build directory, adding new packages and versions to the
|
67
|
+
# in-memory list as it sees them. Yields to the given block with the
|
68
|
+
# package and the new version
|
69
|
+
def watch(&block)
|
70
|
+
@logger.info("Watching for new changes files in #{@dir}")
|
71
|
+
Listen.to(@dir, :filter => /\.changes$/) do |modified, added, removed|
|
72
|
+
added.each do |fname|
|
73
|
+
begin
|
74
|
+
fname = File.basename(fname)
|
75
|
+
name, version_string = parse_changes_fname(fname)
|
76
|
+
version = Version.parse(version_string)
|
77
|
+
|
78
|
+
package = @packages.find {|p| p.name == name }
|
79
|
+
if package.nil?
|
80
|
+
@packages << package = Package.new(name, [version])
|
81
|
+
else
|
82
|
+
package.add_version(version)
|
83
|
+
end
|
84
|
+
|
85
|
+
yield(package, version)
|
86
|
+
rescue => e
|
87
|
+
@logger.error("Could not parse changes filename #{fname}: #{e}")
|
88
|
+
@logger.error(e)
|
89
|
+
next
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Package
|
96
|
+
|
97
|
+
attr_reader :name
|
98
|
+
|
99
|
+
def initialize(name, versions)
|
100
|
+
@name = name
|
101
|
+
@versions = versions
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_version(version)
|
105
|
+
@versions << version
|
106
|
+
end
|
107
|
+
|
108
|
+
def versions
|
109
|
+
@versions.sort
|
110
|
+
end
|
111
|
+
|
112
|
+
def changes_fname(version) ; end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'climate'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module AptControl
|
5
|
+
|
6
|
+
# Some class methods for defining config keys
|
7
|
+
module ConfigDSL
|
8
|
+
|
9
|
+
def config(key, description, options={})
|
10
|
+
options = {:required => true}.merge(options)
|
11
|
+
configs << [key, description, options]
|
12
|
+
end
|
13
|
+
|
14
|
+
def configs
|
15
|
+
@configs ||= []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module CLI
|
20
|
+
|
21
|
+
def self.main
|
22
|
+
init_commands
|
23
|
+
|
24
|
+
Climate.with_standard_exception_handling do
|
25
|
+
begin
|
26
|
+
Root.run(ARGV)
|
27
|
+
rescue Exec::UnexpectedExitStatus => e
|
28
|
+
$stderr.puts("Error executing: #{e.command}")
|
29
|
+
$stderr.puts(e.stderr)
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.init_commands
|
36
|
+
require 'apt_control/cli/status'
|
37
|
+
require 'apt_control/cli/watch'
|
38
|
+
require 'apt_control/cli/include'
|
39
|
+
end
|
40
|
+
|
41
|
+
module Common
|
42
|
+
def apt_site ; ancestor(Root).apt_site ; end
|
43
|
+
def control_file ; ancestor(Root).control_file ; end
|
44
|
+
def build_archive ; ancestor(Root).build_archive ; end
|
45
|
+
def notifier ; ancestor(Root).notify ; end
|
46
|
+
def notify(msg) ; ancestor(Root).notify(msg) ; end
|
47
|
+
def validate_config! ; ancestor(Root).validate_config! ; end
|
48
|
+
def logger ; ancestor(Root).logger ; end
|
49
|
+
|
50
|
+
def each_package_state(&block)
|
51
|
+
control_file.distributions.each do |dist|
|
52
|
+
dist.package_rules.each do |rule|
|
53
|
+
included = apt_site.included_version(dist.name, rule.package_name)
|
54
|
+
available = build_archive[rule.package_name]
|
55
|
+
|
56
|
+
yield(dist, rule, included, available)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Root < Climate::Command('apt_control')
|
63
|
+
|
64
|
+
class << self
|
65
|
+
include ConfigDSL
|
66
|
+
end
|
67
|
+
|
68
|
+
DEFAULT_CONFIG_FILE_LOCATION = '/etc/apt_control/config.yaml'
|
69
|
+
|
70
|
+
config :log_file, "File to send log output to, defaults to stdout", :required => false
|
71
|
+
config :apt_site_dir, "Directory containing apt files"
|
72
|
+
config :control_file, "Path to control file containing inclusion rules"
|
73
|
+
config :build_archive_dir, "Directory containing debian build files"
|
74
|
+
config :jabber_enabled, "Enable jabber integration", :required => false
|
75
|
+
config :jabber_id, "Jabber ID for notifications", :required => false
|
76
|
+
config :jabber_password, "Password for connecting to jabber server", :required => false
|
77
|
+
config :jabber_chatroom_id, "Jabber ID for chatroom to send notifications to", :required => false
|
78
|
+
|
79
|
+
description """
|
80
|
+
Move packages from an archive in to your reprepro style apt repository.
|
81
|
+
|
82
|
+
CONFIG
|
83
|
+
|
84
|
+
All configuration can be set by passing an option on the command line, but you can
|
85
|
+
avoid having to pass these each time by using a config file. By default,
|
86
|
+
apt_control looks for #{DEFAULT_CONFIG_FILE_LOCATION}, which is expected to be a
|
87
|
+
YAML file containing a single hash of key value/pairs for each option.
|
88
|
+
|
89
|
+
#{configs.map {|k, d| "#{k}: #{d}" }.join("\n\n") }
|
90
|
+
"""
|
91
|
+
|
92
|
+
opt :config_file, "Alternative location for config file",
|
93
|
+
:type => :string, :short => 'f'
|
94
|
+
|
95
|
+
opt :config_option, "Supply a config option on the command line", :multi => true,
|
96
|
+
:type => :string, :short => 'o'
|
97
|
+
|
98
|
+
def config
|
99
|
+
@config ||= build_config
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Read yaml file if one exists, then apply overrides from the command line
|
104
|
+
#
|
105
|
+
def build_config
|
106
|
+
file = [options[:config_file], DEFAULT_CONFIG_FILE_LOCATION].
|
107
|
+
compact.find {|f| File.exists?(f) }
|
108
|
+
|
109
|
+
hash =
|
110
|
+
if file
|
111
|
+
YAML.load_file(file).each do |key, value|
|
112
|
+
stderr.puts("Warn: Unknown key in config file: #{key}") unless
|
113
|
+
self.class.configs.find {|opt| opt.first.to_s == key.to_s }
|
114
|
+
end
|
115
|
+
else
|
116
|
+
{}
|
117
|
+
end
|
118
|
+
|
119
|
+
options[:config_option].map {|str| str.split('=') }.
|
120
|
+
inject(hash) {|m, (k,v)| m.merge(k.to_sym => v) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_config!
|
124
|
+
self.class.configs.each do |key, desc, options|
|
125
|
+
if options[:required]
|
126
|
+
config[key] or raise Climate::ExitException, "Error: No config supplied for #{key}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if config[:jabber_enabled]
|
131
|
+
self.class.configs.each do |key, desc, options|
|
132
|
+
next unless key.to_s['jabber_']
|
133
|
+
config[key] or raise Climate::ExitException, "Error: you must supply all jabber options if jabber is enabled"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def logger
|
139
|
+
@logger ||= Logger.new(config[:log_file] || STDOUT).tap do |logger|
|
140
|
+
logger.level = Logger::DEBUG
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def apt_site
|
145
|
+
@apt_site ||= AptSite.new(config[:apt_site_dir], logger)
|
146
|
+
end
|
147
|
+
|
148
|
+
def control_file
|
149
|
+
@control_file ||= ControlFile.new(config[:control_file], logger)
|
150
|
+
end
|
151
|
+
|
152
|
+
def build_archive
|
153
|
+
@build_archive ||= BuildArchive.new(config[:build_archive_dir], logger)
|
154
|
+
end
|
155
|
+
|
156
|
+
def notifier
|
157
|
+
@notify ||= Notify::Jabber.new(:jid => config[:jabber_id], :logger => logger,
|
158
|
+
:password => config[:jabber_password], :room_jid => config[:jabber_chatroom_id])
|
159
|
+
end
|
160
|
+
|
161
|
+
def notify(message)
|
162
|
+
logger.info("notify: #{message}")
|
163
|
+
return unless config[:jabber_enabled]
|
164
|
+
notifier.message(message)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AptControl::CLI
|
2
|
+
class Include < Climate::Command('include')
|
3
|
+
include Common
|
4
|
+
subcommand_of Root
|
5
|
+
description """Include in the apt site all packages from the build-archive
|
6
|
+
that the control file will allow"""
|
7
|
+
|
8
|
+
opt :noop, "Do a dry run, printing what you would do out to stdout", :default => false
|
9
|
+
|
10
|
+
def run
|
11
|
+
validate_config!
|
12
|
+
|
13
|
+
control_file.distributions.each do |dist|
|
14
|
+
dist.package_rules.each do |rule|
|
15
|
+
included = apt_site.included_version(dist.name, rule.package_name)
|
16
|
+
available = build_archive[rule.package_name]
|
17
|
+
|
18
|
+
next unless available
|
19
|
+
|
20
|
+
if rule.upgradeable?(included, available)
|
21
|
+
version = rule.upgradeable_to(available).max
|
22
|
+
if options[:noop]
|
23
|
+
puts "I want to upgrade from #{included} to version #{version} of #{rule.package_name}"
|
24
|
+
else
|
25
|
+
apt_site.include!(dist.name, build_archive.changes_fname(rule.package_name, version))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module AptControl::CLI
|
2
|
+
class Status < Climate::Command('status')
|
3
|
+
include Common
|
4
|
+
subcommand_of Root
|
5
|
+
description "Dump current state of apt site and build archive"
|
6
|
+
|
7
|
+
opt :machine_readable, "If true, output in a unix-friendly format", :default => false
|
8
|
+
|
9
|
+
def run
|
10
|
+
validate_config!
|
11
|
+
|
12
|
+
control_file.distributions.each do |dist|
|
13
|
+
puts dist.name unless options[:machine_readable]
|
14
|
+
dist.package_rules.each do |rule|
|
15
|
+
included = apt_site.included_version(dist.name, rule.package_name)
|
16
|
+
available = build_archive[rule.package_name]
|
17
|
+
|
18
|
+
satisfied = included && rule.satisfied_by?(included)
|
19
|
+
upgradeable = available && rule.upgradeable?(included, available)
|
20
|
+
|
21
|
+
if options[:machine_readable]
|
22
|
+
fields = [
|
23
|
+
dist.name,
|
24
|
+
rule.package_name,
|
25
|
+
"(#{rule.restriction} #{rule.version})",
|
26
|
+
"#{upgradeable ? 'U' : '.'}#{satisfied ? 'S' : '.'}",
|
27
|
+
"included=#{included || '<none>'}",
|
28
|
+
"available=#{available && available.join(', ') || '<none>'} "
|
29
|
+
]
|
30
|
+
puts fields.join(' ')
|
31
|
+
else
|
32
|
+
puts " #{rule.package_name}"
|
33
|
+
puts " rule - #{rule.restriction} #{rule.version}"
|
34
|
+
puts " included - #{included}"
|
35
|
+
puts " available - #{available && available.join(', ')}"
|
36
|
+
puts " satisfied - #{satisfied}"
|
37
|
+
puts " upgradeable - #{upgreadable}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module AptControl::CLI
|
2
|
+
class Watch < Climate::Command('watch')
|
3
|
+
include Common
|
4
|
+
subcommand_of Root
|
5
|
+
description """Watch the build archive for new files to include
|
6
|
+
|
7
|
+
DAEMON
|
8
|
+
|
9
|
+
The watch command can run as a daemon, backgrounding itself after start up and
|
10
|
+
has the usual set of options for running as an init.d style daemon.
|
11
|
+
"""
|
12
|
+
|
13
|
+
opt :noop, "Only pretend to do stuff to the apt archive"
|
14
|
+
opt :daemonize, "Run watcher in the background", :default => false
|
15
|
+
opt :pidfile, "Pidfile when daemonized", :type => :string
|
16
|
+
opt :setuid, "Once daemonized, call setuid with this user id to drop privileges", :type => :integer
|
17
|
+
|
18
|
+
def run
|
19
|
+
validate_config!
|
20
|
+
|
21
|
+
# hit these before we daemonize so we don't just background and die
|
22
|
+
apt_site
|
23
|
+
control_file
|
24
|
+
build_archive
|
25
|
+
|
26
|
+
daemonize! if options[:daemonize]
|
27
|
+
|
28
|
+
start_watching
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def daemonize!
|
33
|
+
pidfile = options[:pidfile]
|
34
|
+
|
35
|
+
if pidfile && File.exists?(pidfile)
|
36
|
+
$stderr.puts("pidfile exists, not starting")
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
if uid = options[:setuid]
|
41
|
+
logger.info("setting uid to #{uid}")
|
42
|
+
begin
|
43
|
+
Process::Sys.setuid(uid)
|
44
|
+
rescue Errno::EPERM => e
|
45
|
+
raise Climate::ExitException, "Could not setuid with #{uid}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
pid = fork
|
50
|
+
exit 0 unless pid.nil?
|
51
|
+
|
52
|
+
File.open(pidfile, 'w') {|f| f.write(Process.pid) } if pidfile
|
53
|
+
|
54
|
+
at_exit { File.delete(pidfile) if File.exists?(pidfile) } if pidfile
|
55
|
+
end
|
56
|
+
|
57
|
+
def start_watching
|
58
|
+
# update the all the rules if the control file changes
|
59
|
+
Thread.new { control_file.watch { notify "Control file reloaded" } }
|
60
|
+
|
61
|
+
notify("Watching for new packages in #{build_archive.dir}")
|
62
|
+
build_archive.watch do |package, new_version|
|
63
|
+
notify("new package: #{package.name} at #{new_version}")
|
64
|
+
|
65
|
+
updated = control_file.distributions.map do |dist|
|
66
|
+
rule = dist[package.name] or next
|
67
|
+
included = apt_site.included_version(dist.name, package.name)
|
68
|
+
|
69
|
+
if rule.upgradeable?(included, [new_version])
|
70
|
+
if options[:noop]
|
71
|
+
notify("package #{package.name} can be upgraded to #{new_version} on #{dist.name} (noop)")
|
72
|
+
else
|
73
|
+
# FIXME error handling here, please
|
74
|
+
begin
|
75
|
+
apt_site.include!(dist.name, build_archive.changes_fname(rule.package_name, new_version))
|
76
|
+
notify("included package #{package.name}-#{new_version} in #{dist.name}")
|
77
|
+
rescue => e
|
78
|
+
notify("Failed to include package #{package.name}-#{new_version}, check log for more details")
|
79
|
+
logger.error("failed to include package #{package.name}")
|
80
|
+
logger.error(e)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
dist.name
|
84
|
+
else
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end.compact
|
88
|
+
|
89
|
+
if updated.size == 0
|
90
|
+
notify("package #{package.name} could not be updated on any distributions")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module AptControl
|
4
|
+
|
5
|
+
# Loads and models the contents of a control.ini file
|
6
|
+
# see example-control.ini in root of project for an example
|
7
|
+
class ControlFile
|
8
|
+
|
9
|
+
def initialize(path, logger)
|
10
|
+
@logger = logger
|
11
|
+
@watch_mutex = Mutex.new
|
12
|
+
@path = path
|
13
|
+
inifile = IniFile.load(@path)
|
14
|
+
@distributions = parse!(inifile)
|
15
|
+
end
|
16
|
+
|
17
|
+
def distributions
|
18
|
+
@watch_mutex.synchronize { @distributions }
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump
|
22
|
+
@watch_mutex.synchronize do
|
23
|
+
@distributions.each do |d|
|
24
|
+
puts "#{d.name}"
|
25
|
+
d.package_rules.each do |pr|
|
26
|
+
puts " #{pr.package_name} #{pr.restriction} #{pr.version}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse!(inifile)
|
33
|
+
inifile.sections.map do |section|
|
34
|
+
rules = inifile[section].map do |key, value|
|
35
|
+
PackageRule.new(key, value)
|
36
|
+
end
|
37
|
+
Distribution.new(section, rules)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Watch the control file for changes, rebuilding
|
42
|
+
# internal data structures when it does
|
43
|
+
def watch(&block)
|
44
|
+
path = File.expand_path(@path)
|
45
|
+
@logger.info("Watching for changes to #{path}")
|
46
|
+
dir = File.dirname(path)
|
47
|
+
fname = File.basename(path)
|
48
|
+
Listen.to(dir, :filter => /#{Regexp.quote(fname)}/) do |modified, added, removed|
|
49
|
+
begin
|
50
|
+
@logger.info("Change to control file detected...")
|
51
|
+
inifile = IniFile.load(path)
|
52
|
+
distributions = parse!(inifile)
|
53
|
+
|
54
|
+
@watch_mutex.synchronize do
|
55
|
+
@distributions = distributions
|
56
|
+
end
|
57
|
+
@logger.info("...rebuilt")
|
58
|
+
yield if block_given?
|
59
|
+
rescue => e
|
60
|
+
@logger.error("Error reloading changes: #{e}")
|
61
|
+
@logger.error(e)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class PackageRule
|
67
|
+
# name of package rule applies to
|
68
|
+
attr_reader :package_name
|
69
|
+
|
70
|
+
# version number for restriction comparison
|
71
|
+
attr_reader :version
|
72
|
+
|
73
|
+
# symbol for rule
|
74
|
+
attr_reader :restriction
|
75
|
+
|
76
|
+
def initialize(name, constraint)
|
77
|
+
@package_name = name
|
78
|
+
version = nil
|
79
|
+
constraint.split(" ").tap do |split|
|
80
|
+
@restriction, version = if split.size == 1
|
81
|
+
['=', split.first]
|
82
|
+
else
|
83
|
+
split
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
['=', '>=', '~>'].include?(@restriction) or
|
88
|
+
raise "unrecognised restriction: '#{@restriction}'"
|
89
|
+
|
90
|
+
@version = Version.parse(version)
|
91
|
+
end
|
92
|
+
|
93
|
+
# will return true if their is a version in available that is higher
|
94
|
+
# than included
|
95
|
+
def higher_available?(included, available)
|
96
|
+
available.find {|a| a > included }
|
97
|
+
end
|
98
|
+
|
99
|
+
# will return true if included satisfies this rule
|
100
|
+
def satisfied_by?(included)
|
101
|
+
case @restriction
|
102
|
+
when '='
|
103
|
+
included.satisfies_exactly(version)
|
104
|
+
when '>='
|
105
|
+
included.satisfies_loosely(version)
|
106
|
+
when '~>'
|
107
|
+
included.satisfies_pessimisticly(version)
|
108
|
+
else raise "this shouldn't have happened"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# will return true if a) there is a higher version available than is
|
113
|
+
# included b) any of the available packages satisfy this rule
|
114
|
+
def upgradeable?(included, available)
|
115
|
+
return false unless higher_available?(included, available)
|
116
|
+
higher = available.select {|a| a > included }
|
117
|
+
return true if higher.any? {|a| satisfied_by?(a) }
|
118
|
+
end
|
119
|
+
|
120
|
+
# will return the subset of versions from available that satisfy this rule
|
121
|
+
def upgradeable_to(available)
|
122
|
+
available.select {|a| satisfied_by?(a) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# represents a set of rules mapped to a particular distribution, i.e.
|
127
|
+
# squeeze is a distribution
|
128
|
+
class Distribution
|
129
|
+
def initialize(name, rules)
|
130
|
+
@name = name
|
131
|
+
@package_rules = rules
|
132
|
+
end
|
133
|
+
attr_reader :name
|
134
|
+
attr_reader :package_rules
|
135
|
+
|
136
|
+
# find a PackageRule by package name
|
137
|
+
def [](package_name)
|
138
|
+
package_rules.find {|rule| rule.package_name == package_name }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'popen4'
|
3
|
+
|
4
|
+
module AptControl
|
5
|
+
# ripped from vershunt
|
6
|
+
class Exec
|
7
|
+
|
8
|
+
class UnexpectedExitStatus < StandardError
|
9
|
+
def initialize(expected, actual, cmd, output, stdout, stderr)
|
10
|
+
@expected = expected
|
11
|
+
@actual = actual
|
12
|
+
@command = cmd
|
13
|
+
@stdout = stdout
|
14
|
+
@stderr = stderr
|
15
|
+
super("command '#{command}' exited with #{actual}, expected #{expected}")
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :expect, :actual, :command, :output, :stdout, :stderr
|
19
|
+
# this marries up with what popen et al return
|
20
|
+
alias :exitstatus :actual
|
21
|
+
end
|
22
|
+
|
23
|
+
module Helpers
|
24
|
+
def exec(command, options={})
|
25
|
+
# use the options we were constructed with if they exist
|
26
|
+
if respond_to?(:options)
|
27
|
+
options[:quiet] = !self.options.verbose? unless options.has_key? :quiet
|
28
|
+
end
|
29
|
+
|
30
|
+
if respond_to? :exec_name
|
31
|
+
options[:name] = exec_name unless options.has_key? :name
|
32
|
+
end
|
33
|
+
|
34
|
+
AptControl::Exec.exec(command, options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.exec(command, options={})
|
39
|
+
new(options).exec(command, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :name
|
43
|
+
attr_reader :quiet
|
44
|
+
alias :quiet? :quiet
|
45
|
+
|
46
|
+
def initialize(options={})
|
47
|
+
@name = options.fetch(:name, nil)
|
48
|
+
@quiet = options.fetch(:quiet, true)
|
49
|
+
@output = options.fetch(:output, $stdout)
|
50
|
+
end
|
51
|
+
|
52
|
+
def last_stdout
|
53
|
+
@last_stdout
|
54
|
+
end
|
55
|
+
|
56
|
+
def last_stderr
|
57
|
+
@last_stderr
|
58
|
+
end
|
59
|
+
|
60
|
+
def last_output
|
61
|
+
@last_output
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_exitstatus
|
65
|
+
@last_exitstatus
|
66
|
+
end
|
67
|
+
|
68
|
+
def exec(command, options={})
|
69
|
+
|
70
|
+
expected = to_expected_array(options.fetch(:status, 0))
|
71
|
+
|
72
|
+
@last_stdout = ""
|
73
|
+
@last_stderr = ""
|
74
|
+
@last_output = ""
|
75
|
+
|
76
|
+
output_semaphore = Mutex.new
|
77
|
+
|
78
|
+
start = name.nil? ? '' : "#{name}: "
|
79
|
+
|
80
|
+
status = POpen4::popen4(command) do |stdout, stderr, stdin, pid|
|
81
|
+
t1 = Thread.new do
|
82
|
+
stdout.each_line do |line|
|
83
|
+
@last_stdout += line
|
84
|
+
output_semaphore.synchronize { @last_output += line }
|
85
|
+
@output.puts("#{start}#{line}") unless quiet?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
t2 = Thread.new do
|
90
|
+
stderr.each_line do |line|
|
91
|
+
@last_stderr += line
|
92
|
+
output_semaphore.synchronize { @last_output += line }
|
93
|
+
@output.puts("#{start}#{line}") unless quiet?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
t1.join
|
98
|
+
t2.join
|
99
|
+
end
|
100
|
+
|
101
|
+
@last_exitstatus = status.exitstatus
|
102
|
+
|
103
|
+
unless expected.nil? || expected.include?(status.exitstatus)
|
104
|
+
raise UnexpectedExitStatus.new(expected, status.exitstatus, command, @last_output, @last_stdout, @last_stderr)
|
105
|
+
end
|
106
|
+
|
107
|
+
@last_stdout
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def to_expected_array(value)
|
113
|
+
case value
|
114
|
+
when :any then nil
|
115
|
+
when Array then value
|
116
|
+
when Numeric then [value]
|
117
|
+
else raise ArgumentError, "#{value} is not an acceptable exit status"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'xmpp4r/jid'
|
3
|
+
require 'xmpp4r/client'
|
4
|
+
require 'xmpp4r/muc'
|
5
|
+
require 'xmpp4r/muc/helper/simplemucclient'
|
6
|
+
|
7
|
+
module AptControl::Notify
|
8
|
+
|
9
|
+
class Jabber
|
10
|
+
include ::Jabber
|
11
|
+
|
12
|
+
def initialize(options={})
|
13
|
+
@jid = options[:jid]
|
14
|
+
@password = options[:password]
|
15
|
+
@room_jid = options[:room_jid]
|
16
|
+
@logger = options[:logger]
|
17
|
+
|
18
|
+
connect!
|
19
|
+
end
|
20
|
+
|
21
|
+
def connect!
|
22
|
+
# ::Jabber::debug = true
|
23
|
+
@logger.info("Jabber connecting with jid #{@jid}")
|
24
|
+
@client = Client.new(JID.new(@jid))
|
25
|
+
@client.connect
|
26
|
+
@client.auth(@password)
|
27
|
+
|
28
|
+
@muc = Jabber::MUC::SimpleMUCClient.new(@client)
|
29
|
+
@muc.join(JID.new(@room_jid))
|
30
|
+
@logger.info("joined room #{@room_jid}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def message(msg)
|
34
|
+
@muc.send(Message.new(nil, msg))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: apt_control
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Nick Griffiths
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-04-28 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: popen4
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: climate
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: inifile
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: xmpp4r
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
type: :runtime
|
75
|
+
version_requirements: *id004
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: listen
|
78
|
+
prerelease: false
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
hash: 3
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
type: :runtime
|
89
|
+
version_requirements: *id005
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: rspec
|
92
|
+
prerelease: false
|
93
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
type: :development
|
103
|
+
version_requirements: *id006
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: minitest
|
106
|
+
prerelease: false
|
107
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
hash: 3
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
type: :development
|
117
|
+
version_requirements: *id007
|
118
|
+
description:
|
119
|
+
email:
|
120
|
+
- nicobrevin@gmail.com
|
121
|
+
executables:
|
122
|
+
- apt_control
|
123
|
+
extensions: []
|
124
|
+
|
125
|
+
extra_rdoc_files: []
|
126
|
+
|
127
|
+
files:
|
128
|
+
- bin/apt_control
|
129
|
+
- lib/apt_control.rb
|
130
|
+
- lib/apt_control/cli.rb
|
131
|
+
- lib/apt_control/notify.rb
|
132
|
+
- lib/apt_control/version.rb
|
133
|
+
- lib/apt_control/exec.rb
|
134
|
+
- lib/apt_control/cli/watch.rb
|
135
|
+
- lib/apt_control/cli/include.rb
|
136
|
+
- lib/apt_control/cli/status.rb
|
137
|
+
- lib/apt_control/build_archive.rb
|
138
|
+
- lib/apt_control/apt_site.rb
|
139
|
+
- lib/apt_control/control_file.rb
|
140
|
+
homepage: http://github.com/playlouder/apt_control
|
141
|
+
licenses: []
|
142
|
+
|
143
|
+
post_install_message:
|
144
|
+
rdoc_options: []
|
145
|
+
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
hash: 3
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
version: "0"
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
hash: 23
|
163
|
+
segments:
|
164
|
+
- 1
|
165
|
+
- 3
|
166
|
+
- 6
|
167
|
+
version: 1.3.6
|
168
|
+
requirements: []
|
169
|
+
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 1.8.24
|
172
|
+
signing_key:
|
173
|
+
specification_version: 3
|
174
|
+
summary: Automatically manage an apt repository that changes a lot
|
175
|
+
test_files: []
|
176
|
+
|