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