tpkg 1.16.2

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,220 @@
1
+ # We store these gems in our thirdparty directory. So we need to add it
2
+ # it to the search path
3
+ # This one is for when everything is installed
4
+ $:.unshift(File.join(File.dirname(__FILE__), 'thirdparty/net-ssh-2.0.11/lib'))
5
+ # And this one for when we're in the svn directory structure
6
+ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)), 'thirdparty/net-ssh-2.0.11/lib'))
7
+
8
+ $debug = true
9
+
10
+ require 'thread_pool'
11
+ begin
12
+ # Try loading net-ssh w/o gems first so that we don't introduce a
13
+ # dependency on gems if it is not needed.
14
+ require 'net/ssh'
15
+ rescue LoadError
16
+ require 'rubygems'
17
+ require 'net/ssh'
18
+ end
19
+ #require 'highline/import'
20
+
21
+ class Deployer
22
+ # def self.new
23
+ # begin
24
+ # require 'rubygems'
25
+ # require 'net/ssh'
26
+ # require 'highline/import'
27
+ # rescue LoadError
28
+ # raise LoadError, "In order to use the deployment feature, you must have rubygems installed. Additionally, you need to install the following gems: net-ssh, highline"
29
+ # else
30
+ # super
31
+ # end
32
+ # end
33
+
34
+ def initialize(options = nil)
35
+ @mutex = Mutex.new
36
+ @max_worker = 4
37
+ @abort_on_failure = false
38
+ @use_ssh_key = false
39
+ @user = Etc.getlogin
40
+ @password = nil
41
+ unless options.nil?
42
+ @user = options["deploy-as"] unless options["deploy-as"].nil?
43
+ @password = options["deploy-password"] unless options["deploy-password"].nil?
44
+ @max_worker = options["max-worker"]
45
+ @abort_on_failure = options["abort-on-failure"]
46
+ @use_ssh_key = options["use-ssh-key"]
47
+ end
48
+ end
49
+
50
+ def prompt
51
+ user = prompt_username
52
+ password = prompt_password
53
+ return user, password
54
+ end
55
+
56
+ def prompt_username
57
+ print "Username: "
58
+ user = $stdin.gets.chomp
59
+ return user
60
+ end
61
+
62
+ def prompt_password
63
+ password = ask("SSH Password (leave blank if using ssh key): ", true)
64
+ return password
65
+ end
66
+
67
+ def ask(str,mask=false)
68
+ begin
69
+ system 'stty -echo;' if mask
70
+ print str
71
+ input = STDIN.gets.chomp
72
+ ensure
73
+ system 'stty echo; echo ""'
74
+ end
75
+ return input
76
+ end
77
+
78
+ $sudo_pw = nil
79
+ def get_sudo_pw
80
+ @mutex.synchronize {
81
+ if $sudo_pw.nil?
82
+ $sudo_pw = ask("Sudo password: ", true)
83
+ else
84
+ return $sudo_pw
85
+ end
86
+ }
87
+ end
88
+
89
+ $passphrases = {}
90
+ def get_passphrase(package)
91
+ @mutex.synchronize {
92
+ if $passphrases[package].nil?
93
+ # $stdout.write package
94
+ # $stdout.flush
95
+ # $passphrases[package] = $stdin.gets.chomp
96
+ $passphrases[package] = ask(package, true)
97
+ else
98
+ return $passphrases[package]
99
+ end
100
+ }
101
+ end
102
+
103
+ def ssh_execute(server, username, password, cmd)
104
+ return lambda {
105
+ exit_status = 0
106
+ result = []
107
+
108
+ begin
109
+ Net::SSH.start(server, username, :password => password) do |ssh|
110
+ puts "Connecting to #{server}"
111
+ ch = ssh.open_channel do |channel|
112
+ # now we request a "pty" (i.e. interactive) session so we can send data
113
+ # back and forth if needed. it WILL NOT WORK without this, and it has to
114
+ # be done before any call to exec.
115
+
116
+ channel.request_pty do |ch, success|
117
+ raise "Could not obtain pty (i.e. an interactive ssh session)" if !success
118
+ end
119
+
120
+ channel.exec(cmd) do |ch, success|
121
+ puts "Executing #{cmd} on #{server}"
122
+ # 'success' isn't related to bash exit codes or anything, but more
123
+ # about ssh internals (i think... not bash related anyways).
124
+ # not sure why it would fail at such a basic level, but it seems smart
125
+ # to do something about it.
126
+ abort "could not execute command" unless success
127
+
128
+ # on_data is a hook that fires when the loop that this block is fired
129
+ # in (see below) returns data. This is what we've been doing all this
130
+ # for; now we can check to see if it's a password prompt, and
131
+ # interactively return data if so (see request_pty above).
132
+ channel.on_data do |ch, data|
133
+ if data =~ /Password/
134
+ #sudo_password = (!password.nil && password != "" && password) || get_sudo_pw
135
+ password = get_sudo_pw unless !password.nil? && password != ""
136
+ channel.send_data "#{password}\n"
137
+ elsif data =~ /Passphrase/ or data =~ /pass phrase/ or data =~ /incorrect passphrase/i
138
+ passphrase = get_passphrase(data)
139
+ channel.send_data "#{passphrase}\n"
140
+ else
141
+ # print "#{server}: #{data}" if $debug
142
+ # ssh channels can be treated as a hash for the specific purpose of
143
+ # getting values out of the block later
144
+ # channel[:result] ||= ""
145
+ # channel[:result] << data
146
+ result << data unless data.nil? or data.empty?
147
+ end
148
+ end
149
+
150
+ channel.on_extended_data do |ch, type, data|
151
+ print "SSH command returned on stderr: #{data}"
152
+ end
153
+
154
+ channel.on_request "exit-status" do |ch, data|
155
+ exit_status = data.read_long
156
+ end
157
+ end
158
+ end
159
+ ch.wait
160
+ ssh.loop
161
+ end
162
+ if $debug
163
+ puts "==================================================\nResult from #{server}:"
164
+ puts result.join
165
+ puts "=================================================="
166
+ end
167
+
168
+ rescue Net::SSH::AuthenticationFailed
169
+ exit_status = 1
170
+ puts "Bad username/password combination"
171
+ rescue Exception => e
172
+ exit_status = 1
173
+ puts e.inspect
174
+ puts e.backtrace
175
+ puts "Can't connect to server"
176
+ end
177
+
178
+ return exit_status
179
+ }
180
+ end
181
+
182
+ # deploy_params is an array that holds the list of paramters that is used when invoking tpkg on to the remote
183
+ # servers where we want to deploy to.
184
+ #
185
+ # servers is an array or a callback that list the remote servers where we want to deploy to
186
+ def deploy(deploy_params, servers)
187
+ params = deploy_params.join(" ")
188
+ cmd = "tpkg #{params} -n"
189
+ user = @user
190
+
191
+ if @user.nil? && !@use_ssh_key
192
+ @user = prompt_username
193
+ end
194
+
195
+ if @password.nil? && !@use_ssh_key
196
+ @password = prompt_password
197
+ end
198
+
199
+ tp = ThreadPool.new(@max_worker)
200
+ statuses = {}
201
+ deploy_to = []
202
+ if servers.kind_of?(Proc)
203
+ deploy_to = servers.call
204
+ else
205
+ deploy_to = servers
206
+ end
207
+
208
+ deploy_to.each do | server |
209
+ tp.process do
210
+ status = ssh_execute(server, @user, @password, cmd).call
211
+ statuses[server] = status
212
+ end
213
+ end
214
+ tp.shutdown
215
+ puts "Exit statuses: "
216
+ puts statuses.inspect
217
+
218
+ return statuses
219
+ end
220
+ end
@@ -0,0 +1,436 @@
1
+ require 'yaml'
2
+
3
+ module SymbolizeKeys
4
+
5
+ # converts any current string keys to symbol keys
6
+ def self.extended(hash)
7
+ hash.each do |key,value|
8
+ if key.is_a?(String)
9
+ hash.delete key
10
+ hash[key] = value #through overridden []=
11
+ end
12
+ if value.is_a?(Hash)
13
+ hash[key]=value.extend(SymbolizeKeys)
14
+ elsif value.is_a?(Array)
15
+ value.each do |val|
16
+ if val.is_a?(Hash)
17
+ val.extend(SymbolizeKeys)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ # assigns a new key/value pair
25
+ # converts they key to a symbol if it is a string
26
+ def []=(*args)
27
+ args[0] = args[0].to_sym if args[0].is_a?(String)
28
+ super
29
+ end
30
+
31
+ # returns new hash which is the merge of self and other hashes
32
+ # the returned hash will also be extended by SymbolizeKeys
33
+ def merge(*other_hashes , &resolution_proc )
34
+ merged = Hash.new.extend SymbolizeKeys
35
+ merged.merge! self , *other_hashes , &resolution_proc
36
+ end
37
+
38
+ # merges the other hashes into self
39
+ # if a proc is submitted , it's return will be the value for the key
40
+ def merge!( *other_hashes , &resolution_proc )
41
+
42
+ # default resolution: value of the other hash
43
+ resolution_proc ||= proc{ |key,oldval,newval| newval }
44
+
45
+ # merge each hash into self
46
+ other_hashes.each do |hash|
47
+ hash.each{ |k,v|
48
+ # assign new k/v into self, resolving conflicts with resolution_proc
49
+ self[k] = self.has_key?(k) ? resolution_proc[k,self[k],v] : v
50
+ }
51
+ end
52
+
53
+ self
54
+ end
55
+ end
56
+
57
+ class Metadata
58
+ attr_accessor :source
59
+ REQUIRED_FIELDS = [:name, :version, :maintainer]
60
+
61
+ # Cleans up a string to make it suitable for use in a filename
62
+ def self.clean_for_filename(dirtystring)
63
+ dirtystring.downcase.gsub(/[^\w]/, '')
64
+ end
65
+
66
+ def self.get_pkgs_metadata_from_yml_doc(yml_doc, metadata=nil, source=nil)
67
+ metadata = {} if metadata.nil?
68
+ metadata_lists = yml_doc.split("---")
69
+ metadata_lists.each do | metadata_text |
70
+ if metadata_text =~ /^:?name:(.+)/
71
+ name = $1.strip
72
+ metadata[name] = [] if !metadata[name]
73
+ metadata[name] << Metadata.new(metadata_text,'yml', source)
74
+ end
75
+ end
76
+ return metadata
77
+ end
78
+
79
+ # metadata_text = text representation of the metadata
80
+ # format = yml, xml, json, etc.
81
+ def initialize(metadata_text, format, source=nil)
82
+ @hash = nil
83
+ @metadata_text = metadata_text
84
+ @format = format
85
+ @source = source
86
+ end
87
+
88
+ def [](key)
89
+ return hash[key]
90
+ end
91
+
92
+ def []=(key,value)
93
+ hash[key]=value
94
+ end
95
+
96
+ def hash
97
+ if @hash
98
+ return @hash
99
+ end
100
+
101
+ if @format == 'yml'
102
+ hash = YAML::load(@metadata_text)
103
+ @hash = hash.extend(SymbolizeKeys)
104
+ else
105
+ @hash = metadata_xml_to_hash
106
+ end
107
+ return @hash
108
+ end
109
+
110
+ def write(file)
111
+ YAML::dump(hash, file)
112
+ end
113
+
114
+ def get_files_list
115
+ end
116
+
117
+ def generate_package_filename
118
+ name = hash[:name]
119
+ version = hash[:version]
120
+ packageversion = nil
121
+ if hash[:package_version] && !hash[:package_version].to_s.empty?
122
+ packageversion = hash[:package_version]
123
+ end
124
+ package_filename = "#{name}-#{version}"
125
+ if packageversion
126
+ package_filename << "-#{packageversion}"
127
+ end
128
+
129
+
130
+ if hash[:operatingsystem] and !hash[:operatingsystem].empty?
131
+ if hash[:operatingsystem].length == 1
132
+ package_filename << "-#{Metadata::clean_for_filename(hash[:operatingsystem].first)}"
133
+ else
134
+ operatingsystems = hash[:operatingsystem].dup
135
+ # Genericize any equivalent operating systems
136
+ # FIXME: more generic handling of equivalent OSs is probably called for
137
+ operatingsystems.each do |os|
138
+ os.sub!('CentOS', 'RedHat')
139
+ end
140
+ firstname = operatingsystems.first.split('-').first
141
+ firstversion = operatingsystems.first.split('-').last
142
+ if operatingsystems.all? { |os| os == operatingsystems.first }
143
+ # After genericizing all OSs are the same
144
+ package_filename << "-#{Metadata::clean_for_filename(operatingsystems.first)}"
145
+ elsif operatingsystems.all? { |os| os =~ /#{firstname}-/ }
146
+ # All of the OSs have the same name, just different versions. It
147
+ # may not be perfect, but name the package after the OS without a
148
+ # version. I.e. if the package specifies RedHat-4,RedHat-5 then
149
+ # name it "redhat". It might be confusing when it won't install on
150
+ # RedHat-3, but it seems better to me than naming it "multios".
151
+ package_filename << "-#{Metadata::clean_for_filename(firstname)}"
152
+ else
153
+ package_filename << "-multios"
154
+ end
155
+ end
156
+ end
157
+ if hash[:architecture] and !hash[:architecture].empty?
158
+ if hash[:architecture].length == 1
159
+ package_filename << "-#{Metadata::clean_for_filename(hash[:architecture].first)}"
160
+ else
161
+ package_filename << "-multiarch"
162
+ end
163
+ end
164
+
165
+ return package_filename
166
+ end
167
+
168
+ def verify_required_fields
169
+ REQUIRED_FIELDS.each do |reqfield|
170
+ if hash[reqfield].nil?
171
+ raise "Required field #{reqfield} not found"
172
+ elsif hash[reqfield].to_s.empty?
173
+ raise "Required field #{reqfield} is empty"
174
+ end
175
+ end
176
+ end
177
+
178
+ def metadata_xml_to_hash
179
+ # Don't do anything if metadata is from xml file
180
+ return if @format != "xml"
181
+
182
+ metadata_hash = {}
183
+ metadata_xml = REXML::Document.new(@metadata_text)
184
+ metadata_hash[:filename] = metadata_xml.root.attributes['filename']
185
+
186
+ REQUIRED_FIELDS.each do |reqfield|
187
+ if metadata_xml.elements["/tpkg/#{reqfield}"]
188
+ metadata_hash[reqfield] = metadata_xml.elements["/tpkg/#{reqfield}"].text
189
+ end
190
+ end
191
+
192
+ [:package_version, :description, :bugreporting].each do |optfield|
193
+ if metadata_xml.elements["/tpkg/#{optfield.to_s}"]
194
+ metadata_hash[optfield] =
195
+ metadata_xml.elements["/tpkg/#{optfield.to_s}"].text
196
+ end
197
+ end
198
+
199
+ [:operatingsystem, :architecture].each do |arrayfield|
200
+ array = []
201
+ # In the tpkg design docs I wrote that the user would specify
202
+ # multiple OSs or architectures by specifying the associated XML
203
+ # element more than once:
204
+ # <tpkg>
205
+ # <operatingsystem>RedHat-4</operatingsystem>
206
+ # <operatingsystem>CentOS-4</operatingsystem>
207
+ # </tpkg>
208
+ # However, I wrote the initial code and built my initial packages
209
+ # using comma separated values in a single instance of the
210
+ # element:
211
+ # <tpkg>
212
+ # <operatingsystem>RedHat-4,CentOS-4</operatingsystem>
213
+ # </tpkg>
214
+ # So we support both.
215
+ metadata_xml.elements.each("/tpkg/#{arrayfield.to_s}") do |af|
216
+ array.concat(af.text.split(/\s*,\s*/))
217
+ end
218
+ metadata_hash[arrayfield] = array unless array.empty?
219
+ end
220
+
221
+ deps = []
222
+ metadata_xml.elements.each('/tpkg/dependencies/dependency') do |depxml|
223
+ dep = {}
224
+ dep[:name] = depxml.elements['name'].text
225
+ [:allowed_versions, :minimum_version, :maximum_version,
226
+ :minimum_package_version, :maximum_package_version].each do |depfield|
227
+ if depxml.elements[depfield.to_s]
228
+ dep[depfield] = depxml.elements[depfield.to_s].text
229
+ end
230
+ end
231
+ if depxml.elements['native']
232
+ dep[:type] = :native
233
+ end
234
+ deps << dep
235
+ end
236
+ metadata_hash[:dependencies] = deps unless deps.empty?
237
+
238
+ conflicts = []
239
+ metadata_xml.elements.each('/tpkg/conflicts/conflict') do |conflictxml|
240
+ conflict = {}
241
+ conflict[:name] = conflictxml.elements['name'].text
242
+ [:minimum_version, :maximum_version,
243
+ :minimum_package_version, :maximum_package_version].each do |conflictfield|
244
+ if conflictxml.elements[conflictfield.to_s]
245
+ conflict[conflictfield] = conflictxml.elements[conflictfield.to_s].text
246
+ end
247
+ end
248
+ if conflictxml.elements['native']
249
+ conflict[:type] = :native
250
+ end
251
+ conflicts << conflict
252
+ end
253
+ metadata_hash[:conflicts] = conflicts unless conflicts.empty?
254
+
255
+ externals = []
256
+ metadata_xml.elements.each('/tpkg/externals/external') do |extxml|
257
+ external = {}
258
+ external[:name] = extxml.elements['name'].text
259
+ if extxml.elements['data']
260
+ external[:data] = extxml.elements['data'].text
261
+ elsif extxml.elements['datafile']
262
+ # We don't have access to the package contents here, so we just save
263
+ # the name of the file and leave it up to others to read the file
264
+ # when the package contents are available.
265
+ external[:datafile] = extxml.elements['datafile'].text
266
+ elsif extxml.elements['datascript']
267
+ # We don't have access to the package contents here, so we just save
268
+ # the name of the script and leave it up to others to run the script
269
+ # when the package contents are available.
270
+ external[:datascript] = extxml.elements['datascript'].text
271
+ end
272
+ externals << external
273
+ end
274
+ metadata_hash[:externals] = externals unless externals.empty?
275
+
276
+ metadata_hash[:files] = {}
277
+ file_defaults = {}
278
+ if metadata_xml.elements['/tpkg/files/file_defaults/posix']
279
+ posix = {}
280
+ if metadata_xml.elements['/tpkg/files/file_defaults/posix/owner']
281
+ owner =
282
+ metadata_xml.elements['/tpkg/files/file_defaults/posix/owner'].text
283
+ posix[:owner] = owner
284
+
285
+ end
286
+ gid = nil
287
+ if metadata_xml.elements['/tpkg/files/file_defaults/posix/group']
288
+ group =
289
+ metadata_xml.elements['/tpkg/files/file_defaults/posix/group'].text
290
+ posix[:group] = group
291
+ end
292
+ perms = nil
293
+ if metadata_xml.elements['/tpkg/files/file_defaults/posix/perms']
294
+ perms =
295
+ metadata_xml.elements['/tpkg/files/file_defaults/posix/perms'].text
296
+ posix[:perms] = perms.oct
297
+ end
298
+ file_defaults[:posix] = posix
299
+ end
300
+ metadata_hash[:files][:file_defaults] = file_defaults unless file_defaults.empty?
301
+
302
+ dir_defaults = {}
303
+ if metadata_xml.elements['/tpkg/files/dir_defaults/posix']
304
+ posix = {}
305
+ if metadata_xml.elements['/tpkg/files/dir_defaults/posix/owner']
306
+ owner =
307
+ metadata_xml.elements['/tpkg/files/dir_defaults/posix/owner'].text
308
+ posix[:owner] = owner
309
+ end
310
+ gid = nil
311
+ if metadata_xml.elements['/tpkg/files/dir_defaults/posix/group']
312
+ group =
313
+ metadata_xml.elements['/tpkg/files/dir_defaults/posix/group'].text
314
+ posix[:group] = group
315
+ end
316
+ perms = nil
317
+ if metadata_xml.elements['/tpkg/files/dir_defaults/posix/perms']
318
+ perms =
319
+ metadata_xml.elements['/tpkg/files/dir_defaults/posix/perms'].text
320
+ posix[:perms] = perms.oct
321
+ end
322
+ dir_defaults[:posix] = posix
323
+ end
324
+ metadata_hash[:files][:dir_defaults] = dir_defaults unless dir_defaults.empty?
325
+
326
+ files = []
327
+ metadata_xml.elements.each('/tpkg/files/file') do |filexml|
328
+ file = {}
329
+ file[:path] = filexml.elements['path'].text
330
+ if filexml.elements['encrypt']
331
+ encrypt = true
332
+ if filexml.elements['encrypt'].attribute('precrypt') &&
333
+ filexml.elements['encrypt'].attribute('precrypt').value == 'true'
334
+ encrypt = "precrypt"
335
+ end
336
+ file[:encrypt] = encrypt
337
+ end
338
+ if filexml.elements['init']
339
+ init = {}
340
+ if filexml.elements['init/start']
341
+ init[:start] = filexml.elements['init/start'].text
342
+ end
343
+ if filexml.elements['init/levels']
344
+ if filexml.elements['init/levels'].text
345
+ # Split '234' into ['2','3','4'], for example
346
+ init[:levels] = filexml.elements['init/levels'].text.split(//)
347
+ else
348
+ # If the element is empty in the XML (<levels/> or
349
+ # <levels></levels>) then we get nil back from the .text
350
+ # call, interpret that as no levels
351
+ init[:levels] = []
352
+ end
353
+ end
354
+ file[:init] = init
355
+ end
356
+ if filexml.elements['crontab']
357
+ crontab = {}
358
+ if filexml.elements['crontab/user']
359
+ crontab[:user] = filexml.elements['crontab/user'].text
360
+ end
361
+ file[:crontab] = crontab
362
+ end
363
+ if filexml.elements['posix']
364
+ posix = {}
365
+ if filexml.elements['posix/owner']
366
+ owner = filexml.elements['posix/owner'].text
367
+ posix[:owner] = owner
368
+ end
369
+ gid = nil
370
+ if filexml.elements['posix/group']
371
+ group = filexml.elements['posix/group'].text
372
+ posix[:group] = group
373
+ end
374
+ perms = nil
375
+ if filexml.elements['posix/perms']
376
+ perms = filexml.elements['posix/perms'].text
377
+ posix[:perms] = perms.oct
378
+ end
379
+ file[:posix] = posix
380
+ end
381
+ files << file
382
+ end
383
+ metadata_hash[:files][:files] = files unless files.empty?
384
+
385
+ return metadata_hash
386
+ end
387
+ end
388
+
389
+ class FileMetadata < Metadata
390
+ def hash
391
+ if @hash
392
+ return @hash
393
+ end
394
+
395
+ if @format == 'bin'
396
+ @hash = Marshal::load(@metadata_text)
397
+ @hash = hash.extend(SymbolizeKeys)
398
+ elsif @format == 'yml'
399
+ hash = YAML::load(@metadata_text)
400
+ @hash = hash.extend(SymbolizeKeys)
401
+ elsif @format == 'xml'
402
+ @hash = file_metadata_xml_to_hash
403
+ end
404
+ return @hash
405
+ end
406
+
407
+ def file_metadata_xml_to_hash
408
+ return if @format != "xml"
409
+
410
+ file_metadata_hash = {}
411
+ files = []
412
+ file_metadata_xml = REXML::Document.new(@metadata_text)
413
+ file_metadata_hash[:package_file] = file_metadata_xml.root.attributes['package_file']
414
+ file_metadata_xml.elements.each("files/file") do | file_ele |
415
+ file = {}
416
+ file[:path] = file_ele.elements['path'].text
417
+ file[:relocatable] = file_ele.attributes["relocatable"] == "true"
418
+
419
+ if file_ele.elements["checksum"]
420
+ digests = []
421
+ file_ele.elements.each("checksum/digest") do | digest_ele |
422
+ digest = {}
423
+ digest['value'] = digest_ele.text
424
+ digest['encrypted'] = digest_ele.attributes['encrypted'] && digest_ele.attributes['encrypted'] == "true"
425
+ digest['decrypted'] = digest_ele.attributes['decrypted'] && digest_ele.attributes['decrypted'] == "true"
426
+ digests << digest
427
+ end
428
+ checksum = {:digests => digests, :algorithm => file_ele.elements["checksum"].elements["algorithm"]}
429
+ end
430
+ file[:checksum] = checksum
431
+ files << file
432
+ end
433
+ file_metadata_hash[:files] = files
434
+ return file_metadata_hash
435
+ end
436
+ end