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.
@@ -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
+