vcseif 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,227 @@
1
+ # Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
2
+ require "yaml"
3
+ require "fileutils"
4
+
5
+ require_relative "vcs_connector_runner"
6
+ require_relative "utils/rally_logger"
7
+ require_relative "utils/exceptions"
8
+
9
+ class VCSConnectorDriver
10
+
11
+ NUMBER_OF_REQUIRED_VCS_CONNECTOR_SECTIONS = 3
12
+ VCS_APP_ROOT = 'VCS_APP_ROOT'
13
+
14
+ attr_reader :script_name, :uber_log_name
15
+ attr_accessor :log
16
+
17
+ def initialize(script_name)
18
+ rationalizeEnvironment()
19
+ @script_name = script_name
20
+ logs_directory = "%s/logs" % ENV[VCS_APP_ROOT]
21
+ if not Dir.exists?(logs_directory)
22
+ begin
23
+ FileUtils.mkdir(logs_directory)
24
+ rescue
25
+ end
26
+ end
27
+ @uber_log_name = "%s/%s.log" % [logs_directory, script_name]
28
+ end
29
+
30
+ def rationalizeEnvironment()
31
+ # set vcs_app_root to the directory where either <vcs>2rally.exe or <vcs>2rally.rb is located
32
+ if ENV.keys.include?('OCRA_EXECUTABLE')
33
+ vcs_app_root = File.dirname(ENV['OCRA_EXECUTABLE'].gsub(/\\/, '/'))
34
+ else
35
+ vcs_app_root = File.expand_path(File.dirname($PROGRAM_NAME))
36
+ end
37
+
38
+ unless ENV.keys.include?(VCS_APP_ROOT)
39
+ ENV[VCS_APP_ROOT] = vcs_app_root # make this available everywhere...
40
+ $LOAD_PATH.unshift(vcs_app_root)
41
+ $LOAD_PATH.unshift("#{vcs_app_root}/lib")
42
+ end
43
+ end
44
+
45
+ def execute(args)
46
+ @log = RallyLogger.new(@uber_log_name)
47
+ @log.info("#{@script_name} started with parameters of: #{args.join(', ')}")
48
+
49
+ ret_code = 0
50
+ for config_name in args do
51
+ config = getConnectorConfiguration(config_name)
52
+ next if config.nil?
53
+ ret_code = delegateToConnectorRunner(config)
54
+ @log.info("finished processing config #{config_name} with status code of #{ret_code}")
55
+ end
56
+ @log.info("#{@script_name} COMPLETED")
57
+ return 0
58
+ end
59
+
60
+ def getConnectorConfiguration(config_name)
61
+ """
62
+ Allow a config_name to map to a file with the same value
63
+ exsiting within the ENV[VCS_APP_ROOT]/configs directory.
64
+ If such a file exists, it must be in YAML format and have at least 3 sections.
65
+ Allow specifying a config_name by 'name', 'name.yml', 'name.yaml' or 'name.txt'.
66
+ """
67
+ # suffix-full options: in ENV[VCS_APP_ROOT]/configs subdir
68
+ # suffix-less options: try '%s.yml' % config_name in ENV[VCS_APP_ROOT]/configs subdir
69
+ # try '%s.yaml' % config_name in ENV[VCS_APP_ROOT]/configs subdir
70
+ # try '%s.txt' % config_name in ENV[VCS_APP_ROOT]/configs subdir
71
+ # TODO: consider cleaning this processing up in another method devoted solely to the sussing the options above
72
+
73
+ config_dir = "%s/configs" % ENV[VCS_APP_ROOT]
74
+ ext_name = File.extname(config_name)
75
+ if ext_name != "" # suffix-full
76
+ config_file_path = '%s/%s' % [config_dir, config_name]
77
+ if Dir.glob("#{config_dir}/*").include?(config_name)
78
+ config_name = config_name.sub(/#{ext_name}$/, '')
79
+ end
80
+ else # suffix-less
81
+ conf_root = "%s/%s" % [config_dir, config_name]
82
+ config_file_path = conf_root
83
+ if File.exists?('%s.yml' % conf_root)
84
+ config_file_path = "%s.yml" % conf_root
85
+ elsif File.exists?('%s.yaml' % conf_root)
86
+ config_file_path = "%s.yaml" % conf_root
87
+ elsif File.exists?('%s.txt' % conf_root)
88
+ config_file_path = "%s.txt" % conf_root
89
+ end
90
+ end
91
+
92
+ if not File.exists?(config_file_path)
93
+ problem = "Config file for name: %s not found" % config_name
94
+ @log.error(problem)
95
+ return nil
96
+ end
97
+
98
+ if not File.file?(config_file_path)
99
+ problem = 'Config file: %s is not a file' % config_file_path
100
+ @log.error(problem)
101
+ return nil
102
+ end
103
+
104
+ if not File.readable?(config_file_path)
105
+ problem = 'Config file: %s not a readable file' % config_file_path
106
+ @log.error(problem)
107
+ return nil
108
+ end
109
+
110
+ config = loadConfiguration(config_file_path)
111
+ config_file_name = config_file_path.split("/")[-1]
112
+ if not config.nil?
113
+ config['ConfigName'] = File.basename(config_file_name, '.*')
114
+ config['ConfigPath'] = config_file_path
115
+ end
116
+
117
+ return config
118
+ end
119
+
120
+ def loadConfiguration(config_file_path)
121
+ begin
122
+ content = "" and File.open(config_file_path, 'r') { |f| content = f.read() }
123
+ # As YAML forbids tab chars, we have to rid the content of tab chars,
124
+ # but emit a log warning that tabs were detected and transformed if we do
125
+ if content =~ /\t/
126
+ content.gsub!(/\t/, ' ')
127
+ @log.warning('One or more tab characters (\\t) was detected in the config file and transformed to spaces. YAML does not allow tab characters.')
128
+ end
129
+ config = YAML.load(content)
130
+ raise "config file: #{} is not a valid YAML file" unless config.class.name == 'Hash'
131
+ sections = config.keys
132
+ rescue Exception => ex
133
+ @log.error("Unable to load YAML from config_file '%s' : %s" % [config_file_path, ex.message])
134
+ return nil
135
+ end
136
+
137
+ if config.keys.length < NUMBER_OF_REQUIRED_VCS_CONNECTOR_SECTIONS
138
+ @log.error("config '%s' not in required YAML format or has too few sections" % config_file_path)
139
+ return nil
140
+ end
141
+
142
+ return config
143
+ end
144
+
145
+ def delegateToConnectorRunner(config)
146
+ """
147
+ Given a Hash with the configuration content,
148
+ obtain the correct _X_ConnectorRunner instance and feed it
149
+ the config and call its run method.
150
+ """
151
+ hits = config.keys.select {|key| key =~ /^\w+Connector$/}
152
+ if hits.nil? or hits.empty?
153
+ @log.error("Config %s lacks an entry identifying the EIF Connector type" % config['ConfigName'])
154
+ return 1
155
+ end
156
+ connector_type = hits.first
157
+
158
+ runner_name = '%sRunner' % connector_type
159
+ begin
160
+ connector_runner_class = Kernel.const_get(runner_name)
161
+ rescue => ex
162
+ problem = 'Unable to locate/load the %sRunner class. Is your vcseif gem properly installed?'
163
+ @log.fatal(problem % connector_type)
164
+ return 2
165
+ end
166
+
167
+ begin
168
+ connector_runner = connector_runner_class.new(config)
169
+ rescue => ex
170
+ @log.error("#{ex.message}")
171
+ problem = 'Unable to obtain an instance of the %sRunner class. Is your vcseif gem properly installed?'
172
+ @log.fatal(problem % connector_type)
173
+ return 3
174
+ end
175
+
176
+ ret_code = 0
177
+ blurtage = ""
178
+ begin
179
+ connector_runner.run()
180
+ rescue VCSEIF_Exceptions::ConfigurationError => ex
181
+ blurtage = ex.verbiage
182
+ err_text = "ERROR: #{@script_name} detected a FATAL configuration error, #{blurtage}."
183
+ log_entry = "FATAL: Configuration error: #{blurtage}"
184
+ recordAnomaly(ex, err_text, blurtage, log_entry)
185
+ ret_code = 4
186
+ rescue VCSEIF_Exceptions::UnrecoverableException => ex
187
+ blurtage = ex.verbiage
188
+ err_text = "ERROR: #{@script_name}, #{blurtage}."
189
+ log_entry = "UnrecoverableException: #{blurtage}"
190
+ recordAnomaly(ex, err_text, blurtage, log_entry)
191
+ ret_code = 5
192
+ rescue VCSEIF_Exceptions::OperationalError => ex
193
+ blurtage = ex.verbiage
194
+ err_text = "ERROR: #{@script_name} detected an OperationalError condition"
195
+ log_entry = "OperationalError: #{blurtage}"
196
+ recordAnomaly(ex, err_text, blurtage, log_entry)
197
+ ret_code = 6
198
+ rescue Exception => ex
199
+ blurtage = ex.message
200
+ err_text = "ERROR: #{@script_name} detected an Exception condition"
201
+ recordAnomaly(ex, err_text, blurtage, "")
202
+ ret_code = 9
203
+ end
204
+
205
+ return ret_code
206
+ end
207
+
208
+ def recordAnomaly(ex, issue, blurtage, log_entry)
209
+ $stderr.write("%s\n" % issue)
210
+ #$stderr.write("ERROR: #{blurtage}\n") if !blurtage.empty?
211
+ #$stderr.write("#{ex.message}\n")
212
+ @log.error(log_entry) if not log_entry.empty?
213
+
214
+ @log.entry("ERROR: #{blurtage} #{ex.message}")
215
+ @log.entry(ex.backtrace.join("\n"))
216
+ @log.write("call chain follows:")
217
+ # blurt out a formatted stack trace in outermost to innermost call sequence
218
+ for bt_line in ex.backtrace.reverse do
219
+ short_bt = bt_line.sub(/#{Dir.pwd}\//, "... /")
220
+ short_bt = short_bt.sub(/^.*\/gems\//, "... gems/")
221
+ @log.write(" #{short_bt}")
222
+ $stderr.write(" #{short_bt}\n")
223
+ end
224
+ @log.write("!!!")
225
+ end
226
+
227
+ end
@@ -0,0 +1,283 @@
1
+ # Copyright 2001-2013 Rally Software Development Corp. All Rights Reserved.
2
+
3
+ ############################################################################################################
4
+ #
5
+ # vcs_connector_runner - detect commits in a VCS Repository (non-Rally) and add
6
+ # any unrecorded changeset (and change) information to
7
+ # an SCMRepository in a Rally Workspace
8
+ #
9
+ ############################################################################################################
10
+
11
+ require 'date'
12
+ require 'time'
13
+
14
+ ############################################################################################################
15
+
16
+ class VCSConnectorRunner
17
+
18
+ attr_reader :log, :config
19
+ # the following declarations are necessary for testing, otherwise nothing accesses them
20
+ # except the instance
21
+ attr_reader :logfile_name, :lockfile_name, :preview, :connector, :extension, :time_file
22
+
23
+ EXISTENCE_PROCLAMATION = %{
24
+ ************************************************************************************************************
25
+
26
+ Rally VCSConnector starting at: %s with pid: %s
27
+ curdir: %s
28
+ command: %s
29
+
30
+ ************************************************************************************************************
31
+ }
32
+
33
+ STD_TS_FMT = '%Y-%m-%d %H:%M:%S Z'
34
+
35
+ DEFAULT_ORIGIN_TIME = (DateTime.now - 3).to_time
36
+ ############################################################################################################
37
+
38
+
39
+ def initialize(config)
40
+ """
41
+ An instance of this class is instantiated with a Hash containing the
42
+ specifics of a configuration to direct the execution. Some rudimentary
43
+ validation of the configuration is done, via use of an instance of a
44
+ Konfabulator, with any detected problem noted and a ConfigurationError is
45
+ raised in that eventuality.
46
+ Once those items are taken care of, the instance obtains an instance of a
47
+ an AuxiliaryClassLoader instance and an instance of a VCSConnector.
48
+ The AuxiliaryClassLoader is delegated the task of pulling in any Extension classes.
49
+ These instances are then provided to the VCSConnector instance.
50
+ This instance then runs the operate_service method that directs the VCSConnector
51
+ instance to obtain unrecorded changesets from the target VCS and reflect them in
52
+ the Rally server.
53
+ """
54
+ @logfile_name = "%s/logs/%s.log" % [ENV['VCS_APP_ROOT'], config['ConfigName']]
55
+ @lockfile_name = '%s/%s.LOCK' % [ENV['VCS_APP_ROOT'], config['ConfigName']]
56
+ @log = RallyLogger.new(@logfile_name)
57
+ VCSEIF_Exceptions::logAllExceptions(true, @log)
58
+ @config = config
59
+ @preview = false
60
+ @connector = nil
61
+ @extension = {}
62
+ @time_file = nil
63
+ end
64
+
65
+ def proclaim_existence
66
+ proc = ProcTable.target_process($$) # $$ is the Ruby shorthand for Process.pid
67
+ start_time = Time.now.utc.strftime(STD_TS_FMT)
68
+ cmd_elements = proc.cmdline.split()
69
+ cmd_elements[0] = File.basename(cmd_elements.first)
70
+ proc.cmdline = cmd_elements.join(" ")
71
+ @log.write(EXISTENCE_PROCLAMATION % [start_time, proc.pid, Dir.pwd, proc.cmdline])
72
+ end
73
+
74
+ def acquireLock
75
+ """
76
+ Check for conditions to proceed based on lock file absence/presence status.
77
+ Acquisition of the lock is a green-light to proceed.
78
+ Failure to acquire the lock prevents any further operation.
79
+ """
80
+ if LockFile.exists?(@lockfile_name)
81
+ @log.warning("A %s file exists" % @lockfile_name)
82
+ locker = LockFile.current_lock_holder(@lockfile_name)
83
+ if not LockFile.locker_is_running?(@lockfile_name, locker)
84
+ message = "A prior connector process (%s) did not clean up "
85
+ message << "the lock file on termination, proceeding with this run"
86
+ @log.warn(message % locker)
87
+ else
88
+ @log.error("Another connector process [%s] is still running, unable to proceed" % locker)
89
+ problem = "Simultaneous processes for this connector are prohibited"
90
+ boomex = VCSEIF_Exceptions::UnrecoverableException.new(problem)
91
+ raise boomex, problem
92
+ end
93
+ end
94
+
95
+ LockFile.create_lock(@lockfile_name)
96
+ return true
97
+ end
98
+
99
+ def releaseLock
100
+ LockFile.destroy_lock(@lockfile_name)
101
+ end
102
+
103
+
104
+ public
105
+ def run
106
+ proclaim_existence()
107
+ own_lock = false
108
+ own_lock = acquireLock()
109
+
110
+ begin
111
+ operateService()
112
+ rescue Exception => ex
113
+ raise
114
+ ensure
115
+ begin
116
+ releaseLock() if own_lock
117
+ rescue Exception => ex
118
+ problem = "ERROR: unable to remove lock file '%s', %s" % [@lockfile_name, ex.message]
119
+ shux = VCSEIF_Exceptions::OperationalError.new(problem)
120
+ raise shux, problem
121
+ end
122
+ end
123
+
124
+ end
125
+
126
+
127
+ private
128
+ def operateService()
129
+ started = finished = elapsed = nil
130
+ connector = nil
131
+ @log.info("processing to commence using content from the %s.yml config file" % @config['ConfigPath'])
132
+
133
+ conf_name = @config['ConfigName']
134
+ conf_path = @config['ConfigPath']
135
+ last_conf_mod = File.mtime(conf_path).utc.strftime(STD_TS_FMT)
136
+ blurb_vars = [ conf_path, last_conf_mod, File.size(conf_path) ]
137
+ @log.info("%s last modified %s, size: %d chars" % blurb_vars)
138
+ @config = inflateConfiguration(@config) # transform @config from Hash to Konfabulator instance
139
+
140
+ this_run = Time.now.utc # be optimistic that reflectChangesetsInRally will succeed
141
+ now_zulu = this_run.strftime(STD_TS_FMT) # zulu <-- universal coordinated time <-- UTC
142
+
143
+ @time_file = TimeFile.new(buildTimeFileName(conf_name), @log)
144
+ if File.exists?(@time_file.filename)
145
+ #last_commit_zulu_seconds = (@time_file.read() - 3600) # the 3600 seconds is a DST compensator
146
+ last_commit_zulu_seconds = @time_file.read()
147
+ last_commit = Time.at(last_commit_zulu_seconds).utc # convert epoch seconds value to Zulu time
148
+ else
149
+ last_commit = DEFAULT_ORIGIN_TIME
150
+ end
151
+ last_commit_zulu = last_commit.strftime(STD_TS_FMT)
152
+ log_msg = "Time File value %s --- Now %s" % [last_commit_zulu, now_zulu]
153
+ @log.info(log_msg)
154
+
155
+ connector = VCSConnector.new(@config, @log)
156
+ @log.debug("got an VCSConnector instance, calling its run method...")
157
+ status, repo_changesets = connector.run(last_commit, @extension)
158
+ finished = Time.now.utc
159
+ elapsed = finished - this_run
160
+ ##
161
+ ## puts "after VCSConnector.run, status: |#{status}| repo_changesets created to follow:"
162
+ ## target_repo = repo_changesets.keys.first
163
+ ## for repo_chgset in repo_changesets[target_repo] do
164
+ ## name = repo_chgset['Name']
165
+ ## tstamp = repo_chgset['CommitTimestamp'].sub('T', ' ')
166
+ ## puts "%36.36s %-19.19s Z" % [name, tstamp]
167
+ ## end
168
+ ##
169
+ logServiceStatistics(repo_changesets, elapsed.round)
170
+
171
+ if @preview
172
+ @log.info("Preview mode in effect, Time File not written/updated")
173
+ return
174
+ end
175
+ if status != true
176
+ # Not writing the time.file may cause repetitive detection of changesets,
177
+ # but that is better than missing out on changesets altogether
178
+ @log.info("There was an error in processing so Time File was not written")
179
+ problem = "OperationalErrors detected during this processing run"
180
+ operr = VCSEIF_Exceptions::OperationalError.new(problem)
181
+ raise operr, problem
182
+ end
183
+
184
+ repo_name = repo_changesets.keys().first
185
+ changesets = repo_changesets[repo_name]
186
+
187
+ # we've added changesets successfully, so update the Time File (x_time.file)
188
+ # the time_file instance converts epoch seconds to a readable representation
189
+ # but we also emit a readable representation of this_run in the log with the desired context
190
+ updated_last_commit = calc_last_commit_time(changesets, last_commit)
191
+ @time_file.write(updated_last_commit)
192
+ @log.info("time file written with value of %s" % updated_last_commit)
193
+ end
194
+
195
+
196
+ public
197
+
198
+ def calc_last_commit_time(changesets, default)
199
+ return default if changesets.nil? || changesets.empty?
200
+ last = changesets.first # default
201
+ last = changesets[-2] if changesets.length >= 2
202
+ #the last commit we get is a timestamp value from Rally which is already in Zulu with milliseconds
203
+ calctime = DateTime.parse(last["CommitTimestamp"]).to_time
204
+ @log.debug("Calculated timefile value of #{calctime}")
205
+ calctime
206
+ end
207
+
208
+ def buildTimeFileName(config_name)
209
+ tf_name = "%s/time.file" % ENV['VCS_APP_ROOT']
210
+ if not config_name.empty?
211
+ tf_name = "%s/%s_time.file" % [ENV['VCS_APP_ROOT'], config_name]
212
+ end
213
+ return tf_name
214
+ end
215
+
216
+
217
+ private
218
+ def logServiceStatistics(repo_changeset, elapsed)
219
+ """
220
+ what we intend to append to the log...
221
+ vcs_conn_config.yml: 32 additional VCS changesets reflected in Rally
222
+ and a line with the elapsed time taken in human readable form
223
+ (elapsed is in seconds (a float value))
224
+ """
225
+ repo_changeset.each_pair do |repo, changesets|
226
+ preview_reminder = "" # default
227
+ preview_reminder = "(Preview Mode)" if @preview == true
228
+ cs_length = changesets.nil? ? 0 : changesets.length
229
+ @log.info("%d changesets created for %s %s" % [cs_length, repo, preview_reminder])
230
+ end
231
+ hours, rem = elapsed.divmod(3600)
232
+ mins, secs = rem.divmod( 60)
233
+ duration = "%d:%02d:%02d (hms)" % [hours, mins, secs]
234
+ @log.info("service run took %s" % duration)
235
+ end
236
+
237
+
238
+ public
239
+ def inflateConfiguration(config)
240
+ """
241
+ Given a Hash with config info, obtain and return a Konfabulator instance
242
+ that incorporates that config information into a form that provides an easy to
243
+ use interface to access the configuration specification elements.
244
+ """
245
+ begin
246
+ konfig = Konfabulator.new(config, @log)
247
+ rescue VCSEIF_Exceptions::ConfigurationError => ex
248
+ # info for this will have already been logged or blurted
249
+ raise
250
+ rescue VCSEIF_Exceptions::NonFatalConfigurationError => ex
251
+ # info for this will have already been logged or blurted
252
+ raise
253
+ rescue Exception => ex
254
+ raise StandardError, ex.message
255
+ end
256
+
257
+ svc_conf = konfig.topLevel('Services')
258
+ @preview = false
259
+ @preview = true if svc_conf and svc_conf['Preview'] == true
260
+ @log.info('Preview mode in effect') if @preview
261
+ @log_level = 'Info'
262
+ ll = svc_conf['LogLevel'] || nil
263
+ ll = ll.downcase.sub(/^(.)/) {$1.capitalize} unless ll.nil? # title case the result
264
+ if ['Fatal', 'Error', 'Warn', 'Info', 'Debug'].include?(ll)
265
+ @log_level = ll
266
+ @log.set_level(@log_level)
267
+ @log.info("LogLevel set to '%s'" % @log_level)
268
+ #else
269
+ # bad LogLevel specified, do nothing, we've already defaulted to Info level
270
+ end
271
+
272
+ # TODO: enable this when a well-defined need for it has been established
273
+ #if svc_conf.contains('PostBatchExtension')
274
+ # pba_class_name = svc_conf.get('PostBatchExtension')
275
+ # pba_class = ExtensionLoader.getExtension(pba_class_name)
276
+ # @extension['PostBatch'] = pba_class.new()
277
+ #end
278
+
279
+ return konfig
280
+ end
281
+
282
+ end
283
+