apt_control 0.3.0

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