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.
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'apt_control'
4
+ require 'apt_control/cli'
5
+ AptControl::CLI.main
@@ -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
@@ -0,0 +1,3 @@
1
+ module AptControl
2
+ VERSION = '0.3.0'
3
+ 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
+