-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathconfig.py
More file actions
639 lines (528 loc) · 24.6 KB
/
config.py
File metadata and controls
639 lines (528 loc) · 24.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# coding: utf-8
#
# Copyright (c) Microsoft Corporation. All rights reserved.
#
##
# Module containing base classes to load configuration
#
# Date: 2008-11-14
#
import os
import stat
from project import *
import subprocess
import sys
##
# Class containing machine defitions
#
class MachineItem:
##
# Ctor.
# \param[in] Host key
# \param[in] Machine address
# \param[in] Destination path
def __init__(self, tag, host, path, project):
self.tag = tag
self.host = host
self.path = path
self.project = project
##
# Return the tag name associated with an entry
#
def GetTag(self):
return self.tag
##
# Return the host name associated with an entry
#
def GetHost(self):
return self.host
##
# Return the directory path associated with an entry
#
def GetPath(self):
return self.path
##
# Return the project associated with an entry
#
def GetProject(self):
return self.project
##
# Class containing generic logic for loading and handling configuration file
#
class Configuration:
##
# Ctor.
# \param[in] configuration Configuration map.
#
def __init__(self, options, args):
self.options = options
self.args = args
self.machineKeys = []
self.machines = {}
self.machines_allselects = {}
self.currentSettings = {}
self.excludeList = []
self.test_attr = ''
self.test_list = ''
self.configure_options = {}
if self.options.select != None:
self.select = self.options.select
else:
self.select = ''
# Define the valid settings options (can be prefixed with "no")
# Set the default settings along the way. Can be overridden by:
# 1. Configuration file
# 2. Command line option
self.validSettings = [ 'checkvalidity', 'debug', 'deletelogfiles', 'diagnoseerrors', 'logfilerename', 'logfileselect', 'progress', 'summaryscreen' ]
self.ParseSettings('defaults', 'CheckValidity,Debug,DeleteLogfiles,NoDiagnoseErrors,NoLogfileRename,NoLogfileSelect,Progress,SummaryScreen')
# Default location for PBUILD logfiles (include trailing "/" in path)
self.logfilePrefix = os.path.join(os.path.expanduser('~'), '')
self.logfilePriorPrefix = ''
# Configuration file is in ~/.pbuild, or overridden via PBUILD (env)
# (If PBUILD defined but empty, just revert to the default)
try:
self.configurationFilename = os.environ['PBUILD']
except KeyError:
self.configurationFilename = ''
if len(self.configurationFilename) == 0:
self.configurationFilename = os.path.join(os.path.expanduser('~'), '.pbuild')
##
# Get the selector to build. If empty, no selector specifications are allowed
# (for backwards compatibility).
#
def GetSelectSpecification(self):
return self.select.lower()
##
# Get the configuration filename. Controlled by environment variable
# 'PBUILD', defaults to '~./pbuild'.
#
def GetConfigurationFilename(self):
return self.configurationFilename
##
# Get the log file prefix - the directory path to write the log files to
#
# This method should return a trailing "/" in the directory path (so we
# can simply append the filename that we need).
#
def GetLogfilePrefix(self):
return self.logfilePrefix
##
# Get the prior log file prefix - the directory path to save prior log files to
#
# This method should return a trailing "/" in the directory path (so we
# can simply append the filename that we need). If this method returns
# the empty string, then no prior logfile directory is defined.
#
def GetLogfilePriorPrefix(self):
return self.logfilePriorPrefix
##
# Get a settings value
# \throw if setting is not valid
#
def GetSetting(self, setting):
if setting.lower() in self.currentSettings:
return self.currentSettings[setting.lower().strip()]
else:
raise KeyError
##
# Get the test attributes - the list of attributes that tests are restricted to
#
def GetTestAttributes(self):
return self.test_attr
##
# Get the test list - the list of test names to include or exclude from the run
#
def GetTestList(self):
return self.test_list
##
# Parse a settings string and set the appropriate settings
#
def ParseSettings(self, source, settings):
settingsList = settings.lower().replace(',', ' ').split()
for entry in settingsList:
# Check for negative ('no' prefix)
positive = True
if entry.startswith('no'):
entry = entry.replace('no', '', 1)
positive = False
if entry in self.validSettings:
self.currentSettings[entry] = positive
else:
sys.stderr.write('Invalid setting found in %s: [no]%s\n' % (source, entry))
sys.exit(-1)
##
# Parse a host name entry from the configuration file
#
# Host entries can be of the following format:
#
# host: tag host directory project [selector]
#
# Note: "host:" tag is removed before we're called.
def ParseHostEntry(self, elements, taglist, hostlist, taglist_global):
line = elements.rstrip()
elements = elements.rstrip().split()
entryTag = ""
entryHost = ""
entryDirPath = ""
entryProject = ""
entrySelect = ""
# Do we have the correct number of entries
if len(elements) == 4 or len(elements) == 5:
entryTag = elements[0].lower()
entryHost = elements[1]
entryDirPath = elements[2]
entryProject = elements[3].lower()
# Was both a project and selector specified on this host entry?
if len(elements) == 5:
entrySelect = elements[4]
if self.GetSelectSpecification() == '':
sys.stderr.write('No selector specified - select specification is required for host entry - offending line:\n'
+ '\'' + line.rstrip() + '\'\n')
sys.exit(-1)
else:
entrySelect = entryProject
# Validate the project name
if not self.VerifyProjectName(entryProject):
raise IOError('Bad project in configuration file - offending line: \'' + line.rstrip() + '\'')
# No match for this selector? Just skip the host entry ...
if entrySelect.lower() != self.GetSelectSpecification():
# But first: Add this machine to the list of machines for all selectors
select_key = "%s<>select_sep<>%s" % (entrySelect, entryTag)
if select_key in taglist_global:
sys.stderr.write('Duplicate key "%s" found in configuration for selector "%s"\n'
% (select_key, entrySelect))
sys.exit(-1)
taglist_global.append(select_key)
self.machines_allselects[select_key] = MachineItem(entryTag, entryHost, entryDirPath, "")
return
else:
raise IOError('Bad configuration file - offending line: \'' + line.rstrip() + '\'')
if entryTag in taglist:
sys.stderr.write('Duplicate key "%s" found in configuration\n' % entryTag)
sys.exit(-1)
if entryHost in hostlist:
sys.stderr.write('Duplicate host "%s" found in configuration\n' % entryHost)
sys.exit(-1)
# Add to list of machines for all selectors
select_key = "%s<>select_sep<>%s" % (entrySelect, entryTag)
if select_key in taglist_global:
sys.stderr.write('Duplicate key "%s" found in configuration for selector "%s"\n'
% (select_key, entrySelect))
sys.exit(-1)
taglist_global.append(select_key)
self.machines_allselects[select_key] = MachineItem(entryTag, entryHost, entryDirPath, "")
# Add to list of machines to process (selector-specific)
taglist.append(entryTag)
hostlist.append(entryHost)
self.machines[entryHost] = MachineItem(entryTag, entryHost, entryDirPath, entryProject)
##
# Parse a test attribute string and set the appropriate settings
#
# For now, attributes can only be: 'SLOW' or '-SLOW'.
#
# We allow mixed case, but beyond that, simple validation (no abbreviations).
# This can be extended if list of test attributes gets more extensive.
#
def ParseTestAttributes(self, source, attributes):
if attributes == '' or attributes.lower() == 'slow' or attributes.lower() == '-slow':
self.test_attr = attributes.upper()
else:
sys.stderr.write('Invalid test attribute found in %s: %s\n' % (source, attributes))
sys.exit(-1)
##
# Read and parse the configuration file
#
def LoadConfigurationFile(self):
# (Keep track of a global taglist for all selectors for --initialize)
taglist = []
hostlist = []
taglist_global = []
# Load the configuration file - and verify it, line by line
f = open(self.configurationFilename, 'r')
for line in f:
if line.strip() != "" and not line.lstrip().startswith('#'):
# Strip off any in-line comment
line = line.split('#')[0]
elements = line.rstrip().split(':')
# The "host:" tag explicitly defines a host and is now required
if len(elements) == 2 and elements[0].strip().lower() == "host":
self.ParseHostEntry(elements[1].rstrip(), taglist, hostlist, taglist_global)
# Allow "select:" to sepcify default selector to build for all builds
elif len(elements) == 2 and elements[0].strip().lower() == "select":
self.select = elements[1].strip()
if self.options.select != None:
self.select = self.options.select
# Allow "exclude:" to specify a list of hosts to exclude
elif len(elements) == 2 and elements[0].strip().lower() == "exclude":
self.excludeList = elements[1].strip().split(',')
# Allow "logdir:" to specify the directory used for log files
elif len(elements) == 2 and elements[0].strip().lower() == "logdir":
self.logfilePrefix = elements[1].strip().replace('~/', os.path.join(os.path.expanduser('~'), ''))
# Include trailing "/" in path
self.logfilePrefix = os.path.join(self.logfilePrefix, '')
# Allow "logdir_prior:" to specify the directory used for prior log files
elif len(elements) == 2 and elements[0].strip().lower() == "logdir_prior":
self.logfilePriorPrefix = elements[1].strip().replace('~/', os.path.join(os.path.expanduser('~'), ''))
# Include trailing "/" in path
self.logfilePriorPrefix = os.path.join(self.logfilePriorPrefix, '')
# Allow "settings:" to override the default settings
elif len(elements) == 2 and elements[0].strip().lower() == "settings":
self.ParseSettings("configuration file", elements[1].strip())
# Special handling for debug - override parsed options if unspecified on command line
# (Build code only checks for parsed options, not setting)
if not self.options.debug and not self.options.nodebug:
if self.GetSetting("Debug"):
self.options.debug = True
else:
self.options.nodebug = True
# Allow "test_attributes:" to specify the test attributes to use
elif len(elements) == 2 and elements[0].strip().lower() == "test_attributes":
self.ParseTestAttributes("configuration file", elements[1].strip())
# Allow "test_names:" to specify the list of tests to run or exclude
elif len(elements) == 2 and elements[0].strip().lower() == "test_list":
self.test_list = elements[1].strip()
# Per-project configuration options ...
#
# Format of these should be:
# keyword:<Project>:<value>
elif len(elements) == 3 and elements[0].strip().lower() == "make_target":
# Validate the project name
elements[1] = elements[1].strip().lower()
if not self.VerifyProjectName(elements[1]):
raise IOError('Bad project name in configuration file - offending line: \'' + line.rstrip() + '\'')
# If target wasn't overridden on command line, replace it with value from configuration file
if self.options.target == "target_default":
self.options.target = elements[2]
elif len(elements) == 3 and elements[0].strip().lower() == "configure_options":
# Validate the project name
elements[1] = elements[1].strip().lower()
if not self.VerifyProjectName(elements[1]):
raise IOError('Bad project name in configuration file - offending line: \'' + line.rstrip() + '\'')
self.configure_options[elements[1]] = elements[2]
else:
raise IOError('Bad configuration file - offending line: \'' + line.rstrip() + '\'')
f.close()
# Handle override for exclude list in the configuration file by command line
if self.options.exclude != None:
self.excludeList = self.options.exclude.replace(',', ' ').split()
# Handle override for logdir (and logdir_prior) in the configuration file by command line
if self.options.logdir != None:
self.logfilePrefix = self.options.logdir.replace('~/', os.path.join(os.path.expanduser('~'), ''))
# Include trailing "/" in path
self.logfilePrefix = os.path.join(self.logfilePrefix, '')
if self.options.logdir_prior != None:
self.logfilePriorPrefix = self.options.logdir_prior.replace('~/', os.path.join(os.path.expanduser('~'), ''))
# Include trailing "/" in path
self.logfilePriorPrefix = os.path.join(self.logfilePriorPrefix, '')
# Handle override for settings in configuration file by command line
# (As optimization, --nocurses forces no progress updates)
if self.options.settings != None:
self.ParseSettings("command line", self.options.settings)
if self.options.nocurses:
self.ParseSettings('optimization', 'NoProgress')
# Handle override for test attributes in configuration file by command line
if self.options.test_attrs != None:
self.ParseTestAttributes("command line", self.options.test_attrs)
# Handle override for test list in the configuration file by command line
if self.options.tests != None:
self.test_list = self.options.tests
# Try to write a file to the log directory to be certain we can!
# (and, in case it's defined, test the prior log directory as well)
self.VerifyLogdirWritable(self.GetLogfilePrefix())
if self.GetLogfilePriorPrefix() != '':
self.VerifyLogdirWritable(self.GetLogfilePriorPrefix())
# No select: tag specified, and no --select qualifier
if len(self.select) == 0:
sys.stderr.write('No --select on command line and no \'select:\' tag in configuration\n')
sys.exit(-1)
# Be sure we have at least one host to deal with ...
if len(taglist) == 0:
if self.GetSelectSpecification() != '':
sys.stderr.write('No host entries found for selector \''
+ self.GetSelectSpecification()
+ '\' in pbuild configuration file\n')
else:
sys.stderr.write('No matching host entries found in pbuild configuration file\n')
sys.exit(-1)
# Final processing:
# Be sure that we have all of our SSH host keys set up
# Validate the host list
self.InitializeSSH()
self.ValidateHostList()
##
# Initialize SSH known hosts
#
# We use file '~/.pbuild_init' to track when we were last initialized.
# If configuration file is newer than initialization file, we init again.
# This helps insure that we have SSH certificates for all of the hosts.
#
def InitializeSSH(self):
pbuildInitialized = False
if not self.GetSetting("CheckValidity"):
pbuildInitialized = True
initFilename = os.path.join(os.path.expanduser('~'), '.pbuild_init')
try:
initStat = os.stat(initFilename)
configStat = os.stat(self.GetConfigurationFilename())
if configStat[stat.ST_MTIME] < initStat[stat.ST_MTIME]:
pbuildInitialized = True
except OSError:
pass
if pbuildInitialized and not self.options.initialize:
return True
# Try and remove the file if it exists
try:
os.remove(initFilename)
except OSError:
# If the file doesn't exist, that's fine
pass
# We only need to check each machine once (not per project).
# Thus, build a set of unique hostnames
uniqueHosts = set()
for key in self.machines_allselects.keys():
host = self.machines_allselects[key].GetHost()
uniqueHosts.add(host)
# We use complete host list rather than hosts specified on command line
# Using git doesn't require pre-setup as such (other than public/private
# key to the host machine), but it DOES require an entry in .known_hosts
# to not require a prompt.
#
# Algorithm:
# 1) Connect to the machine in question with SSH auth forwarding
# 2) Use grep to see if github.com is known in .known_hosts
# 3) If not, issue ssh command to add entry to .known_hosts
hostsOK = True
for host in sorted(uniqueHosts):
print "Checking host:", host
process = subprocess.Popen(
[
'ssh', '-A', host,
'grep github.com, ~/.ssh/known_hosts > /dev/null 2> /dev/null || ssh -o StrictHostKeyChecking=no -o HashKnownHosts=no -T git@github.com; grep github.com, ~/.ssh/known_hosts > /dev/null 2> /dev/null || ssh -o StrictHostKeyChecking=no -T git@github.com'
],
stdin=subprocess.PIPE
)
process.wait()
if process.returncode != 0:
hostsOK = False
if hostsOK:
# If all hosts are okay, create marker file
initFile = open(initFilename, 'w')
initFile.close()
if self.options.initialize:
print "Completed host verification pass"
sys.exit(0)
return hostsOK
##
# Normalize host specification
#
# We support host specifications (in list of machines to include) in one of two
# forms: host "tags" (used for log file names), and DNS name/IP number. However,
# code in pbuild expects hosts to solely be in "tag" form.
#
# This function will "normalize" host specifications. Input is any supported
# form, output is the associated tag.
#
def NormalizeHostSpec(self, hostSpec):
try:
# Do the easy thing first: is the entry a hostname for a host?
if self.machines[hostSpec]:
return hostSpec;
except KeyError:
# Nope - so loop thorugh our list of hosts looking at tags that way
for key in self.machines:
if self.machines[key].GetTag() == hostSpec:
return key;
sys.stderr.write('Failed to identify host \'%s\' in configuration\n' % hostSpec)
sys.exit(-1)
##
# Validate specific list of hosts if one was specified
#
# For a match, we allow either the entry tag for a host or the hostname itself
# Take care to not allow the same machine to be specified twice in the resulting list
#
def ValidateHostList(self):
# Build a list of machines to process (if none, we simply process all hosts)
for entry in self.args:
key = self.NormalizeHostSpec(entry)
if key in self.machineKeys:
sys.stderr.write('Duplicate host \'%s\' already in configuration\n' % key)
sys.exit(-1)
self.machineKeys.append(key)
# Support the list of machines to exclude if one was specified
if len(self.excludeList):
# If no hosts specified to include, then let's include all hosts
fAllHosts = False
if len(self.machineKeys) == 0:
for key in sorted(self.machines.keys()):
self.machineKeys.append(key)
fAllHosts = True
# Now exclude each of the hosts specified in the exclude list
for entry in self.excludeList:
key = self.NormalizeHostSpec(entry)
# If host wasn't in our list, then must be specific list of
# hosts to build - that's not an error condition
#
# Don't remove host that's specifically included in include list
if key in self.machineKeys and fAllHosts:
self.machineKeys.remove(key)
# Verify if the subproject list is sensical for selected hosts
# We validate based on the machines, we're actually building with
# (either the ones specified at launch, or all of the machines in configuraiton)
#
# Note that we can only validate the subproject name, not the branch.
# That is, a subproject looks like: "<dir>:<branch>". We are validating
# the <dir>. To validate the <branch>, we would need to integrate with
# GitHub API, and that doesn't seem worth it right now.
if self.options.subproject:
# Get the list of machines we're actually going to build with
machineList = [ ]
if len(self.machineKeys):
machineList = sorted(self.machineKeys)
else:
machineList = sorted(self.machines.keys())
# We're probably building just one project, but in case we are not,
# validate the subproject list with every machine we're building.
for entry in machineList:
projectName = self.machines[entry].GetProject()
factory = ProjectFactory(projectName)
assert factory.Validate()
project = factory.Create()
subprojectList = self.options.subproject.split(',')
for subproject in subprojectList:
# Subproject spec looks like: <dir>:<branch>
subproject_dir, subproject_branch = subproject.split(':')
if not project.ValidateSubproject(subproject_dir):
sys.stderr.write('Invalid subproject \'%s\' for project \'%s\'\n'
% (subproject_dir, projectName) )
sys.exit(-1)
# Okay, we're done. State of the world:
#
# If self.machineKeys is empty, then all machines should be processed
# Else self.machineKeys is the list of tags to process (perhaps pruned by the exclude list)
##
# Verify that our logfile directory is writable
#
def VerifyLogdirWritable(self, dirpath):
# Try to write a file to the log directory to be certain we can!
outfname = dirpath + '.pbuild_logtest.log'
try:
try:
os.remove(outfname)
except OSError:
# Problems removing for now? That's fine
pass
# Open the output file
outf = open(outfname, 'a+')
outf.close()
os.remove(outfname)
except:
sys.stderr.write('Error writing or deleting during logfile write test - filename \'%s\'\n' % outfname)
sys.exit(-1)
##
# Verify that the project name is valid
#
def VerifyProjectName(self, project):
factory = ProjectFactory(project)
if factory.Validate():
return True
return False