apt_control 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/apt_control.rb +8 -2
- data/lib/apt_control/actors.rb +41 -0
- data/lib/apt_control/apt_site.rb +3 -0
- data/lib/apt_control/bot.rb +121 -0
- data/lib/apt_control/build_archive.rb +2 -0
- data/lib/apt_control/cli.rb +26 -11
- data/lib/apt_control/cli/include.rb +2 -1
- data/lib/apt_control/cli/promote.rb +29 -0
- data/lib/apt_control/cli/set.rb +22 -0
- data/lib/apt_control/cli/status.rb +1 -9
- data/lib/apt_control/cli/watch.rb +30 -4
- data/lib/apt_control/commands.rb +6 -0
- data/lib/apt_control/{includer.rb → commands/include.rb} +8 -10
- data/lib/apt_control/commands/promote.rb +28 -0
- data/lib/apt_control/commands/set.rb +24 -0
- data/lib/apt_control/control_file.rb +73 -9
- data/lib/apt_control/jabber.rb +47 -1
- data/lib/apt_control/package_states.rb +18 -0
- data/lib/apt_control/version.rb +1 -1
- metadata +28 -5
data/lib/apt_control.rb
CHANGED
@@ -2,16 +2,22 @@
|
|
2
2
|
require 'inifile'
|
3
3
|
require 'listen'
|
4
4
|
require 'logger'
|
5
|
+
require 'celluloid'
|
6
|
+
|
7
|
+
# silence celluloid until we have our own logger
|
8
|
+
Celluloid.logger = nil
|
5
9
|
|
6
10
|
module AptControl
|
7
11
|
|
8
12
|
require 'apt_control/exec'
|
13
|
+
require 'apt_control/actors'
|
9
14
|
require 'apt_control/jabber'
|
15
|
+
require 'apt_control/bot'
|
10
16
|
require 'apt_control/control_file'
|
11
17
|
require 'apt_control/apt_site'
|
12
18
|
require 'apt_control/build_archive'
|
13
19
|
require 'apt_control/package_states'
|
14
|
-
require 'apt_control/
|
20
|
+
require 'apt_control/commands'
|
15
21
|
|
16
22
|
class Version
|
17
23
|
include Comparable
|
@@ -19,7 +25,7 @@ module AptControl
|
|
19
25
|
attr_reader :major, :minor, :bugfix, :debian
|
20
26
|
|
21
27
|
def self.parse(string)
|
22
|
-
match =
|
28
|
+
match = /^([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?(?:-(.+))?$/.match(string)
|
23
29
|
match && new(*(1..4).map { |i| match[i] }) or raise "could not parse #{string}"
|
24
30
|
end
|
25
31
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
# Explicitly proxy some objects to have actor proxies - perhaps celluloid already
|
3
|
+
# has this facility?
|
4
|
+
#
|
5
|
+
# The rationale is that I want to leave the underlying objects 'clean' so I
|
6
|
+
# can test them individually without having to worry about celluloid magic
|
7
|
+
#
|
8
|
+
module AptControl::Actors
|
9
|
+
|
10
|
+
module ProxiedClassMethods
|
11
|
+
def proxy(method)
|
12
|
+
proxy_class.class_eval """\
|
13
|
+
def #{method}(*args, &block)
|
14
|
+
@proxied_object.#{method}(*args, &block)
|
15
|
+
end
|
16
|
+
"""
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :proxy_class
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class ActorProxy
|
24
|
+
include Celluloid
|
25
|
+
def initialize(proxied_object)
|
26
|
+
@proxied_object = proxied_object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Proxied
|
31
|
+
def self.included(other_class)
|
32
|
+
@proxy_class = Class.new(ActorProxy)
|
33
|
+
other_class.instance_variable_set('@proxy_class', @proxy_class)
|
34
|
+
other_class.extend(ProxiedClassMethods)
|
35
|
+
end
|
36
|
+
|
37
|
+
def actor
|
38
|
+
@actor_proxy ||= self.class.proxy_class.new(self)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/apt_control/apt_site.rb
CHANGED
@@ -2,6 +2,9 @@ module AptControl
|
|
2
2
|
# represents the reprepro apt site that we query and include packages in to
|
3
3
|
class AptSite
|
4
4
|
include Exec::Helpers
|
5
|
+
include Actors::Proxied
|
6
|
+
proxy :include!
|
7
|
+
proxy :included_version
|
5
8
|
|
6
9
|
def initialize(apt_site_dir, logger)
|
7
10
|
@apt_site_dir = apt_site_dir
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module AptControl
|
2
|
+
class Bot
|
3
|
+
|
4
|
+
# as a module for testing
|
5
|
+
module ArgHelpers
|
6
|
+
def split_args(args_string)
|
7
|
+
# there is probably some code out there that can do this better, maybe
|
8
|
+
# go out and find it if it proves troublsome :)
|
9
|
+
args_string.scan(/[^ '"]+|'[^']+'|"[^"]+"/).map do |str|
|
10
|
+
str.gsub(/'|"/, '')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
include ArgHelpers
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
def method_added(meth)
|
19
|
+
super
|
20
|
+
if match = /^handle_(.+)$/.match(meth.to_s)
|
21
|
+
handlers << match[1]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def handlers
|
26
|
+
@handlers ||= []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
self.extend(ClassMethods)
|
31
|
+
include Actors::Proxied
|
32
|
+
proxy :on_message
|
33
|
+
|
34
|
+
def initialize(dependencies)
|
35
|
+
@jabber = dependencies.fetch(:jabber)
|
36
|
+
@package_states = dependencies.fetch(:package_states)
|
37
|
+
@logger = dependencies.fetch(:logger)
|
38
|
+
@command_start = dependencies.fetch(:command_start)
|
39
|
+
@include_cmd = dependencies.fetch(:include_cmd)
|
40
|
+
@control_file = dependencies.fetch(:control_file)
|
41
|
+
|
42
|
+
@bot_pattern = /#{Regexp.escape(@command_start)}\: ([^ ]+)(?: (.+))?/
|
43
|
+
@logger.info("looking for messages starting with #{@command_start}")
|
44
|
+
@logger.debug(" match pattern: #{@bot_pattern}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_message(text)
|
48
|
+
return unless match = @bot_pattern.match(text)
|
49
|
+
|
50
|
+
command, args = [match[1], match[2]]
|
51
|
+
|
52
|
+
handler = self.class.handlers.include?(command) or
|
53
|
+
return print_help("unknown command '#{command}'")
|
54
|
+
|
55
|
+
args = split_args(args || '')
|
56
|
+
begin
|
57
|
+
self.send("handle_#{command}", args)
|
58
|
+
rescue => e
|
59
|
+
begin ; send_message("error: #{e}") ; rescue => e ; end
|
60
|
+
@logger.error("error handling #{command}")
|
61
|
+
@logger.error(e)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def send_message(msg)
|
66
|
+
@jabber.async.send_message(msg)
|
67
|
+
end
|
68
|
+
|
69
|
+
def print_help(message)
|
70
|
+
send_message(message)
|
71
|
+
send_message("Send commands with '#{@command_start} COMMAND [ARGS...]'")
|
72
|
+
send_message("Available commands: #{self.class.handlers.join(' ')}")
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_status(args)
|
76
|
+
dist = args[0]
|
77
|
+
package_name = args[1]
|
78
|
+
|
79
|
+
found = @package_states.map do |package_state|
|
80
|
+
next if dist && dist != package_state.dist.name
|
81
|
+
next if package_name && package_name != package_state.package_name
|
82
|
+
|
83
|
+
send_message(package_state.status_line)
|
84
|
+
true
|
85
|
+
end.compact
|
86
|
+
|
87
|
+
send_message("no packages found: distribution => #{dist.inspect}, package_name => #{package_name.inspect} ") if found.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_include(args)
|
91
|
+
performed = @include_cmd.run(@package_states) do |state, version|
|
92
|
+
send_message("#{state.dist.name} #{state.package_name} #{state.included} => #{version}")
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
send_message("no packages were included") if performed.empty?
|
97
|
+
end
|
98
|
+
|
99
|
+
def handle_reload(args)
|
100
|
+
@control_file.reload!
|
101
|
+
send_message("control file reloaded")
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_set(args)
|
105
|
+
set_command.run(*args)
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_promote(args)
|
109
|
+
promote_command.run(*args)
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_command
|
113
|
+
AptControl::Commands::Set.new(control_file: @control_file)
|
114
|
+
end
|
115
|
+
|
116
|
+
def promote_command
|
117
|
+
AptControl::Commands::Promote.new(control_file: @control_file,
|
118
|
+
package_states: @package_states)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -50,6 +50,7 @@ module AptControl
|
|
50
50
|
rescue => e
|
51
51
|
@logger.error("Couldn't parse version string: #{s}")
|
52
52
|
@logger.error(e)
|
53
|
+
nil
|
53
54
|
end
|
54
55
|
}.compact
|
55
56
|
Package.new(name, versions)
|
@@ -102,6 +103,7 @@ module AptControl
|
|
102
103
|
end
|
103
104
|
|
104
105
|
def add_version(version)
|
106
|
+
raise unless version.is_a?(AptControl::Version)
|
105
107
|
@versions << version
|
106
108
|
end
|
107
109
|
|
data/lib/apt_control/cli.rb
CHANGED
@@ -36,16 +36,19 @@ module AptControl
|
|
36
36
|
require 'apt_control/cli/status'
|
37
37
|
require 'apt_control/cli/watch'
|
38
38
|
require 'apt_control/cli/include'
|
39
|
+
require 'apt_control/cli/set'
|
40
|
+
require 'apt_control/cli/promote'
|
39
41
|
end
|
40
42
|
|
41
43
|
module Common
|
42
44
|
# FIXME tidy up with some meta magic
|
43
45
|
def package_states ; ancestor(Root).package_states ; end
|
44
|
-
def
|
46
|
+
def new_include_cmd(options={}) ; ancestor(Root).new_include_cmd(options) ; end
|
45
47
|
def apt_site ; ancestor(Root).apt_site ; end
|
46
48
|
def control_file ; ancestor(Root).control_file ; end
|
47
49
|
def build_archive ; ancestor(Root).build_archive ; end
|
48
|
-
def
|
50
|
+
def jabber ; ancestor(Root).jabber ; end
|
51
|
+
def jabber_enabled? ; ancestor(Root).jabber_enabled? ; end
|
49
52
|
def notify(msg) ; ancestor(Root).notify(msg) ; end
|
50
53
|
def validate_config! ; ancestor(Root).validate_config! ; end
|
51
54
|
def logger ; ancestor(Root).logger ; end
|
@@ -71,7 +74,7 @@ module AptControl
|
|
71
74
|
|
72
75
|
DEFAULT_CONFIG_FILE_LOCATION = '/etc/apt_control/config.yaml'
|
73
76
|
|
74
|
-
config :log_file, "File to send log output to, defaults to
|
77
|
+
config :log_file, "File to send log output to, defaults to /dev/null", :required => false
|
75
78
|
config :apt_site_dir, "Directory containing apt files"
|
76
79
|
config :control_file, "Path to control file containing inclusion rules"
|
77
80
|
config :build_archive_dir, "Directory containing debian build files"
|
@@ -138,10 +141,13 @@ YAML file containing a single hash of key value/pairs for each option.
|
|
138
141
|
config[key] or raise Climate::ExitException, "Error: you must supply all jabber options if jabber is enabled"
|
139
142
|
end
|
140
143
|
end
|
144
|
+
|
145
|
+
Celluloid.logger = logger
|
141
146
|
end
|
142
147
|
|
143
148
|
def logger
|
144
|
-
|
149
|
+
log_file = config[:log_file] || '/dev/null'
|
150
|
+
@logger ||= Logger.new(log_file == 'STDOUT' ? STDOUT : log_file).tap do |logger|
|
145
151
|
logger.level = Logger::DEBUG
|
146
152
|
end
|
147
153
|
end
|
@@ -164,13 +170,21 @@ YAML file containing a single hash of key value/pairs for each option.
|
|
164
170
|
@build_archive ||= BuildArchive.new(config[:build_archive_dir], logger)
|
165
171
|
end
|
166
172
|
|
167
|
-
def
|
168
|
-
@
|
169
|
-
:password => config[:jabber_password], :room_jid => config[:jabber_chatroom_id]
|
173
|
+
def jabber
|
174
|
+
@jabber ||= Jabber.new(:jid => config[:jabber_id], :logger => logger,
|
175
|
+
:password => config[:jabber_password], :room_jid => config[:jabber_chatroom_id],
|
176
|
+
:enabled => jabber_enabled?)
|
177
|
+
end
|
178
|
+
|
179
|
+
def jabber_enabled?
|
180
|
+
config[:jabber_enabled].to_s == 'true'
|
170
181
|
end
|
171
182
|
|
172
|
-
def
|
173
|
-
|
183
|
+
def new_include_cmd(options={})
|
184
|
+
defaults = {apt_site: apt_site, build_archive: build_archive}
|
185
|
+
options = options.merge(defaults)
|
186
|
+
|
187
|
+
AptControl::Commands::Include.new(options)
|
174
188
|
end
|
175
189
|
|
176
190
|
class FSListenerFactory
|
@@ -186,6 +200,8 @@ YAML file containing a single hash of key value/pairs for each option.
|
|
186
200
|
if disable_inotify
|
187
201
|
listener.force_polling(true)
|
188
202
|
listener.polling_fallback_message(false)
|
203
|
+
else
|
204
|
+
listener.force_adapter(Listen::Adapters::Linux)
|
189
205
|
end
|
190
206
|
|
191
207
|
listener.change(&on_change)
|
@@ -200,9 +216,8 @@ YAML file containing a single hash of key value/pairs for each option.
|
|
200
216
|
|
201
217
|
def notify(message)
|
202
218
|
logger.info("notify: #{message}")
|
203
|
-
return unless config[:jabber_enabled].to_s == 'true'
|
204
219
|
begin
|
205
|
-
|
220
|
+
jabber.actor.async.send_message(message)
|
206
221
|
rescue => e
|
207
222
|
logger.error("Unable to send notification to jabber: #{e}")
|
208
223
|
logger.error(e)
|
@@ -10,7 +10,8 @@ that the control file will allow"""
|
|
10
10
|
def run
|
11
11
|
validate_config!
|
12
12
|
|
13
|
-
|
13
|
+
|
14
|
+
new_include_cmd.run(package_states) do |state, version|
|
14
15
|
if options[:noop]
|
15
16
|
puts "#{state.dist.name} #{state.package_name} #{state.included} => #{version}"
|
16
17
|
false
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module AptControl::CLI
|
2
|
+
class Promote < Climate::Command('promote')
|
3
|
+
include Common
|
4
|
+
subcommand_of Root
|
5
|
+
description "Promote a currently included package from one distribution to another"
|
6
|
+
|
7
|
+
arg :src_distribution, "Name of distribution to find currently included package version"
|
8
|
+
arg :dest_distribution, "Name of distribution to update"
|
9
|
+
arg :package, "Name of package to promote"
|
10
|
+
|
11
|
+
def run
|
12
|
+
validate_config!
|
13
|
+
|
14
|
+
begin
|
15
|
+
promote_cmd = AptControl::Commands::Promote.new(
|
16
|
+
control_file: control_file,
|
17
|
+
package_states: package_states)
|
18
|
+
|
19
|
+
begin
|
20
|
+
promote_cmd.run(arguments[:src_distribution],
|
21
|
+
arguments[:dest_distribution],
|
22
|
+
arguments[:package])
|
23
|
+
rescue ArgumentError => e
|
24
|
+
raise Climate::ExitException, e.message
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AptControl::CLI
|
2
|
+
class Set < Climate::Command('set')
|
3
|
+
include Common
|
4
|
+
subcommand_of Root
|
5
|
+
description "Set a version restriction for a package and distribution"
|
6
|
+
|
7
|
+
arg :distribution, "Name of distribution"
|
8
|
+
arg :package, "Name of package"
|
9
|
+
arg :constraint, "Version constraint, i.e. '>= 1.5'"
|
10
|
+
|
11
|
+
def run
|
12
|
+
validate_config!
|
13
|
+
|
14
|
+
begin
|
15
|
+
AptControl::Commands::Set.new(control_file: control_file).
|
16
|
+
run(arguments[:distribution], arguments[:package], arguments[:constraint])
|
17
|
+
rescue ArgumentError => e
|
18
|
+
raise Climate::ExitException, e.message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -11,15 +11,7 @@ module AptControl::CLI
|
|
11
11
|
|
12
12
|
if options[:machine_readable]
|
13
13
|
package_states.each do |state|
|
14
|
-
|
15
|
-
state.dist.name,
|
16
|
-
state.package_name,
|
17
|
-
"(#{state.rule.restriction} #{state.rule.version})",
|
18
|
-
"#{state.includeable? ? 'I' : '.'}#{state.satisfied? ? 'S' : '.'}",
|
19
|
-
"included=#{state.included || '<none>'}",
|
20
|
-
"available=#{state.available? ? state.available.join(', ') : '<none>'} "
|
21
|
-
]
|
22
|
-
puts fields.join(' ')
|
14
|
+
puts state.status_line
|
23
15
|
end
|
24
16
|
else
|
25
17
|
last_dist = nil
|
@@ -54,11 +54,18 @@ has the usual set of options for running as an init.d style daemon.
|
|
54
54
|
at_exit { File.delete(pidfile) if File.exists?(pidfile) } if pidfile
|
55
55
|
end
|
56
56
|
|
57
|
+
# for the watch command, we use the actor version of the apt_site so that
|
58
|
+
# reprepro operations are sequential
|
59
|
+
def new_include_cmd
|
60
|
+
super(apt_site: apt_site.actor)
|
61
|
+
end
|
62
|
+
|
57
63
|
def start_watching
|
58
64
|
threads = [
|
59
65
|
watch_control_in_new_thread,
|
60
|
-
watch_build_archive_in_new_thread
|
61
|
-
|
66
|
+
watch_build_archive_in_new_thread,
|
67
|
+
jabber_enabled? && start_aptbot_in_new_thread
|
68
|
+
].compact
|
62
69
|
|
63
70
|
notify("apt_control watcher is up, waiting for changes to control file and new packages...")
|
64
71
|
|
@@ -66,6 +73,25 @@ has the usual set of options for running as an init.d style daemon.
|
|
66
73
|
threads.each(&:join)
|
67
74
|
end
|
68
75
|
|
76
|
+
def start_aptbot_in_new_thread
|
77
|
+
Thread.new do
|
78
|
+
begin
|
79
|
+
bot = AptControl::Bot.new(
|
80
|
+
jabber: jabber.actor,
|
81
|
+
command_start: jabber.room_nick,
|
82
|
+
package_states: package_states,
|
83
|
+
include_cmd: new_include_cmd,
|
84
|
+
control_file: control_file,
|
85
|
+
logger: logger)
|
86
|
+
|
87
|
+
jabber.add_room_listener(bot.actor)
|
88
|
+
rescue => e
|
89
|
+
puts "got an error #{e}"
|
90
|
+
puts e.backtrace
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
69
95
|
def watch_control_in_new_thread
|
70
96
|
# update the all the rules if the control file changes
|
71
97
|
Thread.new do
|
@@ -74,7 +100,7 @@ has the usual set of options for running as an init.d style daemon.
|
|
74
100
|
notify "Control file reloaded"
|
75
101
|
# FIXME need to do some kind of locking or actor style dev for this
|
76
102
|
# as it looks like there could be some concurrency bugs lurking
|
77
|
-
|
103
|
+
new_include_cmd.run(package_states) do |package_state, new_version|
|
78
104
|
notify("included package #{package_state.package_name}-#{new_version} in #{package_state.dist.name}")
|
79
105
|
true
|
80
106
|
end
|
@@ -105,7 +131,7 @@ has the usual set of options for running as an init.d style daemon.
|
|
105
131
|
updated = matched_states.map do |state|
|
106
132
|
if state.includeable_to.max == new_version
|
107
133
|
begin
|
108
|
-
|
134
|
+
new_include_cmd.perform_for(state, new_version, options[:noop])
|
109
135
|
notify("included package #{package.name}-#{new_version} in #{state.dist.name}")
|
110
136
|
state.dist.name
|
111
137
|
rescue => e
|
@@ -1,22 +1,20 @@
|
|
1
1
|
module AptControl
|
2
|
+
class Commands::Include
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
def initialize(apt_site, build_archive)
|
7
|
-
@apt_site = apt_site
|
8
|
-
@build_archive = build_archive
|
4
|
+
def initialize(dependencies)
|
5
|
+
@apt_site = dependencies.fetch(:apt_site)
|
6
|
+
@build_archive = dependencies.fetch(:build_archive)
|
9
7
|
end
|
10
8
|
|
11
|
-
def
|
12
|
-
package_states.
|
9
|
+
def run(package_states, &visitor)
|
10
|
+
package_states.map do |state|
|
13
11
|
next unless state.includeable?
|
14
12
|
|
15
13
|
version = state.includeable_to.max
|
16
14
|
perform = (block_given? && yield(state, version)) || true
|
17
15
|
|
18
|
-
perform_for(state, version) if perform
|
19
|
-
end
|
16
|
+
perform_for(state, version) && [state, version] if perform
|
17
|
+
end.compact
|
20
18
|
end
|
21
19
|
|
22
20
|
def perform_for(state, version, noop=false)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module AptControl
|
2
|
+
class Commands::Promote
|
3
|
+
|
4
|
+
def initialize(dependencies)
|
5
|
+
@control_file = dependencies.fetch(:control_file)
|
6
|
+
@package_states = dependencies.fetch(:package_states)
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(src_name, dest_name, pkg_name)
|
10
|
+
source_dist = @control_file[src_name] or
|
11
|
+
raise ArgumentError, "source distribution '#{src_name}' does not exist"
|
12
|
+
|
13
|
+
dest_dist = @control_file[dest_name] or
|
14
|
+
raise ArgumentError, "destination distribution '#{dest_name}' does not exist"
|
15
|
+
|
16
|
+
src_state = @package_states.find_state(src_name, pkg_name) or
|
17
|
+
raise ArgumentError, "package '#{pkg_name}' does not exist in distribution '#{src_name}'"
|
18
|
+
|
19
|
+
if not src_state.included?
|
20
|
+
raise ArgumentError, "no '#{pkg_name}' package included in '#{src_name}' to promote"
|
21
|
+
end
|
22
|
+
new_constraint = "= #{src_state.included.to_s}"
|
23
|
+
dest_dist[pkg_name] = new_constraint
|
24
|
+
|
25
|
+
@control_file.write
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AptControl
|
2
|
+
class Commands::Set
|
3
|
+
|
4
|
+
def initialize(dependencies)
|
5
|
+
@control_file = dependencies.fetch(:control_file)
|
6
|
+
end
|
7
|
+
|
8
|
+
def run(distribution_name, package_name, constraint)
|
9
|
+
distribution = @control_file[distribution_name] or
|
10
|
+
raise ArgumentError, "no such distribution: #{distribution_name}"
|
11
|
+
|
12
|
+
package_rule = distribution[package_name] or
|
13
|
+
raise ArgumentError, "no such package: #{package_name}"
|
14
|
+
|
15
|
+
begin
|
16
|
+
package_rule.constraint = constraint
|
17
|
+
rescue => e
|
18
|
+
raise ArgumentError, "could not set constraint: #{e}"
|
19
|
+
end
|
20
|
+
|
21
|
+
@control_file.write
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -4,20 +4,35 @@ module AptControl
|
|
4
4
|
|
5
5
|
# Loads and models the contents of a control.ini file
|
6
6
|
# see example-control.ini in root of project for an example
|
7
|
+
#
|
8
|
+
# TODO some sort of actor wrapper that provides sequential write access to
|
9
|
+
# the data model
|
7
10
|
class ControlFile
|
8
11
|
|
9
12
|
def initialize(path, logger)
|
10
13
|
@logger = logger
|
11
14
|
@watch_mutex = Mutex.new
|
12
15
|
@path = path
|
13
|
-
|
14
|
-
|
16
|
+
@distributions =
|
17
|
+
if File.exists?(@path)
|
18
|
+
inifile = IniFile.load(@path)
|
19
|
+
parse!(inifile)
|
20
|
+
else
|
21
|
+
[]
|
22
|
+
end
|
15
23
|
end
|
16
24
|
|
17
25
|
def distributions
|
26
|
+
# not sure that this is strictly necessary - there is no true concurrency
|
27
|
+
# in MRI/YARV, but that doesn't mean a thread couldn't be interrupted half
|
28
|
+
# way through initialising a variable, does it?
|
18
29
|
@watch_mutex.synchronize { @distributions }
|
19
30
|
end
|
20
31
|
|
32
|
+
def [](dist_name)
|
33
|
+
distributions.find {|d| d.name == dist_name }
|
34
|
+
end
|
35
|
+
|
21
36
|
def dump
|
22
37
|
@watch_mutex.synchronize do
|
23
38
|
@distributions.each do |d|
|
@@ -29,6 +44,22 @@ module AptControl
|
|
29
44
|
end
|
30
45
|
end
|
31
46
|
|
47
|
+
def write
|
48
|
+
IniFile.new.tap do |inifile|
|
49
|
+
inifile.filename = @path
|
50
|
+
distributions.each do |distribution|
|
51
|
+
inifile[distribution.name] = distribution.inject({}) do |hash, rule|
|
52
|
+
hash.tap do |h|
|
53
|
+
# quote the restriction, as inifile doesn't do this for you
|
54
|
+
# https://github.com/TwP/inifile/pull/16
|
55
|
+
h[rule.package_name] = '"' + rule.restriction_string + '"'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
inifile.write
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
32
63
|
def parse!(inifile)
|
33
64
|
inifile.sections.map do |section|
|
34
65
|
rules = inifile[section].map do |key, value|
|
@@ -38,6 +69,15 @@ module AptControl
|
|
38
69
|
end
|
39
70
|
end
|
40
71
|
|
72
|
+
def reload!
|
73
|
+
inifile = IniFile.load(@path)
|
74
|
+
distributions = parse!(inifile)
|
75
|
+
|
76
|
+
@watch_mutex.synchronize do
|
77
|
+
@distributions = distributions
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
41
81
|
# Watch the control file for changes, rebuilding
|
42
82
|
# internal data structures when it does
|
43
83
|
def watch(fs_listener_factory, &block)
|
@@ -48,14 +88,9 @@ module AptControl
|
|
48
88
|
fs_listener_factory.new(dir, /#{Regexp.quote(fname)}/) do |modified, added, removed|
|
49
89
|
begin
|
50
90
|
@logger.info("Change to control file detected...")
|
51
|
-
|
52
|
-
distributions = parse!(inifile)
|
53
|
-
|
54
|
-
@watch_mutex.synchronize do
|
55
|
-
@distributions = distributions
|
56
|
-
end
|
57
|
-
@logger.info("...rebuilt")
|
91
|
+
reload!
|
58
92
|
yield if block_given?
|
93
|
+
@logger.info("...rebuilt")
|
59
94
|
rescue => e
|
60
95
|
@logger.error("Error reloading changes: #{e}")
|
61
96
|
@logger.error(e)
|
@@ -75,6 +110,10 @@ module AptControl
|
|
75
110
|
|
76
111
|
def initialize(name, constraint)
|
77
112
|
@package_name = name
|
113
|
+
self.constraint = constraint
|
114
|
+
end
|
115
|
+
|
116
|
+
def constraint=(constraint)
|
78
117
|
version = nil
|
79
118
|
constraint.split(" ").tap do |split|
|
80
119
|
@restriction, version = if split.size == 1
|
@@ -121,22 +160,47 @@ module AptControl
|
|
121
160
|
def includeable_to(available)
|
122
161
|
available.select {|a| satisfied_by?(a) }
|
123
162
|
end
|
163
|
+
|
164
|
+
def restriction_string
|
165
|
+
"#{@restriction} #{@version}"
|
166
|
+
end
|
167
|
+
|
168
|
+
# FIXME to_s should include the package name
|
169
|
+
alias :to_s :restriction_string
|
124
170
|
end
|
125
171
|
|
126
172
|
# represents a set of rules mapped to a particular distribution, i.e.
|
127
173
|
# squeeze is a distribution
|
128
174
|
class Distribution
|
175
|
+
include Enumerable
|
176
|
+
|
129
177
|
def initialize(name, rules)
|
130
178
|
@name = name
|
131
179
|
@package_rules = rules
|
132
180
|
end
|
181
|
+
|
133
182
|
attr_reader :name
|
134
183
|
attr_reader :package_rules
|
135
184
|
|
185
|
+
def each(&block)
|
186
|
+
(@package_rules || []).each do |rule|
|
187
|
+
yield(rule)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
136
191
|
# find a PackageRule by package name
|
137
192
|
def [](package_name)
|
138
193
|
package_rules.find {|rule| rule.package_name == package_name }
|
139
194
|
end
|
195
|
+
|
196
|
+
def []=(package_name, new_constraint)
|
197
|
+
existing_rule = self[package_name]
|
198
|
+
if existing_rule
|
199
|
+
existing_rule.constraint = new_constraint
|
200
|
+
else
|
201
|
+
package_rules << PackageRule.new(package_name, new_constraint)
|
202
|
+
end
|
203
|
+
end
|
140
204
|
end
|
141
205
|
end
|
142
206
|
end
|
data/lib/apt_control/jabber.rb
CHANGED
@@ -8,15 +8,26 @@ module AptControl
|
|
8
8
|
include ::Jabber
|
9
9
|
|
10
10
|
def initialize(options)
|
11
|
+
@enabled = options[:enabled]
|
11
12
|
@jid = options[:jid]
|
12
13
|
@password = options[:password]
|
13
14
|
@room_jid = options[:room_jid]
|
14
15
|
@logger = options[:logger]
|
15
16
|
|
16
|
-
|
17
|
+
if @enabled
|
18
|
+
@room_nick = @room_jid && @room_jid.split('/').last
|
19
|
+
@room_listeners = []
|
20
|
+
|
21
|
+
swallow_errors { connect! } unless options[:defer_connect]
|
22
|
+
end
|
17
23
|
end
|
18
24
|
|
25
|
+
def enabled? ; @enabled ; end
|
26
|
+
def room_nick ; @room_nick ; end
|
27
|
+
|
19
28
|
def send_message(msg)
|
29
|
+
return unless enabled?
|
30
|
+
|
20
31
|
if not_connected?
|
21
32
|
connect!
|
22
33
|
end
|
@@ -52,6 +63,13 @@ module AptControl
|
|
52
63
|
|
53
64
|
def not_connected? ; ! connected? ; end
|
54
65
|
|
66
|
+
def add_room_listener(listener)
|
67
|
+
@room_listeners << listener
|
68
|
+
end
|
69
|
+
|
70
|
+
include Actors::Proxied
|
71
|
+
proxy :send_message
|
72
|
+
|
55
73
|
private
|
56
74
|
|
57
75
|
def attempt_reconnect(&block)
|
@@ -85,11 +103,36 @@ module AptControl
|
|
85
103
|
@muc = Jabber::MUC::SimpleMUCClient.new(@client)
|
86
104
|
@muc.join(JID.new(@room_jid))
|
87
105
|
@logger.info("joined room #{@room_jid}")
|
106
|
+
setup_muc_callbacks
|
88
107
|
rescue JabberError => e
|
89
108
|
raise ConnectionError.new("error joining room", e)
|
90
109
|
end
|
91
110
|
end
|
92
111
|
|
112
|
+
def setup_muc_callbacks
|
113
|
+
@muc.on_message do |time, nick, text|
|
114
|
+
next if time # skip history
|
115
|
+
next if @room_nick == nick
|
116
|
+
notify_room_listeners(text)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def notify_room_listeners(text)
|
121
|
+
@room_listeners.each do |l|
|
122
|
+
begin
|
123
|
+
if l.is_a? Celluloid
|
124
|
+
l.async.on_message(text)
|
125
|
+
else
|
126
|
+
l.on_message(text)
|
127
|
+
end
|
128
|
+
rescue => e
|
129
|
+
@logger.error("listener #{l} raised error: #{e}")
|
130
|
+
@logger.error(e)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
|
93
136
|
# Thank you to http://rubyforge.org/projects/nestegg for the pattern
|
94
137
|
class Error < StandardError
|
95
138
|
|
@@ -114,4 +157,7 @@ module AptControl
|
|
114
157
|
class ConnectionError < Error ; end
|
115
158
|
class SendError < Error ; end
|
116
159
|
end
|
160
|
+
|
161
|
+
|
162
|
+
|
117
163
|
end
|
@@ -17,6 +17,12 @@ module AptControl
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
20
|
+
|
21
|
+
def find_state(dist_name, package_name)
|
22
|
+
find do |state|
|
23
|
+
state.dist.name == dist_name && state.package_name == package_name
|
24
|
+
end
|
25
|
+
end
|
20
26
|
end
|
21
27
|
|
22
28
|
# Brings together the state of a particular package in a particular
|
@@ -55,5 +61,17 @@ module AptControl
|
|
55
61
|
def includeable_to
|
56
62
|
rule.includeable_to(available)
|
57
63
|
end
|
64
|
+
|
65
|
+
def status_line
|
66
|
+
[
|
67
|
+
dist.name,
|
68
|
+
package_name,
|
69
|
+
"(#{rule.restriction} #{rule.version})",
|
70
|
+
"#{includeable? ? 'I' : '.'}#{satisfied? ? 'S' : '.'}",
|
71
|
+
"included=#{included || '<none>'}",
|
72
|
+
"available=#{available? ? available.join(', ') : '<none>'} "
|
73
|
+
].join(' ')
|
74
|
+
end
|
75
|
+
|
58
76
|
end
|
59
77
|
end
|
data/lib/apt_control/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apt_control
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-06-
|
12
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: popen4
|
@@ -82,7 +82,7 @@ dependencies:
|
|
82
82
|
requirements:
|
83
83
|
- - ~>
|
84
84
|
- !ruby/object:Gem::Version
|
85
|
-
version: '1.
|
85
|
+
version: '1.2'
|
86
86
|
type: :runtime
|
87
87
|
prerelease: false
|
88
88
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -90,7 +90,7 @@ dependencies:
|
|
90
90
|
requirements:
|
91
91
|
- - ~>
|
92
92
|
- !ruby/object:Gem::Version
|
93
|
-
version: '1.
|
93
|
+
version: '1.2'
|
94
94
|
- !ruby/object:Gem::Dependency
|
95
95
|
name: rb-inotify
|
96
96
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,6 +107,22 @@ dependencies:
|
|
107
107
|
- - ~>
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '0.9'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: celluloid
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.14.0
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 0.14.0
|
110
126
|
- !ruby/object:Gem::Dependency
|
111
127
|
name: minitest
|
112
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,15 +166,22 @@ files:
|
|
150
166
|
- bin/apt_control
|
151
167
|
- lib/apt_control.rb
|
152
168
|
- lib/apt_control/jabber.rb
|
169
|
+
- lib/apt_control/commands.rb
|
153
170
|
- lib/apt_control/package_states.rb
|
171
|
+
- lib/apt_control/actors.rb
|
172
|
+
- lib/apt_control/bot.rb
|
154
173
|
- lib/apt_control/cli.rb
|
155
174
|
- lib/apt_control/version.rb
|
156
|
-
- lib/apt_control/includer.rb
|
157
175
|
- lib/apt_control/exec.rb
|
176
|
+
- lib/apt_control/cli/set.rb
|
158
177
|
- lib/apt_control/cli/watch.rb
|
178
|
+
- lib/apt_control/cli/promote.rb
|
159
179
|
- lib/apt_control/cli/include.rb
|
160
180
|
- lib/apt_control/cli/status.rb
|
161
181
|
- lib/apt_control/build_archive.rb
|
182
|
+
- lib/apt_control/commands/set.rb
|
183
|
+
- lib/apt_control/commands/promote.rb
|
184
|
+
- lib/apt_control/commands/include.rb
|
162
185
|
- lib/apt_control/apt_site.rb
|
163
186
|
- lib/apt_control/control_file.rb
|
164
187
|
homepage: http://github.com/playlouder/apt_control
|