repomate 0.1.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/bin/repomate ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # expand path if in development mode
4
+ if File.exists?(File.join(File.join(File.dirname(__FILE__), '..', '.git')))
5
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
6
+ end
7
+
8
+ require 'repomate'
9
+ require 'rubygems'
10
+ require 'slop'
11
+
12
+ options = Slop.parse do
13
+ banner "RepoMate (A simple debian repository management tool)
14
+
15
+ Usage: #{$0} add -s squeeze [-c main] <package>
16
+ #{$0} publish
17
+ #{$0} listpackages -r production
18
+
19
+ Actions:
20
+ add - Add a package to the staging area
21
+ publish - Move packages from staging area to production
22
+ save - Save a checkpoint
23
+ load - Load a checkpoint
24
+ listpackages - List packages
25
+ setup - Setup the pool
26
+
27
+ Options:"
28
+ on :s, :suitename=, "Set the name of the suite (lenny/squeeze...)", :argument => true
29
+ on :c, :component=, "Set the name of the component (main/contrib...)", :default => "main"
30
+ on :a, :architecture=, "Set the name of the component (main/contrib...)", :argument => true
31
+ on :r, :repodir, "Type of pool/category (stage/pool/production)", :argument => true
32
+ on :force, "Force action", :default => false
33
+ on :h, :help, 'Print this help message', :tail => true do
34
+ puts help
35
+ exit
36
+ end
37
+ end
38
+
39
+ cli = RepoMate::Cli.new
40
+
41
+ if ARGV.include?("add")
42
+ filename = nil
43
+ ARGV.each do |arg|
44
+ if arg =~ /\.deb/
45
+ filename = arg
46
+ end
47
+ end
48
+ if filename && File.exists?(filename)
49
+ cli.stage(options, filename)
50
+ else
51
+ STDERR.puts "File does not exist"
52
+ end
53
+ elsif ARGV.include?("publish")
54
+ cli.publish(options)
55
+ elsif ARGV.include?("save")
56
+ cli.save_checkpoint
57
+ elsif ARGV.include?("load")
58
+ cli.choose_checkpoint
59
+ elsif ARGV.include?("listpackages")
60
+ cli.list_packagelist(options)
61
+ elsif ARGV.include?("setup")
62
+ cli.setup(options)
63
+ else
64
+ puts options.help
65
+ end
data/etc/config.yml ADDED
@@ -0,0 +1,18 @@
1
+ ---
2
+ :rootdir: /var/lib/repomate/repository
3
+ :logdir: /var/log/repomate
4
+ :dpkg: /usr/bin/dpkg
5
+ :suites:
6
+ - lenny
7
+ - squeeze
8
+ :components:
9
+ - main
10
+ - contrib
11
+ :architectures:
12
+ - all
13
+ - amd64
14
+ :origin: Repository
15
+ :label: Repository
16
+ :gpg_enable: yes
17
+ :gpg_email: someone@example.net
18
+ :gpg_password: secret
@@ -0,0 +1,105 @@
1
+ # RepoMate module
2
+ module RepoMate
3
+
4
+ # Class for the architecture layer of the directory structure
5
+ class Architecture
6
+
7
+ # Init
8
+ def initialize(architecture, component, suitename, category)
9
+ @architecture = architecture
10
+ @component = component
11
+ @suitename = suitename
12
+ @category = category
13
+ end
14
+
15
+ # Returns the given architecture name (eg. all, amd64)
16
+ def name
17
+ @architecture
18
+ end
19
+
20
+ # Returns the directory strcuture of the architecture including all lower layers
21
+ def directory
22
+ File.join(Cfg.rootdir, @category, @suitename, @component, "binary-#{name}")
23
+ end
24
+
25
+ # Checks if the architecture directory exists
26
+ def exist?
27
+ Dir.exist?(directory)
28
+ end
29
+
30
+ # Checks if the architecture is allowed (See: configurationfile)
31
+ def is_allowed?
32
+ self.allowed.include?(@architecture)
33
+ end
34
+
35
+ # Checks if directory is unused
36
+ def is_unused?(dir)
37
+ status = true
38
+
39
+ path = Dir.glob(File.join(dir, "*"))
40
+ path.each do |dirorfile|
41
+ status = false if File.directory?(dirorfile)
42
+ status = false if File.basename(dirorfile) =~ /\.deb$/
43
+ end
44
+
45
+ status
46
+ end
47
+
48
+ # Creates the directory strcuture of the architecture including all lower layers
49
+ def create
50
+ FileUtils.mkdir_p(directory) unless exist?
51
+ end
52
+
53
+ # Deletes the architecture directory including all lower layers
54
+ def destroy
55
+ FileUtils.rm_r(directory) if exist?
56
+ end
57
+
58
+ # Returns a list of all debian files in the architecture directory
59
+ def files
60
+ Dir.glob(File.join(directory, "*.deb"))
61
+ end
62
+
63
+ # Returns a dataset including the name of the architecture and the fullpath recursive through all lower layers
64
+ def self.dataset(category=nil)
65
+ data = []
66
+ self.all.each do |entry|
67
+ parts = entry.split(/\//)
68
+ unless parts.length < 4
69
+ next unless parts[0].eql?(category) || category.eql?("all")
70
+ architecture = parts[3].split(/-/)
71
+ data << {
72
+ :category => parts[0],
73
+ :suitename => parts[1],
74
+ :component => parts[2],
75
+ :architecture_dir => parts[3],
76
+ :architecture => architecture[1],
77
+ :fullpath => File.join(Cfg.rootdir, entry)
78
+ }
79
+ end
80
+ end
81
+ data
82
+ end
83
+
84
+ # Returns all directories without @rootdir
85
+ def self.all
86
+ components = Component.all
87
+ dirs = []
88
+ rootdir = Cfg.rootdir
89
+ components.each do |component|
90
+ architectures = Dir.glob(File.join(rootdir, component, "*"))
91
+ architectures.each do |architecture|
92
+ dirs.push architecture.gsub(/#{rootdir}\//, '') if File.directory? architecture
93
+ end
94
+ end
95
+ return dirs
96
+ end
97
+
98
+ # Gets all configured architectures
99
+ def self.allowed
100
+ Cfg.architectures.uniq
101
+ end
102
+
103
+ end
104
+ end
105
+
@@ -0,0 +1,335 @@
1
+ require 'repomate'
2
+ require 'date'
3
+ require 'time'
4
+ require 'colors'
5
+
6
+ # RepoMate module
7
+ module RepoMate
8
+
9
+ # Class containing the main logic
10
+ class Base
11
+
12
+ # Init
13
+ def initialize
14
+ FileUtils.mkdir_p(Cfg.rootdir)
15
+
16
+ @repository = Repository.new
17
+ @metafile = Metafile.new
18
+ @cpdbfile = File.join(Cfg.rootdir, "checkpoints.db")
19
+ @cpdb = Database.new(@cpdbfile)
20
+
21
+ unless Dir.exists?(Cfg.logdir)
22
+ puts
23
+ puts "\tPlease run \"repomate setup\" first!".hl(:red)
24
+ puts
25
+ end
26
+
27
+ create_checkpoints_table
28
+
29
+ end
30
+
31
+ # Add's a package to the staging area
32
+ def stage(workload)
33
+ workload.each do |entry|
34
+ @repository.create(entry[:suitename], entry[:component])
35
+
36
+ package = Package.new(entry[:package_fullname], entry[:suitename], entry[:component])
37
+ destination = Component.new(entry[:component], entry[:suitename], "stage")
38
+
39
+ FileUtils.copy(entry[:package_fullname], File.join(destination.directory, package.newbasename))
40
+ end
41
+ end
42
+
43
+ # Returns a list of staged packages for cli confirmation packed as array of hashes
44
+ def prepare_publish
45
+ workload = []
46
+
47
+ source_category = "stage"
48
+ destination_category = "pool"
49
+
50
+ Component.dataset(source_category).each do |entry|
51
+ source = Component.new(entry[:component], entry[:suitename], source_category)
52
+ source.files.each do |fullname|
53
+ package = Package.new(fullname, entry[:suitename], entry[:component])
54
+ destination = Architecture.new(package.architecture, entry[:component], entry[:suitename], destination_category)
55
+
56
+ workload << {
57
+ :source_fullname => fullname,
58
+ :destination_fullname => File.join(destination.directory, package.newbasename),
59
+ :component => entry[:component],
60
+ :suitename => entry[:suitename],
61
+ :architecture => package.architecture
62
+ }
63
+ end
64
+ end
65
+ workload
66
+ end
67
+
68
+ # Publish all staged packages. Packages will be moved from stage to pool and linked to dists
69
+ def publish(workload)
70
+ newworkload = []
71
+ workload.each do |entry|
72
+ destination = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], "dists")
73
+ basename = File.basename(entry[:source_fullname])
74
+
75
+ @repository.create(entry[:suitename], entry[:component], entry[:architecture])
76
+
77
+ newworkload << {
78
+ :source_fullname => entry[:destination_fullname],
79
+ :destination_dir => destination.directory,
80
+ :component => entry[:component],
81
+ :suitename => entry[:suitename],
82
+ :architecture => entry[:architecture]
83
+ }
84
+ FileUtils.move(entry[:source_fullname], entry[:destination_fullname])
85
+ end
86
+ workload = newworkload
87
+
88
+ save_checkpoint
89
+ check_versions(workload)
90
+ end
91
+
92
+ # Does the link job after checking versions through dpkg
93
+ def check_versions(workload)
94
+ dpkg = Cfg.dpkg
95
+
96
+ raise "dpkg is not installed" unless File.exists?(dpkg)
97
+
98
+ link_workload = []
99
+ unlink_workload = []
100
+
101
+ workload.each do |entry|
102
+ source_package = Package.new(entry[:source_fullname], entry[:suitename], entry[:component])
103
+ destination_fullname = File.join(entry[:destination_dir], source_package.newbasename)
104
+
105
+ Dir.glob("#{entry[:destination_dir]}/#{source_package.name}*.deb") do |target_fullname|
106
+ target_package = Package.new(target_fullname, entry[:suitename], entry[:component] )
107
+
108
+ if system("#{dpkg} --compare-versions #{source_package.version} gt #{target_package.version}")
109
+ puts "Package: #{target_package.newbasename} will be replaced with #{source_package.newbasename}"
110
+ unlink_workload << {
111
+ :destination_fullname => target_fullname,
112
+ :newbasename => target_package.newbasename
113
+ }
114
+ elsif system("#{dpkg} --compare-versions #{source_package.version} eq #{target_package.version}")
115
+ puts "Package: #{source_package.newbasename} already exists with same version"
116
+ return
117
+ elsif system("#{dpkg} --compare-versions #{source_package.version} lt #{target_package.version}")
118
+ puts "Package: #{source_package.newbasename} already exists with higher version"
119
+ return
120
+ end
121
+ end
122
+
123
+ link_workload << {
124
+ :source_fullname => source_package.fullname,
125
+ :destination_fullname => destination_fullname,
126
+ :suitename => source_package.suitename,
127
+ :component => source_package.component,
128
+ :newbasename => source_package.newbasename
129
+ }
130
+ end
131
+
132
+ unlink(unlink_workload)
133
+ link(link_workload)
134
+ end
135
+
136
+ # links the workload
137
+ def link(workload)
138
+ action = false
139
+
140
+ workload.each do |entry|
141
+ @repository.create(entry[:suitename], entry[:component], entry[:architecture])
142
+ unless File.exists?(entry[:destination_fullname])
143
+ package = Package.new(entry[:source_fullname], entry[:suitename], entry[:component])
144
+ package.create_checksums
145
+
146
+ File.symlink(entry[:source_fullname], entry[:destination_fullname])
147
+ puts "Package: #{package.newbasename} linked to production => #{entry[:suitename]}/#{entry[:component]}"
148
+ action = true
149
+ end
150
+ end
151
+
152
+ if action
153
+ @metafile.create
154
+ end
155
+ end
156
+
157
+ # unlinks workload
158
+ def unlink(workload)
159
+ action = false
160
+
161
+ workload.each do |entry|
162
+ package = Package.new(entry[:destination_fullname], entry[:suitename], entry[:component])
163
+ package.delete_checksums
164
+
165
+ File.unlink(entry[:destination_fullname])
166
+ puts "Package: #{package.newbasename} unlinked"
167
+ action = true
168
+ end
169
+
170
+ if action
171
+ cleandirs
172
+ @metafile.create
173
+ end
174
+ end
175
+
176
+ # Create the checkpoint table
177
+ def create_checkpoints_table
178
+ sql = "create table if not exists checkpoints (
179
+ date varchar(25),
180
+ suitename varchar(10),
181
+ component varchar(10),
182
+ architecture varchar(10),
183
+ basename varchar(70)
184
+ )"
185
+ @cpdb.query(sql)
186
+ end
187
+
188
+ # Saves a checkpoint
189
+ def save_checkpoint
190
+ datetime = DateTime.now
191
+ source_category = "dists"
192
+
193
+ Architecture.dataset(source_category).each do |entry|
194
+ source = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], source_category)
195
+ source.files.each do |fullname|
196
+ basename = File.basename(fullname)
197
+ @cpdb.query("insert into checkpoints values ( '#{datetime}', '#{entry[:suitename]}', '#{entry[:component]}', '#{entry[:architecture]}', '#{basename}' )")
198
+ end
199
+ end
200
+
201
+ puts "Checkpoint (#{datetime.strftime("%F %T")}) saved"
202
+ end
203
+
204
+ # Loads a checkpoint
205
+ def load_checkpoint(number)
206
+ list = get_checkpoints
207
+ link_workload = []
208
+ unlink_workload = []
209
+ source_category = "dists"
210
+
211
+ Architecture.dataset(source_category).each do |entry|
212
+ destination = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], source_category)
213
+ destination.files.each do |fullname|
214
+ unlink_workload << {
215
+ :destination_fullname => fullname,
216
+ :component => entry[:component],
217
+ :suitename => entry[:suitename],
218
+ :architecture => entry[:architecture]
219
+ }
220
+ end
221
+ end
222
+
223
+ @cpdb.query("select date, suitename, component, architecture, basename from checkpoints").each do |row|
224
+ if row[0] == list[number]
225
+ suitename = row[1]
226
+ component = row[2]
227
+ architecture = row[3]
228
+ basename = row[4]
229
+ source = Architecture.new(architecture, component, suitename, "pool")
230
+ destination = Architecture.new(architecture, component, suitename, "dists")
231
+
232
+ link_workload << {
233
+ :source_fullname => File.join(source.directory, basename),
234
+ :destination_fullname => File.join(destination.directory, basename),
235
+ :component => component,
236
+ :suitename => suitename,
237
+ :architecture => architecture
238
+ }
239
+ end
240
+ end
241
+
242
+ unlink(unlink_workload)
243
+ link(link_workload)
244
+ end
245
+
246
+ # Returns a list of checkpoints for the cli
247
+ def get_checkpoints
248
+ order = 0
249
+ dates = []
250
+ list = {}
251
+
252
+ @cpdb.query("select date from checkpoints group by date order by date asc").each do |row|
253
+ dates << row.first
254
+ end
255
+
256
+ dates.each do |date|
257
+ order += 1
258
+ list[order] = date
259
+ end
260
+
261
+ list
262
+ end
263
+
264
+ # Returns a list of packages
265
+ def get_packagelist(category)
266
+ packages = []
267
+ if category.eql?("stage")
268
+ Component.dataset(category).each do |entry|
269
+ source = Component.new(entry[:component], entry[:suitename], category)
270
+ source.files.each do |fullname|
271
+ package = Package.new(fullname, entry[:suitename], entry[:component])
272
+
273
+ packages << {
274
+ :fullname => fullname,
275
+ :controlfile => package.controlfile,
276
+ :component => entry[:component],
277
+ :suitename => entry[:suitename]
278
+ }
279
+ end
280
+ end
281
+ else
282
+ Architecture.dataset(category).each do |entry|
283
+ source = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], category)
284
+ source.files.each do |fullname|
285
+ package = Package.new(fullname, entry[:suitename], entry[:component])
286
+
287
+ packages << {
288
+ :fullname => fullname,
289
+ :controlfile => package.controlfile,
290
+ :component => entry[:component],
291
+ :suitename => entry[:suitename],
292
+ :architecture => entry[:architecture]
293
+ }
294
+ end
295
+ end
296
+ end
297
+ packages
298
+ end
299
+
300
+ # cleans up unused directories
301
+ def cleandirs
302
+ action = false
303
+
304
+ @repository.categories.each do |category|
305
+ next if category.eql?("stage")
306
+ Architecture.dataset(category).each do |entry|
307
+ directory = Architecture.new(entry[:architecture], entry[:component], entry[:suitename], category)
308
+ if directory.is_unused?(entry[:fullpath])
309
+ action = true
310
+ directory.destroy
311
+ end
312
+ end
313
+ Component.dataset(category).each do |entry|
314
+ directory = Component.new(entry[:component], entry[:suitename], category)
315
+ if directory.is_unused?(entry[:fullpath])
316
+ action = true
317
+ directory.destroy
318
+ end
319
+ end
320
+ Suite.dataset(category).each do |entry|
321
+ directory = Suite.new(entry[:suitename], category)
322
+ if directory.is_unused?(entry[:fullpath])
323
+ action = true
324
+ directory.destroy
325
+ end
326
+ end
327
+ end
328
+ if action
329
+ puts "Cleaning structure"
330
+ @metafile.create
331
+ end
332
+ end
333
+ end
334
+ end
335
+
@@ -0,0 +1,60 @@
1
+ # RepoMate module
2
+ module RepoMate
3
+
4
+ # Class for the category layer of the directory structure
5
+ class Category
6
+
7
+ # Init
8
+ def initialize(category)
9
+ @category = category
10
+ end
11
+
12
+ # Returns the name of the category (eg. pool, dists)
13
+ def name
14
+ @category
15
+ end
16
+
17
+ # Returns the full path of the categories directory
18
+ def directory
19
+ File.join(Cfg.rootdir, @category)
20
+ end
21
+
22
+ # Checks if the category directory exists
23
+ def exist?
24
+ Dir.exist?(directory)
25
+ end
26
+
27
+ # Creates the directory strcuture of the category
28
+ def create
29
+ FileUtils.mkdir_p(directory) unless exist?
30
+ end
31
+
32
+ # Deletes a categories directory
33
+ def destroy
34
+ FileUtils.rm_r(directory) if exist?
35
+ end
36
+
37
+ # Returns a dataset including the name of the category and the fullpath
38
+ def self.dataset(category=nil)
39
+ data = []
40
+ self.all.each do |entry|
41
+ unless entry.nil?
42
+ next unless entry.eql?(category) || category.eql?("all")
43
+ data << {
44
+ :category => entry,
45
+ :fullpath => File.join(Cfg.rootdir, entry)
46
+ }
47
+ end
48
+ end
49
+ data
50
+ end
51
+
52
+ # Returns all directories
53
+ def self.all
54
+ dirs = Dir.glob(File.join(Cfg.rootdir, "*"))
55
+ dirs.map{ |dir| File.basename(dir) unless dirs.include?(File.basename(dir)) }
56
+ end
57
+
58
+ end
59
+ end
60
+
@@ -0,0 +1,123 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ # RepoMate module
5
+ module RepoMate
6
+
7
+ # Class for the commandline interface
8
+ class Cli
9
+
10
+ # Init
11
+ def initialize
12
+ @repomate = Base.new
13
+ @repository = Repository.new
14
+ end
15
+
16
+ # Sets up the base directory structure by calling the repository class
17
+ def setup(options)
18
+ if options.suitename?
19
+ @repository.create(options[:suitename], options[:component], options[:architecture])
20
+ else
21
+ STDERR.puts "Specify a suitename with [-s|--suitname]"
22
+ exit 1
23
+ end
24
+ end
25
+
26
+ # Adds a given package to the staging area by calling the base class
27
+ def stage(options, filename)
28
+ if options.suitename?
29
+ workload = []
30
+ workload << {:package_fullname => filename, :suitename => options[:suitename], :component => options[:component]}
31
+
32
+ puts "Package: #{filename} moving to stage => #{options[:suitename]}/#{options[:component]}"
33
+
34
+ @repomate.stage(workload)
35
+ else
36
+ STDERR.puts "Specify a suitename with [-s|--suitname]"
37
+ exit 1
38
+ end
39
+ end
40
+
41
+ # Get's all packages from the staging area. Packages need to be confirmed here.
42
+ def publish(options)
43
+ @repomate.prepare_publish.each do |entry|
44
+ workload = []
45
+ basename = File.basename(entry[:source_fullname])
46
+ suitename = entry[:suitename]
47
+ component = entry[:component]
48
+
49
+ unless options.force?
50
+ printf "\n%s", "Link #{basename} to production => #{suitename}/#{component}? [y|yes|n|no]: "
51
+ input = STDIN.gets
52
+ end
53
+
54
+ if options.force? || input =~ /(y|yes)/
55
+ workload << {
56
+ :source_fullname => entry[:source_fullname],
57
+ :destination_fullname => entry[:destination_fullname],
58
+ :component => entry[:component],
59
+ :suitename => entry[:suitename],
60
+ :architecture => entry[:architecture]
61
+ }
62
+ end
63
+ @repomate.publish(workload) unless workload.empty?
64
+ end
65
+ end
66
+
67
+ # Save a checkpoint
68
+ def save_checkpoint
69
+ # Add verification and some output here
70
+ @repomate.save_checkpoint
71
+ end
72
+
73
+ # List all packages, see cli output
74
+ def list_packagelist(options)
75
+ if options.repodir?
76
+ architecture = "unknown"
77
+
78
+ packages = @repomate.get_packagelist(options[:repodir])
79
+ packages.each do |package|
80
+ architecture = package[:architecture] if package[:architecture]
81
+ printf "%-50s%-20s%s\n", package[:controlfile]['Package'], package[:controlfile]['Version'], "#{package[:suitename]}/#{package[:component]}/#{architecture}"
82
+ end
83
+ else
84
+ STDERR.puts "Specify a category with [-r|--repodir]"
85
+ exit 1
86
+ end
87
+ end
88
+
89
+ # Choose a checkpoint to restore.
90
+ def choose_checkpoint
91
+ list = @repomate.get_checkpoints
92
+
93
+ if list.empty?
94
+ STDERR.puts "We can't restore because we don't have checkpoints"
95
+ exit 1
96
+ end
97
+
98
+ puts "\n*** Restore production links to a date below. ***
99
+ Remember: If you need to restore, the last entry might be the one you want!
100
+ Everything between the last two \"publish (-P) commands\" will be lost if you proceed!\n\n"
101
+
102
+ list.each do |num, date|
103
+ datetime = DateTime.parse(date)
104
+ puts "#{num}) #{datetime.strftime("%F %T")}"
105
+ end
106
+
107
+ printf "\n%s", "Enter number or [q|quit] to abord: "
108
+ input = STDIN.gets
109
+ number = input.to_i
110
+
111
+ if input =~ /(q|quit)/
112
+ puts "Aborting..."
113
+ exit 0
114
+ elsif list[number].nil?
115
+ STDERR.puts "Invalid number"
116
+ exit 1
117
+ else
118
+ @repomate.load_checkpoint(number)
119
+ end
120
+ end
121
+ end
122
+ end
123
+