vcseif 1.2.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/lib/vcseif.rb +23 -0
- data/lib/vcseif/connection.rb +285 -0
- data/lib/vcseif/rally_vcs_connection.rb +925 -0
- data/lib/vcseif/utils/auxloader.rb +255 -0
- data/lib/vcseif/utils/exceptions.rb +101 -0
- data/lib/vcseif/utils/fuzzer.rb +71 -0
- data/lib/vcseif/utils/konfigger.rb +421 -0
- data/lib/vcseif/utils/lock_file.rb +90 -0
- data/lib/vcseif/utils/proctbl.rb +146 -0
- data/lib/vcseif/utils/rally_logger.rb +223 -0
- data/lib/vcseif/utils/time_file.rb +80 -0
- data/lib/vcseif/vcs_connector.rb +487 -0
- data/lib/vcseif/vcs_connector_driver.rb +227 -0
- data/lib/vcseif/vcs_connector_runner.rb +283 -0
- data/lib/version.rb +18 -0
- metadata +173 -0
@@ -0,0 +1,255 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
#################################################################################################
|
4
|
+
#
|
5
|
+
# auxload - Auxiliary functionality loader
|
6
|
+
# Contains classes to load auxiliary (plugin and extension) classes
|
7
|
+
# or the *_connection classes.
|
8
|
+
# A specification for functionality looks like:
|
9
|
+
# class_name.method_name
|
10
|
+
# or
|
11
|
+
# facility_name.class_name.method_name
|
12
|
+
#
|
13
|
+
#################################################################################################
|
14
|
+
|
15
|
+
class AuxLoader
|
16
|
+
|
17
|
+
@@facility_class_name = ""
|
18
|
+
@@facility_class = nil
|
19
|
+
@@facility = {} # key by @@facility_class_name, value is @@facility_class object
|
20
|
+
|
21
|
+
def self.getFacility(facility_type, facility_class_name, facility_name=nil)
|
22
|
+
"""
|
23
|
+
Given a facility_class_name, look into @@facility for a key match.
|
24
|
+
If found return the corresponding value, which must be the
|
25
|
+
class object for the facility_class.
|
26
|
+
"""
|
27
|
+
if not @@facility.include?(facility_class_name)
|
28
|
+
loadFacility(facility_type, facility_class_name, facility_name)
|
29
|
+
end
|
30
|
+
return @@facility[facility_class_name] || nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.is_qualified?(target_dir, fn)
|
34
|
+
qualified = fn.end_with?('.rb') and File.file?('%s/%s' % [target_dir, fn])
|
35
|
+
return qualified
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.loadFacility(subdir, class_name, facility=nil, method_existence=nil)
|
39
|
+
%{
|
40
|
+
Work the Ruby machinery to require in a Ruby file such that
|
41
|
+
a subsequent Kernel.const_get(class_name) works successfully.
|
42
|
+
The subdir parm is relative to ENV['VCS_APP_ROOT'].
|
43
|
+
The optional facility parm can "name" a specific file the class
|
44
|
+
must exist in.
|
45
|
+
Caller can also specify a string or an array of strings with method
|
46
|
+
names that must exist in the loaded class.
|
47
|
+
}
|
48
|
+
fq_facility_dir = "#{ENV['VCS_APP_ROOT']}/#{subdir}" # fully qualified path to subdir
|
49
|
+
##
|
50
|
+
## puts " loadFacility fq_facility_dir value: |%s|" % fq_facility_dir
|
51
|
+
## $stdout.flush()
|
52
|
+
##
|
53
|
+
|
54
|
+
if not File.exists?(fq_facility_dir)
|
55
|
+
raise StandardError, 'target subdir: %s not found' % [fq_facility_dir]
|
56
|
+
end
|
57
|
+
if not File.directory?(fq_facility_dir)
|
58
|
+
raise StandardError, 'target path %s is not a directory' % [fq_facility_dir]
|
59
|
+
end
|
60
|
+
|
61
|
+
if facility.nil?
|
62
|
+
target_dir = fq_facility_dir
|
63
|
+
rbfiles = Dir.entries(target_dir).select {|fn| self.is_qualified?(target_dir, fn)}
|
64
|
+
facility_names = rbfiles.collect {|fn| fn[0...-3]} # squeeze off the '.rb' portion of the name
|
65
|
+
else
|
66
|
+
facility_path = '%s/%s.rb' % [fq_facility_dir, facility]
|
67
|
+
if not File.file?(facility_path)
|
68
|
+
raise StandardError, 'No such %s module: %s found' % [subdir, facility_path]
|
69
|
+
end
|
70
|
+
facility_names = [facility]
|
71
|
+
end
|
72
|
+
##
|
73
|
+
## puts " facility_names: #{facility_names.inspect}"
|
74
|
+
## $stdout.flush()
|
75
|
+
##
|
76
|
+
|
77
|
+
target_classes_found = 0
|
78
|
+
tc_facility = {} # target class facility lookup dict
|
79
|
+
# look in all facilities, record the facility name if the facility_class_name is defined in there
|
80
|
+
facility_names.each do |facility_name|
|
81
|
+
facility_file = '%s/%s.rb' % [fq_facility_dir, facility_name]
|
82
|
+
File.open(facility_file) do |ff|
|
83
|
+
target_class_defs = ff.readlines.select {|line| line =~ /^class #{class_name}\s*/}
|
84
|
+
if target_class_defs.length > 0
|
85
|
+
tc_facility[class_name] = "%s/%s" % [subdir, facility_name]
|
86
|
+
target_classes_found +=1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if target_classes_found < 1
|
92
|
+
problem = "No class: '%s' defined in any file located in the '%s' directory"
|
93
|
+
raise StandardError, problem % [class_name, fq_facility_dir]
|
94
|
+
end
|
95
|
+
if target_classes_found > 1
|
96
|
+
problem = "%s class: '%s' defined in more than one file located in the '%s' directory"
|
97
|
+
raise StandardError, problem % [subdir, class_name, fq_facility_dir]
|
98
|
+
end
|
99
|
+
|
100
|
+
begin
|
101
|
+
require "#{tc_facility[class_name]}"
|
102
|
+
rescue Exception => ex
|
103
|
+
$stderr.write("Auxloader detected exception on require of #{tc_facility[class_name]}:\n")
|
104
|
+
$stderr.write(" #{ex.message}\n")
|
105
|
+
$stderr.flush()
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
|
109
|
+
begin
|
110
|
+
@@facility_class = Kernel.const_get(class_name)
|
111
|
+
rescue Exception => ex
|
112
|
+
$stderr.write("Caught exception loading #{class_name}: #{ex.message}\n")
|
113
|
+
raise
|
114
|
+
end
|
115
|
+
|
116
|
+
if method_existence != nil
|
117
|
+
if method_existence.class.name == 'String'
|
118
|
+
method_existence = method_existence.split(',').collect {|mn| mn.strip()}
|
119
|
+
elsif method_existence.class.name != 'Array'
|
120
|
+
raise Exception, "method_existence parm must be a String or an Array of Strings"
|
121
|
+
end
|
122
|
+
for method_name in method_existence do
|
123
|
+
method = @@facility_class.instance_methods.select {|m| m == method_name.to_sym}
|
124
|
+
if method == nil or method == false
|
125
|
+
puts "Unable to validate that the %s class has a %s method" % [class_name, method_name]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
@@facility[class_name] = @@facility_class
|
131
|
+
return @@facility[class_name]
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
#################################################################################################
|
137
|
+
|
138
|
+
class ClassLoader < AuxLoader
|
139
|
+
|
140
|
+
def self.loadConnectionClass(subdir, class_name, facility_name=nil)
|
141
|
+
%{
|
142
|
+
A generalized means of loading a class.
|
143
|
+
The priority ladder is to first attempt to find and load
|
144
|
+
the class_name from the specific subdir named in any facility_name supplied,
|
145
|
+
In the event that fails, the next attempt to get the class loaded in
|
146
|
+
looks in the '#{ENV['VCS_APP_ROOT']}/lib/vcseif' subdirectory.
|
147
|
+
If that also fails, an attempt is made to load the class via
|
148
|
+
Kernel.const_get in the event that the class is in an already loaded gem.
|
149
|
+
If a facility_name is given, it restricts the location of the class
|
150
|
+
to a specific Ruby file (named by facility_name, which would be the part of
|
151
|
+
the filename without the ".rb" suffix).
|
152
|
+
}
|
153
|
+
##
|
154
|
+
## $stdout.write("call of ClassLoader.loadConnectionClass(#{subdir}, #{class_name}, #{facility_name})\n")
|
155
|
+
## $stdout.flush()
|
156
|
+
##
|
157
|
+
candidates = [[1, subdir], [2, 'lib/vcseif']]
|
158
|
+
for rung, candidate in candidates do
|
159
|
+
fq_facility_dir = "#{ENV['VCS_APP_ROOT']}/#{candidate}" # fully qualified path to candidate
|
160
|
+
if not File.exists?(fq_facility_dir) or not File.directory?(fq_facility_dir)
|
161
|
+
##
|
162
|
+
## $stdout.write(" rung ##{rung} of loadConnectionClass skipped\n")
|
163
|
+
##
|
164
|
+
next
|
165
|
+
end
|
166
|
+
|
167
|
+
success = false
|
168
|
+
begin
|
169
|
+
conn_class = loadFacility(candidate, class_name, facility_name)
|
170
|
+
##
|
171
|
+
## $stdout.write(" rung ##{rung} of loadConnectionClass succeeded\n")
|
172
|
+
## $stdout.flush()
|
173
|
+
##
|
174
|
+
success = true
|
175
|
+
rescue Exception => ex
|
176
|
+
##
|
177
|
+
## $stdout.write(" rung ##{rung} of loadConnectionClass failed...\n")
|
178
|
+
## $stdout.flush()
|
179
|
+
##
|
180
|
+
raise if ex.message.include?("defined in more than one file")
|
181
|
+
end
|
182
|
+
return conn_class if success
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# try a Kernel.const_get(class_name) to see
|
188
|
+
# if we can obtain the class from an already loaded relevant *eif gem
|
189
|
+
#
|
190
|
+
begin
|
191
|
+
@@facility_class = Kernel.const_get(class_name)
|
192
|
+
success = true
|
193
|
+
rescue Exception => ex
|
194
|
+
$stderr.write("Caught exception attempting to load #{class_name}: #{ex.message}\n")
|
195
|
+
raise
|
196
|
+
end
|
197
|
+
return @@facility_class if success
|
198
|
+
|
199
|
+
return nil
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
#################################################################################################
|
205
|
+
|
206
|
+
class PluginLoader < AuxLoader
|
207
|
+
|
208
|
+
def self.loadFacility(facility_type, facility_class_name, facility_name)
|
209
|
+
super('plugin', facility_class_name, facility_name, 'service')
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.getPlugin(plugin_class_name)
|
213
|
+
"""
|
214
|
+
Given a plugin_class_name as a String, return the corresponding class
|
215
|
+
if such a class exists in the conventional location.
|
216
|
+
"""
|
217
|
+
return getFacility('plugin', plugin_class_name)
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
##########################################################################################################
|
223
|
+
|
224
|
+
class ExtensionLoader < AuxLoader
|
225
|
+
|
226
|
+
def self.loadFacility(facility_type, facility_class_name, facility_name)
|
227
|
+
super('extension', facility_class_name, facility_name, 'service')
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.getExtension(extension_spec)
|
231
|
+
"""
|
232
|
+
Given an extension_spec as a String (in either 'ClassName or facility.ClassName'
|
233
|
+
format), return the corresponding class if such a class exists in the
|
234
|
+
extension subdirectory.
|
235
|
+
"""
|
236
|
+
##
|
237
|
+
## puts "ExtensionLoader.getExtension extension_spec: |%s|" % extension_spec
|
238
|
+
##
|
239
|
+
facility_name = nil
|
240
|
+
#split the extension_spec on '.'
|
241
|
+
if not extension_spec.include?('.')
|
242
|
+
extension_class_name = extension_spec
|
243
|
+
elsif extension_spec.count('.') == 1
|
244
|
+
facility_name, extension_class_name = extension_spec.split('.')
|
245
|
+
# where facility_name is the basename of a Ruby file containing the extension_class_name
|
246
|
+
else
|
247
|
+
raise StandardError, 'Invalid extension specification: %s' % extension_spec
|
248
|
+
end
|
249
|
+
|
250
|
+
return getFacility('extension', extension_class_name, facility_name)
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
module VCSEIF_Exceptions
|
4
|
+
|
5
|
+
######################################################################################
|
6
|
+
|
7
|
+
$_blurt = false
|
8
|
+
$_logger = nil
|
9
|
+
|
10
|
+
######################################################################################
|
11
|
+
|
12
|
+
def self.logAllExceptions(truthiness, logger=nil)
|
13
|
+
"""
|
14
|
+
Call this method when you want the convenience of raising the
|
15
|
+
Exception subclasses defined in this file module without having to supply
|
16
|
+
an instance of a logger for each raise statement.
|
17
|
+
Simply supply the logger to be used in this call and you're set
|
18
|
+
for the duration!
|
19
|
+
"""
|
20
|
+
if truthiness and !logger.nil?
|
21
|
+
$_blurt = true
|
22
|
+
$_logger = logger
|
23
|
+
else
|
24
|
+
$_blurt = false
|
25
|
+
$_logger = nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
######################################################################################
|
30
|
+
|
31
|
+
class VCSEIF_Exception < StandardError
|
32
|
+
|
33
|
+
@@log_level = 'info'
|
34
|
+
|
35
|
+
attr_accessor :verbiage
|
36
|
+
|
37
|
+
def initialize(msg, logger=nil)
|
38
|
+
mod_name, class_name = self.class.name.split("::")
|
39
|
+
if !logger.nil?
|
40
|
+
logger.send(@@log_level, msg, self)
|
41
|
+
elsif ($_blurt and !$_logger.nil?)
|
42
|
+
$_logger.send(@@log_level, msg, self)
|
43
|
+
#else
|
44
|
+
## prepend the specific exception class name to the msg text
|
45
|
+
# $stderr.write("%s: %s\n" % [class_name, msg])
|
46
|
+
end
|
47
|
+
@verbiage = msg
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class RecoverableException < VCSEIF_Exception
|
52
|
+
"""
|
53
|
+
This brand of VCSEIF_Exception is intended be used when a single atomic operation
|
54
|
+
has failed but further processing can be attempted.
|
55
|
+
"""
|
56
|
+
@@log_level = 'warn'
|
57
|
+
end
|
58
|
+
|
59
|
+
class UnrecoverableException < VCSEIF_Exception
|
60
|
+
"""
|
61
|
+
This brand of VCSEIF_Exception is intended be used when an operation has failed and
|
62
|
+
no further processing can or should be attempted.
|
63
|
+
The condition that caused this operation failure is either caused by a
|
64
|
+
bad configuration or something else that won't be different with the passage
|
65
|
+
of time.
|
66
|
+
"""
|
67
|
+
@@log_level = 'error'
|
68
|
+
end
|
69
|
+
|
70
|
+
class ConfigurationError < VCSEIF_Exception
|
71
|
+
"""
|
72
|
+
This brand of VCSEIF_Exception is intended to be used when a configuration error
|
73
|
+
has been detected. Because of the configuration error no further processing
|
74
|
+
can or should proceed. The configuration will have to be fixed or this
|
75
|
+
exception would be raised again in a future invocation.
|
76
|
+
"""
|
77
|
+
@@log_level = 'fatal'
|
78
|
+
end
|
79
|
+
|
80
|
+
class NonFatalConfigurationError < VCSEIF_Exception
|
81
|
+
"""
|
82
|
+
This brand of VCSEIF_Exception is intended to be used when a configuration error
|
83
|
+
has been detected. This exception differs from the ConfigurationError
|
84
|
+
in that the configuration error detected will not prevent reasonable operation
|
85
|
+
from proceeding but should be noted for future correction.
|
86
|
+
"""
|
87
|
+
@@log_level = 'error'
|
88
|
+
end
|
89
|
+
|
90
|
+
class OperationalError < VCSEIF_Exception
|
91
|
+
"""
|
92
|
+
This brand of VCSEIF_Exception is intendedto be used when an operation has failed and
|
93
|
+
no further processing can or should be attempted.
|
94
|
+
The condition that caused this though may not be in place if the operation were
|
95
|
+
attempted again at a later time.
|
96
|
+
"""
|
97
|
+
@@log_level = 'error'
|
98
|
+
end
|
99
|
+
|
100
|
+
end # of the VCSEIF_Exceptions Module
|
101
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
# This really basic algorithm encodes but does not encrypt strings
|
6
|
+
|
7
|
+
#Thanks to Erika for the idea about adding some text at the beginning of the encoded password
|
8
|
+
# This clues people in that the password is encoded and also makes sure that the encoding decoding will never fail.
|
9
|
+
# Without the extra text, "It will fail if a password has the same number of SEPARATORs as characters"
|
10
|
+
|
11
|
+
class Fuzzer
|
12
|
+
SEPARATOR = "-"
|
13
|
+
ENCODED = "encoded" + SEPARATOR
|
14
|
+
|
15
|
+
def self.encode(string)
|
16
|
+
return string if string.class.name != 'String'
|
17
|
+
return string if string.empty?
|
18
|
+
encoded = ""
|
19
|
+
Base64.encode64(string).each_byte { |b|
|
20
|
+
if ( b != 10 ) # \n
|
21
|
+
encoded = encoded + b.chr + SEPARATOR
|
22
|
+
end
|
23
|
+
}
|
24
|
+
return ENCODED + encoded
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.fuzz(string)
|
28
|
+
# would have been nice to be able to do alias_method :fuzz, :encode
|
29
|
+
return self.encode(string)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def self.decode(string)
|
34
|
+
return string if string.class.name != 'String'
|
35
|
+
return string if string.empty?
|
36
|
+
if self.is_encoded?(string)
|
37
|
+
decoded = self.remove_every_second_char(string.slice(ENCODED.length,string.length))
|
38
|
+
return Base64.decode64(decoded)
|
39
|
+
else
|
40
|
+
return string
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.defuzz(string)
|
45
|
+
#would have been nice to be able to do alias_method :defuzz, :decode
|
46
|
+
return self.decode(string)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def self.is_encoded?(string)
|
51
|
+
%{
|
52
|
+
Does the string arg start with "encoded-" and are there an
|
53
|
+
equal number of separator strings and regular characters?
|
54
|
+
}
|
55
|
+
return false if string.class.name != 'String'
|
56
|
+
sliced = string.slice(ENCODED.length, string.length)
|
57
|
+
return string.slice(0,ENCODED.length) == ENCODED && sliced.length == sliced.count(SEPARATOR)*2
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def self.remove_every_second_char(string)
|
62
|
+
scrunched = ""
|
63
|
+
for i in 0..string.length-1
|
64
|
+
if i%2 == 0
|
65
|
+
scrunched = scrunched + string.slice(i, 1)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
return scrunched
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,421 @@
|
|
1
|
+
# Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
|
2
|
+
|
3
|
+
__doc__ = %{
|
4
|
+
The Konfabulator class defined in this file handles deconstructing a Hash based
|
5
|
+
configuration to enable easy access to the semantics of the configuration specification.
|
6
|
+
In addition, the Konfabulator encodes any clear text passwords in the underlying file
|
7
|
+
from which the Hash was populated.
|
8
|
+
}
|
9
|
+
|
10
|
+
#################################################################################################
|
11
|
+
|
12
|
+
require 'fileutils' # for copy, move, remove_file
|
13
|
+
|
14
|
+
require_relative "exceptions" # for ConfigurationError, NonFatalConfigurationError
|
15
|
+
require_relative "fuzzer" # for Fuzzer
|
16
|
+
|
17
|
+
#################################################################################################
|
18
|
+
|
19
|
+
SUPPORTED_VCS_TARGETS = ['Mercurial', 'Git', 'Subversion', 'Perforce', 'TFS', 'Bazaar']
|
20
|
+
|
21
|
+
#################################################################################################
|
22
|
+
|
23
|
+
class Konfabulator
|
24
|
+
%{
|
25
|
+
An instance of this class provides a means to slurp configuration information
|
26
|
+
from a Ruby Hash structure and arrange it within the instance for easy consumption
|
27
|
+
by holders of a Konfabulator instance.
|
28
|
+
An instance also offers the capability to "fuzz" clear-text passwords in a
|
29
|
+
config file into an encoded (but not encrypted) form and to be able to handle
|
30
|
+
defuzzing those encoded passwords for holders of the instance when they access
|
31
|
+
those values in the instance.
|
32
|
+
}
|
33
|
+
attr_accessor :top_level_sequence
|
34
|
+
attr_reader :config, :config_name, :log, :vcs_header, :content
|
35
|
+
|
36
|
+
def initialize(config_spec, logger)
|
37
|
+
"""
|
38
|
+
config_spec is a Hash
|
39
|
+
"""
|
40
|
+
|
41
|
+
@config = config_spec
|
42
|
+
@config_name = config_spec['ConfigName']
|
43
|
+
@log = logger
|
44
|
+
@top_level_sequence = []
|
45
|
+
|
46
|
+
content = []
|
47
|
+
begin
|
48
|
+
cf = File.open(config_spec['ConfigPath'], "r")
|
49
|
+
@content = cf.readlines()
|
50
|
+
cf.close()
|
51
|
+
rescue IOError => ex
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
|
55
|
+
conf_lines = @content.select { |line| line.class.name == "String" and line !~ /^\s*#/ }
|
56
|
+
section_headers = conf_lines.select { |line| line =~ /^[A-Z][A-Za-z_]+.+\s*:/}
|
57
|
+
if section_headers.length < 2
|
58
|
+
problem = 'Insufficient content in config file: %s' % @config['ConfigPath']
|
59
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
60
|
+
raise confex, problem
|
61
|
+
end
|
62
|
+
if section_headers.length > 5
|
63
|
+
admonition = 'Excess content in config file: %s' % @config['ConfigPath']
|
64
|
+
confex = VCSEIF_Exceptions::NonFatalConfigurationError.new(admonition)
|
65
|
+
raise confex, admonition
|
66
|
+
end
|
67
|
+
section_headers = section_headers.collect {|sh| sh.strip().sub(/\s*:.*$/, '')}
|
68
|
+
|
69
|
+
connector_ident_header = section_headers.shift()
|
70
|
+
if connector_ident_header !~ /Connector/
|
71
|
+
problem = 'First section in config file must identify the Connector type'
|
72
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
73
|
+
raise confex, problem
|
74
|
+
end
|
75
|
+
|
76
|
+
rally_header = section_headers.shift()
|
77
|
+
if not rally_header.start_with?('Rally')
|
78
|
+
problem = 'Second section in config file must be Rally section'
|
79
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
80
|
+
raise confex, problem
|
81
|
+
end
|
82
|
+
@top_level_sequence << 'Rally'
|
83
|
+
|
84
|
+
@vcs_header = section_headers.shift
|
85
|
+
|
86
|
+
known = SUPPORTED_VCS_TARGETS.include?(@vcs_header)
|
87
|
+
if known.nil? or not known
|
88
|
+
problem = 'Third section in config file must identify a known VCS identifier'
|
89
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
90
|
+
raise confex, problem
|
91
|
+
end
|
92
|
+
@top_level_sequence << @vcs_header
|
93
|
+
|
94
|
+
# set up defaults for the Services and Transforms sections if those sections aren't in the config file
|
95
|
+
if not @config.has_key?('Services')
|
96
|
+
@config['Services'] = {}
|
97
|
+
@config['Services']['LogLevel'] = 'Info'
|
98
|
+
@config['Services']['Preview'] = false
|
99
|
+
@config['Services']['PostBatchExtension'] = nil
|
100
|
+
@log.info('no Services section specified in the config, using default settings')
|
101
|
+
@top_level_sequence << 'Services'
|
102
|
+
end
|
103
|
+
|
104
|
+
if not @config.has_key?('Transforms')
|
105
|
+
@config['Transforms'] = {}
|
106
|
+
@log.info('no Transforms section specified in the config')
|
107
|
+
@top_level_sequence << 'Transforms'
|
108
|
+
end
|
109
|
+
|
110
|
+
while section_headers.length > 0 do
|
111
|
+
header = section_headers.shift
|
112
|
+
begin
|
113
|
+
if not ['Services', 'Transforms'].include?(header)
|
114
|
+
problem = 'Unrecognized section header "%s" found in config' % header
|
115
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
116
|
+
raise confex, problem
|
117
|
+
else
|
118
|
+
@top_level_sequence << header
|
119
|
+
end
|
120
|
+
rescue VCSEIF_Exceptions::ConfigurationError => ex
|
121
|
+
# noop
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
checkRallySectionSanity
|
126
|
+
checkTargetVCSSectionSanity
|
127
|
+
|
128
|
+
# defuzz the passwords for our internal use if they are fuzzed,
|
129
|
+
# and if they are not, fuzz them in the file
|
130
|
+
defuzzPasswords()
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
private
|
135
|
+
def checkRallySectionSanity()
|
136
|
+
""" do some rudimentary checking for Rally section subitems of Server,
|
137
|
+
Protocol, Username, Password, Workspace, RepositoryName
|
138
|
+
"""
|
139
|
+
rally = @config['Rally']
|
140
|
+
|
141
|
+
server_spec = rally['Server'] || nil
|
142
|
+
if server_spec.nil? or server_spec.empty?
|
143
|
+
problem = 'No Server specification item value detected in the Rally section of the config'
|
144
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
145
|
+
raise confex, problem
|
146
|
+
end
|
147
|
+
|
148
|
+
proto_spec = rally['Protocol'] || nil
|
149
|
+
if proto_spec.nil? or proto_spec.empty?
|
150
|
+
proto_spec = 'https'
|
151
|
+
rally['Protocol'] = proto_spec
|
152
|
+
end
|
153
|
+
if not ['http', 'https'].include?(proto_spec)
|
154
|
+
problem = "Invalid Protocol specification item value '%s' detected in the Rally section of the config" % proto_spec
|
155
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
156
|
+
raise confex, problem
|
157
|
+
end
|
158
|
+
|
159
|
+
user_name = rally['Username'] || nil
|
160
|
+
if user_name.nil? or user_name.empty?
|
161
|
+
problem = 'No Username value detected in the Rally section of the config'
|
162
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
163
|
+
raise confex, problem
|
164
|
+
end
|
165
|
+
|
166
|
+
password = rally['Password'] || nil
|
167
|
+
if password.nil? or password.empty?
|
168
|
+
problem = 'No Password value detected in the Rally section of the config'
|
169
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
170
|
+
raise confex, problem
|
171
|
+
end
|
172
|
+
|
173
|
+
workspace = rally['Workspace'] || nil
|
174
|
+
if workspace.nil? or workspace.empty?
|
175
|
+
problem = 'No Workspace value detected in the Rally section of the config'
|
176
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
177
|
+
raise confex, problem
|
178
|
+
end
|
179
|
+
|
180
|
+
repo_name = rally['RepositoryName'] || nil
|
181
|
+
if repo_name.nil? or repo_name.empty?
|
182
|
+
problem = 'No RepositoryName value detected in the Rally section of the config'
|
183
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
184
|
+
raise confex, problem
|
185
|
+
end
|
186
|
+
|
187
|
+
lookback = rally['Lookback'] || nil
|
188
|
+
if lookback.nil? #or lookback.class.name != "Fixnum"
|
189
|
+
rally['Lookback'] = 90 # default to 90 minutes if not specified
|
190
|
+
end
|
191
|
+
|
192
|
+
upd_art_state = rally['UpdateArtifactState'] || nil
|
193
|
+
if upd_art_state.nil?
|
194
|
+
rally['UpdateArtifactState'] = false
|
195
|
+
end
|
196
|
+
|
197
|
+
if rally['UpdateArtifactState']
|
198
|
+
statex_class_name = rally['StateExtractorClass'] || nil
|
199
|
+
if statex_class_name.nil?
|
200
|
+
rally['StateExtractorClass'] = 'BasicActionsAndArtifactsExtractor'
|
201
|
+
@log.warn('Defaulting StateExtractorClass to %s, as UpdateArtifactState was set to true but no value was provided for the StateExtractorClass' % rally['StateExtractorClass'])
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
def checkTargetVCSSectionSanity()
|
208
|
+
"""
|
209
|
+
Need to have RepositoryBase for non TFS VCS systems (Git, Hg, Subversion)
|
210
|
+
Can have Server, Username, Password
|
211
|
+
Uri, Lookback, MaxItems, Debug
|
212
|
+
"""
|
213
|
+
non_vcs_sections = ['Rally', 'Transforms', 'Services']
|
214
|
+
sections = topLevels().select {|name| non_vcs_sections.include?(name) == false}
|
215
|
+
vcs_section_name = sections.first
|
216
|
+
vcs = @config[vcs_section_name]
|
217
|
+
|
218
|
+
if not vcs_section_name.start_with?('TFS')
|
219
|
+
repo_base = vcs['RepositoryBase'] || nil
|
220
|
+
if repo_base.nil?
|
221
|
+
problem = 'No RepositoryBase value detected in the %s section of the config' % vcs_section_name
|
222
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
223
|
+
raise confex, problem
|
224
|
+
end
|
225
|
+
else
|
226
|
+
collection_name = vcs['CollectionName'] || nil
|
227
|
+
if collection_name.nil? || collection_name.empty?
|
228
|
+
problem = 'No CollectionName value detected in the %s section of the config' % vcs_section_name
|
229
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
230
|
+
raise confex, problem
|
231
|
+
end
|
232
|
+
team_project_name = vcs['TeamProject'] || nil
|
233
|
+
if team_project_name.nil? || team_project_name.empty?
|
234
|
+
problem = 'No TeamProject value detected in the %s section of the config' % vcs_section_name
|
235
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
236
|
+
raise confex, problem
|
237
|
+
end
|
238
|
+
tfs_server_version = vcs['TfsServerVersion'] || nil
|
239
|
+
if tfs_server_version.nil? || tfs_server_version.to_s.empty?
|
240
|
+
problem = 'No TfsServerVersion value detected in the %s section of the config' % vcs_section_name
|
241
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
242
|
+
raise confex, problem
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# default VCS Lookback value to 90 if not set or not set correctly
|
247
|
+
lookback = vcs['Lookback'] || nil
|
248
|
+
if lookback.nil? #or lookback.class.name != "Fixnum"
|
249
|
+
vcs['Lookback'] = 90
|
250
|
+
end
|
251
|
+
|
252
|
+
# default VCS MaxItems to 1000 if not set or not set correctly
|
253
|
+
max_items = vcs['MaxItems'] || nil
|
254
|
+
if max_items.nil? or max_items.class.name != "Fixnum"
|
255
|
+
vcs['MaxItems'] = 1000
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
public
|
260
|
+
def topLevels()
|
261
|
+
return @top_level_sequence
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
def topLevel(section_name)
|
266
|
+
if section_name == 'VCS'
|
267
|
+
non_vcs_sections = ['Rally', 'Transforms', 'Services']
|
268
|
+
sections = topLevels().select {|name| non_vcs_sections.include?(name) == false}
|
269
|
+
section_name = sections.first
|
270
|
+
end
|
271
|
+
|
272
|
+
if @top_level_sequence.include?(section_name) and @config.has_key?(section_name)
|
273
|
+
return @config[section_name]
|
274
|
+
else
|
275
|
+
problem = 'Attempt to retrieve non-existent top level config section for %s'
|
276
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem % section_name)
|
277
|
+
raise confex, problem % section_name
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
def connectionClassName(section_name)
|
283
|
+
if not @config.has_key?(section_name)
|
284
|
+
problem = 'Attempt to identify connection class name for %s, operation not supported'
|
285
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem % section_name)
|
286
|
+
raise confex, problem % section_name
|
287
|
+
end
|
288
|
+
if not ['Rally', @vcs_header].include?(section_name)
|
289
|
+
problem ='Candidate connection class name "%s" not viable for operation' % section_name
|
290
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
291
|
+
raise confex, problem % section_name
|
292
|
+
end
|
293
|
+
section = @config[section_name]
|
294
|
+
if section.has_key?('Class')
|
295
|
+
class_name = section['Class']
|
296
|
+
else
|
297
|
+
class_name = 'RallyVCSConnection'
|
298
|
+
if section_name != 'Rally'
|
299
|
+
class_name = '%sConnection' % @vcs_header
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
return class_name
|
304
|
+
end
|
305
|
+
|
306
|
+
|
307
|
+
private
|
308
|
+
def defuzzPasswords()
|
309
|
+
rpw = @config['Rally']['Password']
|
310
|
+
if Fuzzer.is_encoded?(rpw)
|
311
|
+
@config['Rally']['Password'] = Fuzzer.defuzz(rpw)
|
312
|
+
else
|
313
|
+
fuzzPassword('Rally', 'Password', rpw)
|
314
|
+
end
|
315
|
+
|
316
|
+
proxy_pw = @config['Rally']['ProxyPassword']
|
317
|
+
unless proxy_pw.nil?
|
318
|
+
if Fuzzer.is_encoded?(proxy_pw)
|
319
|
+
@config['Rally']['ProxyPassword'] = Fuzzer.defuzz(proxy_pw)
|
320
|
+
else
|
321
|
+
fuzzPassword('Rally', 'ProxyPassword', proxy_pw)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# in many cases, there isn't a requirement that the VCS section have a Password entry
|
326
|
+
vpw = @config[@vcs_header]['Password'] || nil
|
327
|
+
if not vpw.nil?
|
328
|
+
if Fuzzer.is_encoded?(vpw)
|
329
|
+
@config[@vcs_header]['Password'] = Fuzzer.defuzz(vpw)
|
330
|
+
else
|
331
|
+
fuzzPassword(@vcs_header, 'Password', vpw)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
|
337
|
+
def fuzzPassword(conn_section, password_section, clear_text_password)
|
338
|
+
"""
|
339
|
+
Check to see whether or not the Password entries in one or both of the connection
|
340
|
+
sections are not encoded. If not, encode them and write out the config file
|
341
|
+
changing *only* those two entries.
|
342
|
+
"""
|
343
|
+
return false if Fuzzer.is_encoded?(clear_text_password)
|
344
|
+
encoded_password = Fuzzer.encode(clear_text_password)
|
345
|
+
|
346
|
+
conf_lines = []
|
347
|
+
begin
|
348
|
+
cf = File.open(@config['ConfigPath'], 'r')
|
349
|
+
rescue IOError => ex
|
350
|
+
problem = 'Unable to open %s for reading, %s' % [@config['ConfigPath'], ex.message]
|
351
|
+
confex = VCSEIF_Exceptions::ConfigurationError.new(problem)
|
352
|
+
raise confex, problem
|
353
|
+
end
|
354
|
+
conf_lines = cf.readlines()
|
355
|
+
cf.close()
|
356
|
+
|
357
|
+
out_lines = []
|
358
|
+
ix = 0
|
359
|
+
|
360
|
+
# objective: Find index of conn_section entry in conf_lines
|
361
|
+
# then find index of next occurring Password : xxxx entry
|
362
|
+
# substitute entry for Password : current with Password : encoded_password
|
363
|
+
hits = []
|
364
|
+
conf_lines.each_with_index do |line, ix|
|
365
|
+
hits << ix if line =~ /^#{conn_section}\s*:/
|
366
|
+
end
|
367
|
+
return false if hits.length == 0
|
368
|
+
|
369
|
+
section_ix = hits.first
|
370
|
+
hits = []
|
371
|
+
conf_lines.each_with_index do |line, ix|
|
372
|
+
hits << ix if line =~ /^\s+#{password_section}\s*:\s*/ and ix > section_ix
|
373
|
+
end
|
374
|
+
return false if hits.length == 0
|
375
|
+
|
376
|
+
pwent_ix = hits.first
|
377
|
+
conf_lines[pwent_ix] = " #{password_section} : %s\n" % encoded_password
|
378
|
+
|
379
|
+
enc_file_name = "#{@config['ConfigPath']}.pwenc"
|
380
|
+
enf = File.open(enc_file_name, 'w')
|
381
|
+
enf.write(conf_lines.join(""))
|
382
|
+
enf.close()
|
383
|
+
|
384
|
+
bkup_name = "#{@config['ConfigPath']}.bak"
|
385
|
+
begin
|
386
|
+
FileUtils.copy(@config['ConfigPath'], bkup_name)
|
387
|
+
rescue Exception => ex
|
388
|
+
problem = "Unable to write a temporary backup file '%s' with config info, %s"
|
389
|
+
@log.warn(problem % [bkup_name, ex.message])
|
390
|
+
return false
|
391
|
+
end
|
392
|
+
|
393
|
+
begin
|
394
|
+
FileUtils.remove_file(@config['ConfigPath'])
|
395
|
+
rescue Exception => ex
|
396
|
+
problem = "Unable to remove config file prior to replacement with "
|
397
|
+
problem << "password encoded version of the file, %s"
|
398
|
+
@log.warn(problem % ex.message)
|
399
|
+
return false
|
400
|
+
end
|
401
|
+
|
402
|
+
begin
|
403
|
+
FileUtils.move(enc_file_name, @config['ConfigPath'])
|
404
|
+
rescue Exception => ex
|
405
|
+
problem = "Unable to rename config file with password encoded "
|
406
|
+
problem << "to standard config filename of %s, %s"
|
407
|
+
@log.error(problem % [@onfig['ConfigPath'], ex.message])
|
408
|
+
return false
|
409
|
+
end
|
410
|
+
|
411
|
+
begin
|
412
|
+
FileUtils.remove_file(bkup_name)
|
413
|
+
rescue Exception => ex
|
414
|
+
@log.warn("Unable to remove temporary backup file for config, %s" % ex.message)
|
415
|
+
return false
|
416
|
+
end
|
417
|
+
|
418
|
+
return true
|
419
|
+
end
|
420
|
+
|
421
|
+
end # of Konfabulator
|