aptly 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.
- checksums.yaml +7 -0
- data/lib/aptly.rb +102 -0
- data/lib/aptly/error.rb +9 -0
- data/lib/aptly/hash.rb +9 -0
- data/lib/aptly/mirror.rb +150 -0
- data/lib/aptly/mutex.rb +78 -0
- data/lib/aptly/publish.rb +147 -0
- data/lib/aptly/repo.rb +255 -0
- data/lib/aptly/snapshot.rb +270 -0
- data/lib/aptly/string.rb +6 -0
- data/lib/aptly/version.rb +3 -0
- data/spec/aptly.conf +3 -0
- data/spec/aptly/aptly_spec.rb +9 -0
- data/spec/aptly/error_spec.rb +11 -0
- data/spec/aptly/hash_spec.rb +13 -0
- data/spec/aptly/mirror_spec.rb +56 -0
- data/spec/aptly/mutex_spec.rb +25 -0
- data/spec/aptly/repo_spec.rb +148 -0
- data/spec/aptly/snapshot_spec.rb +19 -0
- data/spec/aptly/string_spec.rb +13 -0
- data/spec/bin/aptly +3 -0
- data/spec/pkgs/pkg1_1.0.1-1_amd64.deb +0 -0
- data/spec/pkgs/pkg2_1.0.2-2_amd64.deb +0 -0
- data/spec/setup.sh +21 -0
- data/spec/spec_helper.rb +22 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d28012fc713f236f2439309dddc1d30fdb66338e
|
4
|
+
data.tar.gz: ee1285d3545517e13dd2dff95b3222b8a081babf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4d9e6934ea4a9b23b2c3028110d39b8bcbcd7e07dc25132e47854d2bdb570961e32e514147759dd7f5274c28f85ff6f041663960e405479329e5cf2960fb5953
|
7
|
+
data.tar.gz: 3b39146ff8e28f1af97c602c4415a48948a666c6a66d5660aa7d2fbc170f29812af166613188b19e180c3ad5a42d930d61496112b037b6132ef1a9349e4478f1
|
data/lib/aptly.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'aptly/version'
|
2
|
+
require 'aptly/mutex'
|
3
|
+
require 'aptly/error'
|
4
|
+
require 'aptly/mirror'
|
5
|
+
require 'aptly/repo'
|
6
|
+
require 'aptly/snapshot'
|
7
|
+
require 'aptly/publish'
|
8
|
+
require 'aptly/string'
|
9
|
+
require 'aptly/hash'
|
10
|
+
|
11
|
+
module Aptly
|
12
|
+
|
13
|
+
# runcmd handles running arbitrary commands. It is intended to run aptly-
|
14
|
+
# specific commands, and as such, it uses a mutex to take gurantee
|
15
|
+
# consistency and avoid multiple processes modifying aptly simultaneously.
|
16
|
+
#
|
17
|
+
# == Parameters:
|
18
|
+
# cmd::
|
19
|
+
# A string holding the command to run
|
20
|
+
#
|
21
|
+
# == Returns:
|
22
|
+
# The content of stdout
|
23
|
+
#
|
24
|
+
def runcmd cmd
|
25
|
+
Mutex.lock
|
26
|
+
at_exit { Mutex.unlock }
|
27
|
+
|
28
|
+
out = %x(#{cmd} 2>&1)
|
29
|
+
res = $?.exitstatus
|
30
|
+
|
31
|
+
Mutex.unlock
|
32
|
+
|
33
|
+
if res != 0
|
34
|
+
raise AptlyError.new "aptly: command failed: #{cmd}", out
|
35
|
+
end
|
36
|
+
|
37
|
+
return out
|
38
|
+
end
|
39
|
+
|
40
|
+
# Parses the output lines of aptly listing commands
|
41
|
+
#
|
42
|
+
# == Parameters:
|
43
|
+
# lines::
|
44
|
+
# An array of lines of string output from aptly list commands
|
45
|
+
#
|
46
|
+
# == Returns:
|
47
|
+
# An array of items
|
48
|
+
#
|
49
|
+
def parse_list lines
|
50
|
+
items = Array.new
|
51
|
+
lines.each do |line|
|
52
|
+
if line.start_with?(' * ')
|
53
|
+
parts = line.split(/\[|\]/, 3)
|
54
|
+
items << parts[1] if parts.length == 3
|
55
|
+
end
|
56
|
+
end
|
57
|
+
items
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parses the output of aptly show commands.
|
61
|
+
#
|
62
|
+
# == Parameters:
|
63
|
+
# lines::
|
64
|
+
# An array of strings of aptly output
|
65
|
+
#
|
66
|
+
# == Returns:
|
67
|
+
# A hash of information
|
68
|
+
#
|
69
|
+
def parse_info lines
|
70
|
+
items = Hash.new
|
71
|
+
lines.reject{|l| l.empty?}.each do |line|
|
72
|
+
parts = line.split(/:\s/, 2)
|
73
|
+
items[parts[0]] = parts[1].strip if parts.length == 2
|
74
|
+
end
|
75
|
+
|
76
|
+
def multi items, key
|
77
|
+
items[key] = items[key].split(' ') if items.has_key? key
|
78
|
+
end
|
79
|
+
|
80
|
+
multi items, 'Components'
|
81
|
+
multi items, 'Architectures'
|
82
|
+
|
83
|
+
items
|
84
|
+
end
|
85
|
+
|
86
|
+
# Parses aptly output of double-space indented lists
|
87
|
+
#
|
88
|
+
# == Parameters:
|
89
|
+
# lines::
|
90
|
+
# Output lines from an aptly list command
|
91
|
+
#
|
92
|
+
# == Result:
|
93
|
+
# An array of items
|
94
|
+
#
|
95
|
+
def parse_indented_list lines
|
96
|
+
items = Array.new
|
97
|
+
lines.each do |line|
|
98
|
+
items << line.strip if line.start_with? ' '
|
99
|
+
end
|
100
|
+
items
|
101
|
+
end
|
102
|
+
end
|
data/lib/aptly/error.rb
ADDED
data/lib/aptly/hash.rb
ADDED
data/lib/aptly/mirror.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
module Aptly
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Creates a new mirror in aptly. This simply creates the necessary records in
|
5
|
+
# leveldb and doesn't do any heavy lifting.
|
6
|
+
#
|
7
|
+
# == Parameters:
|
8
|
+
# name::
|
9
|
+
# The name of the new repository
|
10
|
+
# baseurl::
|
11
|
+
# The URL to the repository content
|
12
|
+
# dist::
|
13
|
+
# The distribution (e.g. precise, quantal)
|
14
|
+
# components::
|
15
|
+
# The repository components (e.g. main, stable)
|
16
|
+
# archlist::
|
17
|
+
# A list of architecture types to mirror
|
18
|
+
# ignoresigs::
|
19
|
+
# Ignore package signature mismatches
|
20
|
+
# source::
|
21
|
+
# Optionally mirror in source packages
|
22
|
+
#
|
23
|
+
# == Returns:
|
24
|
+
# An Aptly::Mirror object
|
25
|
+
#
|
26
|
+
def create_mirror(
|
27
|
+
name,
|
28
|
+
baseurl,
|
29
|
+
dist,
|
30
|
+
kwargs={}
|
31
|
+
)
|
32
|
+
components = kwargs.arg :components, []
|
33
|
+
archlist = kwargs.arg :archlist, []
|
34
|
+
ignoresigs = kwargs.arg :ignoresigs, false
|
35
|
+
source = kwargs.arg :source, false
|
36
|
+
|
37
|
+
if list_mirrors.include? name
|
38
|
+
raise AptlyError.new "Mirror '#{name}' already exists"
|
39
|
+
end
|
40
|
+
|
41
|
+
if components.length < 1
|
42
|
+
raise AptlyError.new "1 or more components are required"
|
43
|
+
end
|
44
|
+
|
45
|
+
cmd = 'aptly mirror create'
|
46
|
+
cmd += " -architectures #{archlist.join(',')}" if !archlist.empty?
|
47
|
+
cmd += ' -ignore-signatures' if ignoresigs
|
48
|
+
cmd += ' -with-sources' if source
|
49
|
+
cmd += " #{name.quote} #{baseurl.quote} #{dist.quote}"
|
50
|
+
cmd += " #{components.join(' ')}"
|
51
|
+
|
52
|
+
runcmd cmd
|
53
|
+
return Mirror.new name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a list of existing mirrors in aptly
|
57
|
+
#
|
58
|
+
# == Returns
|
59
|
+
# An array of mirror names
|
60
|
+
#
|
61
|
+
def list_mirrors
|
62
|
+
out = runcmd 'aptly mirror list'
|
63
|
+
parse_list out.lines
|
64
|
+
end
|
65
|
+
|
66
|
+
# Retrieves information about a mirror
|
67
|
+
#
|
68
|
+
# == Parameters:
|
69
|
+
# name::
|
70
|
+
# The name of the mirror to retrieve info for
|
71
|
+
#
|
72
|
+
# == Returns:
|
73
|
+
# A hash of mirror information
|
74
|
+
#
|
75
|
+
def mirror_info name
|
76
|
+
out = runcmd "aptly mirror show #{name.quote}"
|
77
|
+
parse_info out.lines
|
78
|
+
end
|
79
|
+
|
80
|
+
class Mirror
|
81
|
+
attr_accessor :name, :baseurl, :dist, :components, :archlist
|
82
|
+
|
83
|
+
@name = ''
|
84
|
+
@baseurl = ''
|
85
|
+
@dist = ''
|
86
|
+
@components = []
|
87
|
+
@archlist = []
|
88
|
+
|
89
|
+
# Instantiates a new Mirror object
|
90
|
+
#
|
91
|
+
# == Parameters:
|
92
|
+
# name::
|
93
|
+
# Then name associated with the mirror
|
94
|
+
#
|
95
|
+
def initialize name
|
96
|
+
if !Aptly::list_mirrors.include? name
|
97
|
+
raise AptlyError.new("Mirror '#{name}' does not exist")
|
98
|
+
end
|
99
|
+
|
100
|
+
info = Aptly::mirror_info name
|
101
|
+
@name = info['Name']
|
102
|
+
@baseurl = info['Archive Root URL']
|
103
|
+
@dist = info['Distribution']
|
104
|
+
@components = info['Components']
|
105
|
+
@archlist = info['Architectures']
|
106
|
+
end
|
107
|
+
|
108
|
+
# Drops an existing mirror from aptly's configuration
|
109
|
+
def drop
|
110
|
+
Aptly::runcmd "aptly mirror drop #{@name.quote}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# List all packages contained in a mirror
|
114
|
+
#
|
115
|
+
# == Returns:
|
116
|
+
# An array of packages
|
117
|
+
#
|
118
|
+
def list_packages
|
119
|
+
res = []
|
120
|
+
out = Aptly::runcmd "aptly mirror show -with-packages #{@name.quote}"
|
121
|
+
Aptly::parse_indented_list out.lines
|
122
|
+
end
|
123
|
+
|
124
|
+
# Shortcut method to snapshot an Aptly::Mirror object
|
125
|
+
def snapshot name
|
126
|
+
Aptly::create_mirror_snapshot name, @name
|
127
|
+
end
|
128
|
+
|
129
|
+
# Updates a repository, syncing in all packages which have not already been
|
130
|
+
# downloaded and caches them locally.
|
131
|
+
#
|
132
|
+
# == Parameters:
|
133
|
+
# ignore_cksum::
|
134
|
+
# Ignore checksum mismatches
|
135
|
+
# ignore_sigs::
|
136
|
+
# Ignore author signature mismatches
|
137
|
+
#
|
138
|
+
def update kwargs={}
|
139
|
+
ignore_cksum = kwargs.arg :ignore_cksum, false
|
140
|
+
ignore_sigs = kwargs.arg :ignore_sigs, false
|
141
|
+
|
142
|
+
cmd = 'aptly mirror update'
|
143
|
+
cmd += ' -ignore-checksums' if ignore_cksum
|
144
|
+
cmd += ' -ignore-signatures' if ignore_sigs
|
145
|
+
cmd += " #{@name.quote}"
|
146
|
+
|
147
|
+
Aptly::runcmd cmd
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/aptly/mutex.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module Aptly
|
2
|
+
extend self
|
3
|
+
|
4
|
+
class Mutex
|
5
|
+
@@mutex_path = '/tmp/aptly.lock'
|
6
|
+
|
7
|
+
# Static attribute accessor for mutex path
|
8
|
+
#
|
9
|
+
# == Returns:
|
10
|
+
# The path to the aptly mutex file
|
11
|
+
#
|
12
|
+
def self.mutex_path
|
13
|
+
@@mutex_path
|
14
|
+
end
|
15
|
+
|
16
|
+
# Alter the mutex path. This should be set to the same path in all places
|
17
|
+
# where ruby-aptly will be used on the same host, since the mutex is meant
|
18
|
+
# to be system-wide.
|
19
|
+
#
|
20
|
+
# == Parameters:
|
21
|
+
# path::
|
22
|
+
# The desired path for the mutex
|
23
|
+
#
|
24
|
+
def self.mutex_path= path
|
25
|
+
@@mutex_path = path
|
26
|
+
end
|
27
|
+
|
28
|
+
# Attempts to acquire the aptly mutex. This method will wait if the mutex
|
29
|
+
# is already locked elsewhere, and check back every 5 seconds to see if it
|
30
|
+
# has been freed. On each check where the mutex is determined to be in use,
|
31
|
+
# we check if the process which originally acquired the lock is still
|
32
|
+
# running so we can acquire a stale mutex should we need to.
|
33
|
+
#
|
34
|
+
# == Returns:
|
35
|
+
# True, once the lock is acquired.
|
36
|
+
#
|
37
|
+
def self.lock
|
38
|
+
while self.locked?
|
39
|
+
self.running? && sleep(5) || self.unlock
|
40
|
+
end
|
41
|
+
File.open(@@mutex_path, 'w') {|f| f.write Process.pid}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Checks if the mutex is in use. This is done via a simple file check.
|
45
|
+
#
|
46
|
+
# == Returns:
|
47
|
+
# True if locked, else false
|
48
|
+
#
|
49
|
+
def self.locked?
|
50
|
+
File.exist? @@mutex_path
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks if the process which originally acquired the mutex is still
|
54
|
+
# running. This is useful to determine staleness of a locked mutex.
|
55
|
+
#
|
56
|
+
# == Returns:
|
57
|
+
# True if running, else false
|
58
|
+
#
|
59
|
+
def self.running?
|
60
|
+
begin
|
61
|
+
pid = File.open(@@mutex_path).read.to_i
|
62
|
+
Process.kill(0, pid)
|
63
|
+
return true
|
64
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Unlocks the mutex so that other processes may acquire it.
|
70
|
+
#
|
71
|
+
# == Returns:
|
72
|
+
# True if unlock happens, else false
|
73
|
+
#
|
74
|
+
def self.unlock
|
75
|
+
File.delete @@mutex_path if self.locked?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Aptly
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Publish a snapshot or repo resource.
|
5
|
+
#
|
6
|
+
# == Parameters:
|
7
|
+
# type::
|
8
|
+
# The type of resource to publish. 'snapshot' and 'repo' supported.
|
9
|
+
# name::
|
10
|
+
# The name of the resource to publish.
|
11
|
+
# prefix::
|
12
|
+
# An optional prefix to publish to.
|
13
|
+
# component::
|
14
|
+
# The component to publish to. If empty, aptly will attempt to guess
|
15
|
+
# from the source, or use 'main' as the default.
|
16
|
+
# dist::
|
17
|
+
# The distribution name. If empty, aptly will try to guess.
|
18
|
+
# gpg_key::
|
19
|
+
# The gpg key to sign the repository. Uses default if not specified.
|
20
|
+
# keyring::
|
21
|
+
# GPG keyring to use
|
22
|
+
# label::
|
23
|
+
# An optional label to give the published resource
|
24
|
+
# origin::
|
25
|
+
# The value of the 'Origin' field
|
26
|
+
# secret_keyring::
|
27
|
+
# GPG secret keyring to use
|
28
|
+
# sign::
|
29
|
+
# When false, don't sign Release files. Defaults to true.
|
30
|
+
#
|
31
|
+
def publish(
|
32
|
+
type,
|
33
|
+
name,
|
34
|
+
kwargs={}
|
35
|
+
)
|
36
|
+
prefix = kwargs.arg :prefix, ''
|
37
|
+
component = kwargs.arg :component, ''
|
38
|
+
dist = kwargs.arg :dist, ''
|
39
|
+
gpg_key = kwargs.arg :gpg_key, ''
|
40
|
+
keyring = kwargs.arg :keyring, ''
|
41
|
+
label = kwargs.arg :label, ''
|
42
|
+
origin = kwargs.arg :origin, ''
|
43
|
+
secret_keyring = kwargs.arg :secret_keyring, ''
|
44
|
+
sign = kwargs.arg :sign, true
|
45
|
+
|
46
|
+
if type != 'repo' && type != 'snapshot'
|
47
|
+
raise AptlyError "Invalid publish type: #{type}"
|
48
|
+
end
|
49
|
+
|
50
|
+
cmd = "aptly publish #{type}"
|
51
|
+
cmd += ' -skip-signing' if !sign
|
52
|
+
cmd += " -component #{component.quote}" if !component.empty?
|
53
|
+
cmd += " -distribution #{dist.quote}" if !dist.empty?
|
54
|
+
cmd += " -gpg-key #{gpg_key.quote}" if !gpg_key.empty?
|
55
|
+
cmd += " -keyring #{keyring.quote}" if !keyring.empty?
|
56
|
+
cmd += " -label #{label.quote}" if !label.empty?
|
57
|
+
cmd += " -origin #{origin.quote}" if !origin.empty?
|
58
|
+
cmd += " #{name.quote}"
|
59
|
+
cmd += " #{prefix.quote}" if !prefix.empty?
|
60
|
+
if !secret_keyring.empty?
|
61
|
+
cmd += " -secret-keyring #{secret_keyring.quote}"
|
62
|
+
end
|
63
|
+
|
64
|
+
runcmd cmd
|
65
|
+
end
|
66
|
+
|
67
|
+
# List existing published resources.
|
68
|
+
#
|
69
|
+
# == Returns
|
70
|
+
# A hash of published resource information. The outer hash key is the
|
71
|
+
# published resource path, and its value is the resource metadata.
|
72
|
+
#
|
73
|
+
def list_published
|
74
|
+
res = Hash.new
|
75
|
+
out = runcmd 'aptly publish list'
|
76
|
+
out.lines.each do |line|
|
77
|
+
if line.start_with? ' * '
|
78
|
+
resource = {}
|
79
|
+
parts = line[3..-1].split(/\[|\]|\(|\)/)
|
80
|
+
resource['path'] = parts[0].strip
|
81
|
+
resource['component'] = parts[1].strip
|
82
|
+
resource['archlist'] = parts[3].split(', ')
|
83
|
+
resource['from_name'] = parts[5].strip
|
84
|
+
if parts[6].include? 'Snapshot'
|
85
|
+
resource['from_type'] = 'snapshot'
|
86
|
+
elsif parts[6].include? 'Repo'
|
87
|
+
resource['from_type'] = 'repo'
|
88
|
+
else
|
89
|
+
next
|
90
|
+
end
|
91
|
+
dist = line.split.last
|
92
|
+
res[dist] = resource
|
93
|
+
end
|
94
|
+
end
|
95
|
+
res
|
96
|
+
end
|
97
|
+
|
98
|
+
class PublishedResource
|
99
|
+
attr_accessor :path, :from_name, :from_type, :component
|
100
|
+
attr_accessor :archlist, :dist
|
101
|
+
|
102
|
+
@path = ''
|
103
|
+
@from_name = ''
|
104
|
+
@from_type = ''
|
105
|
+
@component = ''
|
106
|
+
@archlist = []
|
107
|
+
@dist = ''
|
108
|
+
|
109
|
+
# Instantiates a new published resource object from an existing published
|
110
|
+
# resource.
|
111
|
+
#
|
112
|
+
# == Parameters:
|
113
|
+
# dist::
|
114
|
+
# The distribution name
|
115
|
+
# prefix::
|
116
|
+
# An optional prefix
|
117
|
+
#
|
118
|
+
# == Returns:
|
119
|
+
# An Aptly::PublishedResource object
|
120
|
+
#
|
121
|
+
def initialize dist, prefix=''
|
122
|
+
idx = "#{prefix}#{prefix.empty? ? '' : '/'}#{dist}"
|
123
|
+
|
124
|
+
published = Aptly::list_published
|
125
|
+
if !published.include? idx
|
126
|
+
raise AptlyError.new "Published resource #{idx} does not exist"
|
127
|
+
end
|
128
|
+
|
129
|
+
@dist = dist
|
130
|
+
@prefix = prefix
|
131
|
+
@path = published[idx]['path']
|
132
|
+
@from_name = published[idx]['from_name']
|
133
|
+
@from_type = published[idx]['from_type']
|
134
|
+
@component = published[idx]['component']
|
135
|
+
@archlist = published[idx]['archlist']
|
136
|
+
end
|
137
|
+
|
138
|
+
# Unpublish a currently published resource.
|
139
|
+
def drop
|
140
|
+
cmd = 'aptly publish drop'
|
141
|
+
cmd += " #{@dist.quote}"
|
142
|
+
cmd += " #{@prefix.quote}" if !@prefix.empty?
|
143
|
+
|
144
|
+
Aptly::runcmd cmd
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|