tpkg 1.16.2

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