conf.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. #!/usr/bin/python -tt
  2. # vim: ai ts=4 sts=4 et sw=4
  3. #
  4. # Copyright (c) 2011 Intel, Inc.
  5. #
  6. # This program is free software; you can redistribute it and/or modify it
  7. # under the terms of the GNU General Public License as published by the Free
  8. # Software Foundation; version 2 of the License
  9. #
  10. # This program is distributed in the hope that it will be useful, but
  11. # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  12. # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  13. # for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License along
  16. # with this program; if not, write to the Free Software Foundation, Inc., 59
  17. # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  18. '''
  19. Provides classes and functions to read and write gbs.conf.
  20. '''
  21. from __future__ import with_statement
  22. import os
  23. import re
  24. import base64
  25. import shutil
  26. from collections import namedtuple
  27. from ConfigParser import SafeConfigParser, NoSectionError, \
  28. MissingSectionHeaderError, Error
  29. from gitbuildsys import errors
  30. from gitbuildsys.safe_url import SafeURL
  31. from gitbuildsys.utils import Temp
  32. from gitbuildsys.log import LOGGER as log
  33. def decode_passwdx(passwdx):
  34. '''decode passwdx into plain format'''
  35. return base64.b64decode(passwdx).decode('bz2')
  36. def encode_passwd(passwd):
  37. '''encode passwd by bz2 and base64'''
  38. return base64.b64encode(passwd.encode('bz2'))
  39. class BrainConfigParser(SafeConfigParser):
  40. """Standard ConfigParser derived class which can reserve most of the
  41. comments, indents, and other user customized stuff inside the ini file.
  42. """
  43. def read_one(self, filename):
  44. """only support one input file"""
  45. return SafeConfigParser.read(self, filename)
  46. def _read(self, fptr, fname):
  47. """Parse a sectioned setup file.
  48. Override the same method of parent class.
  49. Customization: save filename and file contents
  50. """
  51. # save the original filepath and contents
  52. self._fpname = fname
  53. self._flines = fptr.readlines()
  54. fptr.seek(0)
  55. return SafeConfigParser._read(self, fptr, fname)
  56. def _set_into_file(self, section, option, value, replace_opt=None):
  57. """Set the value in the file contents
  58. Parsing logic and lot of the code was copied directly from the
  59. ConfigParser module of Python standard library.
  60. """
  61. cursect = None # None, or a str
  62. optname = None
  63. new_line = '%s = %s\n' % (option, value)
  64. new_line_written = False
  65. last_section_line = None
  66. for lineno in range(len(self._flines)):
  67. line = self._flines[lineno]
  68. # We might have 'None' lines because of earlier updates
  69. if line is None:
  70. continue
  71. # comment or blank line?
  72. if line.strip() == '' or line[0] in '#;':
  73. continue
  74. if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
  75. # no leading whitespace
  76. continue
  77. # continuation line?
  78. if line[0].isspace() and cursect == section and \
  79. (optname == option or optname == replace_opt):
  80. self._flines[lineno] = None
  81. else:
  82. # is it a section header?
  83. match = self.SECTCRE.match(line)
  84. if match:
  85. cursect = match.group('header')
  86. # So sections can't start with a continuation line
  87. optname = None
  88. # no section header in the file?
  89. elif cursect is None:
  90. raise MissingSectionHeaderError(self._fpname,
  91. lineno + 1, line)
  92. # an option line?
  93. else:
  94. match = self.OPTCRE.match(line)
  95. if match:
  96. optname = match.group('option')
  97. optname = self.optionxform(optname.rstrip())
  98. # Replace / remove options
  99. if cursect == section and \
  100. (optname == option or optname == replace_opt):
  101. if not new_line_written:
  102. self._flines[lineno] = new_line
  103. new_line_written = True
  104. else:
  105. # Just remove all matching lines, if we've
  106. # already written the new value
  107. self._flines[lineno] = None
  108. # Just ignore non-fatal parsing errors
  109. # Save the last line of the matching section
  110. if cursect == section:
  111. last_section_line = lineno
  112. # Insert new key
  113. if not new_line_written:
  114. if last_section_line is not None:
  115. self._flines.insert(last_section_line + 1, new_line)
  116. else:
  117. raise NoSectionError(section)
  118. def set_into_file(self, section, option, value, replace_opt=None):
  119. """When set new value, need to update the readin file lines,
  120. which can be saved back to file later.
  121. """
  122. try:
  123. SafeConfigParser.set(self, section, option, value)
  124. if replace_opt:
  125. SafeConfigParser.remove_option(self, section, replace_opt)
  126. except NoSectionError, err:
  127. raise errors.ConfigError(str(err))
  128. # If the code reach here, it means the section and key are ok
  129. try:
  130. self._set_into_file(section, option, value, replace_opt)
  131. except Exception as err:
  132. # This really shouldn't happen, we've already once parsed the file
  133. # contents successfully.
  134. raise errors.ConfigError('BUG: ' + str(err))
  135. def update(self):
  136. """Update the original config file using updated values"""
  137. if self._fpname == '<???>':
  138. return
  139. with open(self._fpname, 'w') as fptr:
  140. buf = ''.join([ line for line in self._flines if line is not None ])
  141. fptr.write(buf)
  142. class ConfigMgr(object):
  143. '''Support multi-levels of gbs.conf. Use this class to get and set
  144. item value without caring about concrete ini format'''
  145. DEFAULTS = {
  146. 'general': {
  147. 'tmpdir': '/var/tmp',
  148. 'editor': '',
  149. 'packaging_branch': 'master',
  150. 'upstream_branch': 'upstream',
  151. 'upstream_tag': 'upstream/${upstreamversion}',
  152. 'squash_patches_until': '',
  153. 'buildroot': '~/GBS-ROOT/',
  154. 'packaging_dir': 'packaging',
  155. 'work_dir': '.',
  156. },
  157. }
  158. DEFAULT_CONF_TEMPLATE = '''[general]
  159. #Current profile name which should match a profile section name
  160. profile = profile.tizen
  161. [profile.tizen]
  162. #Common authentication info for whole profile
  163. #user =
  164. #CAUTION: please use the key name "passwd" to reset plaintext password
  165. #passwd =
  166. obs = obs.tizen
  167. #Comma separated list of repositories
  168. repos = repo.tizen_latest
  169. #repos = repo.tizen_main, repo.tizen_base
  170. [obs.tizen]
  171. #OBS API URL pointing to a remote OBS.
  172. url = https://api.tizen.org
  173. #Optional user and password, set if differ from profile's user and password
  174. #user =
  175. #passwd =
  176. #Repo section example
  177. [repo.tizen_latest]
  178. #Build against repo's URL
  179. url = http://download.tizen.org/releases/daily/trunk/ivi/latest/
  180. #Optional user and password, set if differ from profile's user and password
  181. #user =
  182. #passwd =
  183. #Individual repo is also supported
  184. #[repo.tizen_base]
  185. #url = http://download.tizen.org/releases/daily/trunk/ivi/latest/repos/base/ia32/packages/
  186. #[repo.tizen_main]
  187. #url = http://download.tizen.org/releases/daily/trunk/ivi/latest/repos/ivi/ia32/packages/
  188. '''
  189. # make the manager class as singleton
  190. _instance = None
  191. def __new__(cls, *args, **kwargs):
  192. if not cls._instance:
  193. cls._instance = super(ConfigMgr, cls).__new__(cls, *args, **kwargs)
  194. return cls._instance
  195. def __init__(self, fpath=None):
  196. self._cfgfiles = []
  197. self._cfgparsers = []
  198. if fpath:
  199. if not os.path.exists(fpath):
  200. raise errors.ConfigError('Configuration file %s does not '\
  201. 'exist' % fpath)
  202. self._cfgfiles.append(fpath)
  203. # find the default path
  204. fpaths = self._lookfor_confs()
  205. if not fpaths:
  206. self._new_conf()
  207. fpaths = self._lookfor_confs()
  208. self._cfgfiles.extend(fpaths)
  209. self.load_confs()
  210. def _create_default_parser(self):
  211. 'create a default parser that handle DEFAULTS values'
  212. parser = BrainConfigParser()
  213. for sec, options in self.DEFAULTS.iteritems():
  214. parser.add_section(sec)
  215. for key, val in options.iteritems():
  216. parser.set(sec, key, val)
  217. return parser
  218. def load_confs(self):
  219. 'reset all config values by files passed in'
  220. self._cfgparsers = []
  221. for fpath in self._cfgfiles:
  222. cfgparser = BrainConfigParser()
  223. try:
  224. cfgparser.read_one(fpath)
  225. if cfgparser.has_section('general') and \
  226. cfgparser.has_option('general', 'work_dir') and \
  227. cfgparser.get('general', 'work_dir') == '.':
  228. cfgparser.set('general', 'work_dir',
  229. os.path.abspath(os.path.dirname(fpath)))
  230. except Error, err:
  231. raise errors.ConfigError('config file error:%s' % err)
  232. self._cfgparsers.append(cfgparser)
  233. self._cfgparsers.append(self._create_default_parser())
  234. self._check_passwd()
  235. def add_conf(self, fpath):
  236. """ Add new config to configmgr, and new added config file has
  237. highest priority
  238. """
  239. if not fpath:
  240. return
  241. if not os.path.exists(fpath):
  242. raise errors.ConfigError('Configuration file %s does not '\
  243. 'exist' % fpath)
  244. # new added conf has highest priority
  245. self._cfgfiles.insert(0, fpath)
  246. # reload config files
  247. self.load_confs()
  248. @staticmethod
  249. def _lookfor_confs():
  250. """Look for available config files following the order:
  251. > Current project
  252. > User
  253. > System
  254. """
  255. paths = []
  256. def lookfor_tizen_conf(start_dir):
  257. """ Search topdir of tizen source code cloned using repo tool,
  258. if .gbs.conf exists under that dir, then return it
  259. """
  260. cur_dir = os.path.abspath(start_dir)
  261. while True:
  262. if os.path.exists(os.path.join(cur_dir, '.repo')) and \
  263. os.path.exists(os.path.join(cur_dir, '.gbs.conf')):
  264. return os.path.join(cur_dir, '.gbs.conf')
  265. if cur_dir == '/':
  266. break
  267. cur_dir = os.path.dirname(cur_dir)
  268. return None
  269. tizen_conf = lookfor_tizen_conf(os.getcwd())
  270. if tizen_conf:
  271. paths.append(tizen_conf)
  272. for path in (os.path.abspath('.gbs.conf'),
  273. os.path.expanduser('~/.gbs.conf'),
  274. '/etc/gbs.conf'):
  275. if os.path.exists(path) and path not in paths:
  276. paths.append(path)
  277. return paths
  278. def _new_conf(self):
  279. 'generate a default conf file in home dir'
  280. fpath = os.path.expanduser('~/.gbs.conf')
  281. with open(fpath, 'w') as wfile:
  282. wfile.write(self.DEFAULT_CONF_TEMPLATE)
  283. os.chmod(fpath, 0600)
  284. log.warning('Created a new config file %s. Please check and edit '
  285. 'your authentication information.' % fpath)
  286. def _check_passwd(self):
  287. 'convert passwd item to passwdx and then update origin conf files'
  288. dirty = set()
  289. all_sections = set()
  290. for layer in self._cfgparsers:
  291. for sec in layer.sections():
  292. all_sections.add(sec)
  293. for sec in all_sections:
  294. for key in self.options(sec):
  295. if key.endswith('passwd'):
  296. for cfgparser in self._cfgparsers:
  297. if cfgparser.has_option(sec, key):
  298. plainpass = cfgparser.get(sec, key)
  299. if plainpass is None:
  300. # empty string password is acceptable here
  301. continue
  302. cfgparser.set_into_file(sec,
  303. key + 'x',
  304. encode_passwd(plainpass),
  305. key)
  306. dirty.add(cfgparser)
  307. if dirty:
  308. log.warning('plaintext password in config files will '
  309. 'be replaced by encoded ones')
  310. self.update(dirty)
  311. def _get(self, opt, section='general'):
  312. 'get value from multi-levels of config file'
  313. for cfgparser in self._cfgparsers:
  314. try:
  315. return cfgparser.get(section, opt)
  316. except Error, err:
  317. pass
  318. raise errors.ConfigError(err)
  319. def options(self, section='general'):
  320. 'merge and return options of certain section from multi-levels'
  321. sect_found = False
  322. options = set()
  323. for cfgparser in self._cfgparsers:
  324. try:
  325. options.update(cfgparser.options(section))
  326. sect_found = True
  327. except Error, err:
  328. pass
  329. if not sect_found:
  330. raise errors.ConfigError(err)
  331. return options
  332. def has_section(self, section):
  333. 'indicate whether a section exists'
  334. for parser in self._cfgparsers:
  335. if parser.has_section(section):
  336. return True
  337. return False
  338. def get(self, opt, section='general'):
  339. 'get item value. return plain text of password if item is passwd'
  340. if opt == 'passwd':
  341. val = self._get('passwdx', section)
  342. try:
  343. return decode_passwdx(val)
  344. except (TypeError, IOError), err:
  345. raise errors.ConfigError('passwdx:%s' % err)
  346. else:
  347. return self._get(opt, section)
  348. def get_arg_conf(self, args, opt, section='general'):
  349. """get value from command line arguments if found there, otherwise fall
  350. back to config
  351. """
  352. if hasattr(args, opt):
  353. value = getattr(args, opt)
  354. if value is not None:
  355. return value
  356. return self.get(opt, section)
  357. @staticmethod
  358. def update(cfgparsers):
  359. 'update changed values into files on disk'
  360. for cfgparser in cfgparsers:
  361. try:
  362. cfgparser.update()
  363. except IOError, err:
  364. log.warning('update config file error: %s' % err)
  365. URL = namedtuple('URL', 'url user password')
  366. class SectionConf(object):
  367. """Config items related to obs and repo sections."""
  368. def __init__(self, parent, name, url, base=None, target=None):
  369. self.parent = parent
  370. self.name = name
  371. self.base = base
  372. self.target = target
  373. user = url.user or parent.common_user
  374. password = url.password or parent.common_password
  375. try:
  376. self.url = SafeURL(url.url, user, password)
  377. except ValueError, err:
  378. raise errors.ConfigError('%s for %s' % (str(err), url.url))
  379. def dump(self, fhandler):
  380. """Dump ini to file object."""
  381. parser = BrainConfigParser()
  382. parser.add_section(self.name)
  383. parser.set(self.name, 'url', self.url)
  384. if self.url.user and self.url.user != self.parent.common_user:
  385. parser.set(self.name, 'user', self.url.user)
  386. if self.url.passwd and self.url.passwd != self.parent.common_password:
  387. parser.set(self.name, 'passwdx',
  388. encode_passwd(self.url.passwd))
  389. if self.base:
  390. parser.set(self.name, 'base_prj', self.base)
  391. if self.target:
  392. parser.set(self.name, 'target_prj', self.target)
  393. parser.write(fhandler)
  394. class Profile(object):
  395. '''Profile which contains all config values related to same domain'''
  396. def __init__(self, name, user, password):
  397. self.name = name
  398. self.common_user = user
  399. self.common_password = password
  400. self.repos = []
  401. self.obs = None
  402. self.buildroot = None
  403. self.buildconf = None
  404. def add_repo(self, repoconf):
  405. '''add a repo to repo list of the profile'''
  406. self.repos.append(repoconf)
  407. def set_obs(self, obsconf):
  408. '''set OBS api of the profile'''
  409. self.obs = obsconf
  410. def dump(self, fhandler):
  411. 'dump ini to file object'
  412. parser = BrainConfigParser()
  413. parser.add_section(self.name)
  414. if self.common_user:
  415. parser.set(self.name, 'user', self.common_user)
  416. if self.common_password:
  417. parser.set(self.name, 'passwdx',
  418. encode_passwd(self.common_password))
  419. if self.buildroot:
  420. parser.set(self.name, 'buildroot', self.buildroot)
  421. if self.obs:
  422. parser.set(self.name, 'obs', self.obs.name)
  423. self.obs.dump(fhandler)
  424. if self.repos:
  425. names = []
  426. for repo in self.repos:
  427. names.append(repo.name)
  428. repo.dump(fhandler)
  429. parser.set(self.name, 'repos', ', '.join(names))
  430. parser.write(fhandler)
  431. class BizConfigManager(ConfigMgr):
  432. '''config manager which handles high level conception, such as profile info
  433. '''
  434. def _interpolate(self, value):
  435. '''do string interpolation'''
  436. general_keys = {}
  437. for opt in self.DEFAULTS['general']:
  438. if opt == 'work_dir' and self.get(opt, 'general') == '.':
  439. general_keys[opt] = os.getcwd()
  440. else:
  441. general_keys[opt] = self.get(opt, 'general')
  442. value = re.sub(r'\$\{([^}]+)\}', r'%(\1)s', value)
  443. try:
  444. value = value % general_keys
  445. except KeyError, err:
  446. raise errors.ConfigError('unknown key: %s. Supportted '\
  447. 'keys are %s' % (str(err), ' '.join( \
  448. self.DEFAULTS['general'].keys())))
  449. return value
  450. def is_profile_oriented(self):
  451. '''return True if config file is profile oriented'''
  452. return self.get_optional_item('general', 'profile') is not None
  453. def get_current_profile(self):
  454. '''get profile current used'''
  455. if self.is_profile_oriented():
  456. return self.build_profile_by_name(self.get('profile'))
  457. profile = self._build_profile_by_subcommand()
  458. self.convert_to_new_style(profile)
  459. return profile
  460. def convert_to_new_style(self, profile):
  461. 'convert ~/.gbs.conf to new style'
  462. def dump_general(fhandler):
  463. 'dump options in general section'
  464. parser = BrainConfigParser()
  465. parser.add_section('general')
  466. parser.set('general', 'profile', profile.name)
  467. for opt in self.options('general'):
  468. val = self.get(opt)
  469. if val != self.DEFAULTS['general'].get(opt):
  470. parser.set('general', opt, val)
  471. parser.write(fhandler)
  472. fname = '~/.gbs.conf.template'
  473. try:
  474. tmp = Temp()
  475. with open(tmp.path, 'w') as fhandler:
  476. dump_general(fhandler)
  477. profile.dump(fhandler)
  478. shutil.move(tmp.path, os.path.expanduser(fname))
  479. except IOError, err:
  480. raise errors.ConfigError(err)
  481. log.warning('subcommand oriented style of config is deprecated. '
  482. 'Please check %s, a new profile oriented style of config which'
  483. ' was converted from your current settings.' % fname)
  484. def get_optional_item(self, section, option, default=None):
  485. '''return default if section.option does not exist'''
  486. try:
  487. return self.get(option, section)
  488. except errors.ConfigError:
  489. return default
  490. def _get_url_options(self, section_id):
  491. '''get url/user/passwd from a section'''
  492. url = os.path.expanduser(self._interpolate(self.get('url', section_id)))
  493. user = self.get_optional_item(section_id, 'user')
  494. password = self.get_optional_item(section_id, 'passwd')
  495. return URL(url, user, password)
  496. def build_profile_by_name(self, name):
  497. '''return profile object by a given section'''
  498. if not name.startswith('profile.'):
  499. raise errors.ConfigError('section name specified by general.profile'
  500. ' must start with string "profile.": %s' % name)
  501. if not self.has_section(name):
  502. raise errors.ConfigError('no such section: %s' % name)
  503. user = self.get_optional_item(name, 'user')
  504. password = self.get_optional_item(name, 'passwd')
  505. profile = Profile(name, user, password)
  506. obs = self.get_optional_item(name, 'obs')
  507. if obs:
  508. if not obs.startswith('obs.'):
  509. raise errors.ConfigError('obs section name should start '
  510. 'with string "obs.": %s' % obs)
  511. obsconf = SectionConf(profile, obs,
  512. self._get_url_options(obs),
  513. self.get_optional_item(obs, 'base_prj'),
  514. self.get_optional_item(obs, 'target_prj'))
  515. profile.set_obs(obsconf)
  516. repos = self.get_optional_item(name, 'repos')
  517. if repos:
  518. for repo in repos.split(','):
  519. repo = repo.strip()
  520. if not repo.startswith('repo.'):
  521. log.warning('ignore %s, repo section name should start '
  522. 'with string "repo."' % repo)
  523. continue
  524. repoconf = SectionConf(profile, repo,
  525. self._get_url_options(repo))
  526. profile.add_repo(repoconf)
  527. profile.buildroot = self.get_optional_item(name, 'buildroot')
  528. if self.get_optional_item(name, 'buildconf'):
  529. profile.buildconf = os.path.expanduser(self._interpolate(
  530. self.get_optional_item(name,
  531. 'buildconf')))
  532. return profile
  533. def _parse_build_repos(self):
  534. """
  535. Make list of urls using repox.url, repox.user and repox.passwd
  536. configuration file parameters from 'build' section.
  537. Validate configuration parameters.
  538. """
  539. repos = {}
  540. # get repo settings form build section
  541. for opt in self.options('build'):
  542. if opt.startswith('repo'):
  543. try:
  544. key, name = opt.split('.')
  545. except ValueError:
  546. raise errors.ConfigError("invalid repo option: %s" % opt)
  547. if name not in ('url', 'user', 'passwdx'):
  548. raise errors.ConfigError("invalid repo option: %s" % opt)
  549. if key not in repos:
  550. repos[key] = {}
  551. if name in repos[key]:
  552. raise errors.ConfigError('Duplicate entry %s' % opt)
  553. value = self.get(opt, 'build')
  554. if name == 'passwdx':
  555. try:
  556. value = decode_passwdx(value)
  557. except (TypeError, IOError), err:
  558. raise errors.ConfigError('Error decoding %s: %s' % \
  559. (opt, err))
  560. repos[key]['passwd'] = value
  561. else:
  562. repos[key][name] = value
  563. return sorted(repos.items(), key=lambda i: i[0])
  564. def _build_profile_by_subcommand(self):
  565. '''return profile object from subcommand oriented style of config'''
  566. profile = Profile('profile.current', None, None)
  567. sec = 'remotebuild'
  568. addr = self.get_optional_item(sec, 'build_server')
  569. if addr:
  570. user = self.get_optional_item(sec, 'user')
  571. password = self.get_optional_item(sec, 'passwd')
  572. url = URL(addr, user, password)
  573. obsconf = SectionConf(profile, 'obs.%s' % sec, url,
  574. self.get_optional_item('remotebuild', 'base_prj'),
  575. self.get_optional_item('remotebuild', 'target_prj'))
  576. profile.set_obs(obsconf)
  577. repos = self._parse_build_repos()
  578. for key, item in repos:
  579. if 'url' not in item:
  580. raise errors.ConfigError("URL is not specified for %s" % key)
  581. url = URL(item['url'], item.get('user'), item.get('passwd'))
  582. repoconf = SectionConf(profile, 'repo.%s' % key, url)
  583. profile.add_repo(repoconf)
  584. return profile
  585. configmgr = BizConfigManager()