apt_control 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|