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.
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ class AptlyError < RuntimeError
2
+ attr_accessor :output
3
+ @output = nil
4
+
5
+ def initialize msg=nil, output=nil
6
+ @output = output
7
+ super msg
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class Hash
2
+ def arg arg, default
3
+ if self.has_key? arg
4
+ return self[arg]
5
+ else
6
+ return default
7
+ end
8
+ end
9
+ end
@@ -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
@@ -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