- bump version to 0.130.1
[opensuse:osc.git] / osc / conf.py
1 # Copyright (C) 2006-2009 Novell Inc.  All rights reserved.
2 # This program is free software; it may be used, copied, modified
3 # and distributed under the terms of the GNU General Public Licence,
4 # either version 2, or version 3 (at your option).
5
6 """Read osc configuration and store it in a dictionary
7
8 This module reads and parses ~/.oscrc. The resulting configuration is stored
9 for later usage in a dictionary named 'config'.
10 The .oscrc is kept mode 0600, so that it is not publically readable.
11 This gives no real security for storing passwords.
12 If in doubt, use your favourite keyring.
13 Password is stored on ~/.oscrc as bz2 compressed and base64 encoded, so that is fairly
14 large and not to be recognized or remembered easily by an occasional spectator.
15
16 If information is missing, it asks the user questions.
17
18 After reading the config, urllib2 is initialized.
19
20 The configuration dictionary could look like this:
21
22 {'apisrv': 'https://api.opensuse.org/',
23  'user': 'joe',
24  'api_host_options': {'api.opensuse.org': {'user': 'joe', 'pass': 'secret'},
25                       'apitest.opensuse.org': {'user': 'joe', 'pass': 'secret',
26                                                'http_headers':(('Host','api.suse.de'),
27                                                                ('User','faye'))},
28                       'foo.opensuse.org': {'user': 'foo', 'pass': 'foo'}},
29  'build-cmd': '/usr/bin/build',
30  'build-root': '/abuild/oscbuild-%(repo)s-%(arch)s',
31  'packagecachedir': '/var/cache/osbuild',
32  'su-wrapper': 'sudo',
33  }
34
35 """
36
37 import OscConfigParser
38 from osc import oscerr
39 from oscsslexcp import NoSecureSSLError
40 import os
41
42 GENERIC_KEYRING = False
43 GNOME_KEYRING = False
44
45 try:
46     import keyring
47     GENERIC_KEYRING = True
48
49 except:
50     try:
51         import gobject
52         gobject.set_application_name('osc')
53         import gnomekeyring
54         if os.environ['GNOME_DESKTOP_SESSION_ID']:
55             # otherwise gnome keyring bindings spit out errors, when you have
56             # it installed, but you are not under gnome
57             # (even though hundreds of gnome-keyring daemons got started in parallel)
58             # another option would be to support kwallet here
59             GNOME_KEYRING = gnomekeyring.is_available()
60     except:
61         pass
62
63 DEFAULTS = { 'apiurl': 'https://api.opensuse.org',
64              'user': 'your_username',
65              'pass': 'your_password',
66              'passx': '',
67              'packagecachedir': '/var/tmp/osbuild-packagecache',
68              'su-wrapper': 'su -c',
69
70              # build type settings
71              'build-cmd': '/usr/bin/build',
72              'build-type': '', # may be empty for chroot, kvm or xen
73              'build-root': '/var/tmp/build-root',
74              'build-uid': '', # use the default provided by build
75              'build-device': '', # required for VM builds
76              'build-memory': '',# required for VM builds
77              'build-swap': '',  # optional for VM builds
78              'build-vmdisk-rootsize': '', # optional for VM builds
79              'build-vmdisk-swapsize': '', # optional for VM builds
80
81              'build-jobs': os.sysconf('SC_NPROCESSORS_ONLN'), # compile with N jobs
82              'builtin_signature_check': '1', # by default use builtin check for verify pkgs
83              'icecream': '0',
84
85              'debug': '0',
86              'http_debug': '0',
87              'http_full_debug': '0',
88              'verbose': '1',
89              'traceback': '0',
90              'post_mortem': '0',
91              'use_keyring': '1',
92              'gnome_keyring': '1',
93              'cookiejar': '~/.osc_cookiejar',
94              # fallback for osc build option --no-verify
95              'no_verify': '0',
96              # enable project tracking by default
97              'do_package_tracking': '1',
98              # default for osc build
99              'extra-pkgs': '',
100              # default repository
101              'build_repository': 'openSUSE_Factory',
102              # default project for branch or bco
103              'getpac_default_project': 'openSUSE:Factory',
104              # alternate filesystem layout: have multiple subdirs, where colons were.
105              'checkout_no_colon': '0',
106              # local files to ignore with status, addremove, ....
107              # local files to ignore with status, addremove, ....
108              'exclude_glob': '.osc CVS .svn .* _linkerror *~ #*# *.orig *.bak *.changes.vctmp.*',
109              # keep passwords in plaintext. If you see this comment, your osc
110              # already uses the encrypted password, and only keeps them in plain text
111              # for backwards compatibility. Default will change to 0 in future releases.
112              'plaintext_passwd': '1',
113              # limit the age of requests shown with 'osc req list'.
114              # this is a default only, can be overridden by 'osc req list -D NNN'
115              # Use 0 for unlimted.
116              'request_list_days': 0,
117              # check for unversioned/removed files before commit
118              'check_filelist': '1',
119              # External scripts to validate sources, esp before commit. This is a directory
120              'source_validator_directory': '/usr/lib/osc/source_validators',
121              # check for pending requests after executing an action (e.g. checkout, update, commit)
122              'check_for_request_on_action': '0',
123              # what to do with the source package if the submitrequest has been accepted
124              'submitrequest_on_accept_action': '',
125              'request_show_interactive': '0',
126              'submitrequest_accepted_template': '',
127              'submitrequest_declined_template': '',
128              'linkcontrol': '0',
129
130              # Maintenance defaults to OBS instance defaults
131              'maintained_attribute': 'OBS:Maintained',
132              'maintained_update_project_attribute': 'OBS:UpdateProject',
133              'show_download_progress': '0',
134 }
135
136 # being global to this module, this dict can be accessed from outside
137 # it will hold the parsed configuration
138 config = DEFAULTS.copy()
139
140 boolean_opts = ['debug', 'do_package_tracking', 'http_debug', 'post_mortem', 'traceback', 'check_filelist', 'plaintext_passwd',
141     'checkout_no_colon', 'check_for_request_on_action', 'linkcontrol', 'show_download_progress', 'request_show_interactive',
142     'use_keyring', 'gnome_keyring', 'no_verify', 'builtin_signature_check', 'http_full_debug']
143
144 api_host_options = ['user', 'pass', 'passx', 'aliases', 'http_headers', 'email', 'sslcertck', 'cafile', 'capath', 'trusted_prj']
145
146 new_conf_template = """
147 [general]
148
149 # URL to access API server, e.g. %(apiurl)s
150 # you also need a section [%(apiurl)s] with the credentials
151 apiurl = %(apiurl)s
152
153 # Downloaded packages are cached here. Must be writable by you.
154 #packagecachedir = %(packagecachedir)s
155
156 # Wrapper to call build as root (sudo, su -, ...)
157 #su-wrapper = %(su-wrapper)s
158
159 # rootdir to setup the chroot environment
160 # can contain %%(repo)s, %%(arch)s, %%(project)s and %%(package)s for replacement, e.g.
161 # /srv/oscbuild/%%(repo)s-%%(arch)s or
162 # /srv/oscbuild/%%(repo)s-%%(arch)s-%%(project)s-%%(package)s
163 #build-root = %(build-root)s
164
165 # compile with N jobs (default: "getconf _NPROCESSORS_ONLN")
166 #build-jobs = N
167
168 # build-type to use - values can be (depending on the capabilities of the 'build' script)
169 # empty    -  chroot build
170 # kvm      -  kvm VM build  (needs build-device, build-swap, build-memory)
171 # xen      -  xen VM build  (needs build-device, build-swap, build-memory)
172 #   experimental:
173 #     qemu -  qemu VM build
174 #     lxc  -  lxc build
175 #build-type =
176
177 # build-device is the disk-image file to use as root for VM builds
178 # e.g. /var/tmp/FILE.root
179 #build-device = /var/tmp/FILE.root
180
181 # build-swap is the disk-image to use as swap for VM builds
182 # e.g. /var/tmp/FILE.swap
183 #build-swap = /var/tmp/FILE.swap
184
185 # build-memory is the amount of memory used in the VM
186 # value in MB - e.g. 512
187 #build-memory = 512
188
189 # build-vmdisk-rootsize is the size of the disk-image used as root in a VM build
190 # values in MB - e.g. 4096
191 #build-vmdisk-rootsize = 4096
192
193 # build-vmdisk-swapsize is the size of the disk-image used as swap in a VM build
194 # values in MB - e.g. 1024
195 #build-vmdisk-swapsize = 1024
196
197 # Numeric uid:gid to assign to the "abuild" user in the build-root
198 # or "caller" to use the current users uid:gid
199 # This is convenient when sharing the buildroot with ordinary userids
200 # on the host.
201 # This should not be 0
202 # build-uid =
203
204 # extra packages to install when building packages locally (osc build)
205 # this corresponds to osc build's -x option and can be overridden with that
206 # -x '' can also be given on the command line to override this setting, or
207 # you can have an empty setting here.
208 #extra-pkgs = vim gdb strace
209
210 # build platform is used if the platform argument is omitted to osc build
211 #build_repository = %(build_repository)s
212
213 # default project for getpac or bco
214 #getpac_default_project = %(getpac_default_project)s
215
216 # alternate filesystem layout: have multiple subdirs, where colons were.
217 #checkout_no_colon = %(checkout_no_colon)s
218
219 # local files to ignore with status, addremove, ....
220 #exclude_glob = %(exclude_glob)s
221
222 # keep passwords in plaintext. If you see this comment, your osc
223 # already uses the encrypted password, and only keeps them in plain text
224 # for backwards compatibility. Default will change to 0 in future releases.
225 # You can remove the plaintext password without harm, if you do not need
226 # backwards compatibility.
227 #plaintext_passwd = %(plaintext_passwd)s
228
229 # limit the age of requests shown with 'osc req list'.
230 # this is a default only, can be overridden by 'osc req list -D NNN'
231 # Use 0 for unlimted.
232 #request_list_days = %(request_list_days)s
233
234 # show info useful for debugging
235 #debug = 1
236
237 # show HTTP traffic useful for debugging
238 #http_debug = 1
239
240 # Skip signature verification of packages used for build.
241 #no_verify = 1
242
243 # jump into the debugger in case of errors
244 #post_mortem = 1
245
246 # print call traces in case of errors
247 #traceback = 1
248
249 # use KDE/Gnome/MacOS/Windows keyring for credentials if available
250 #use_keyring = 1
251
252 # check for unversioned/removed files before commit
253 #check_filelist = 1
254
255 # check for pending requests after executing an action (e.g. checkout, update, commit)
256 #check_for_request_on_action = 0
257
258 # what to do with the source package if the submitrequest has been accepted. If
259 # nothing is specified the API default is used
260 #submitrequest_on_accept_action = cleanup|update|noupdate
261
262 # template for an accepted submitrequest
263 #submitrequest_accepted_template = Hi %%(who)s,\\n
264 # thanks for working on:\\t%%(dst_project)s/%%(dst_package)s.
265 # SR %%(reqid)s has been accepted.\\n\\nYour maintainers
266
267 # template for a declined submitrequest
268 #submitrequest_declined_template = Hi %%(who)s,\\n
269 # sorry your SR %%(reqid)s (request type: %%(type)s) for
270 # %%(dst_project)s/%%(dst_package)s has been declined because...
271
272 #review requests interactively (default: off)
273 #request_show_review = 1
274
275 # Directory with executables to validate sources, esp before committing
276 #source_validator_directory = /usr/lib/osc/source_validators
277
278 [%(apiurl)s]
279 user = %(user)s
280 pass = %(pass)s
281 passx = %(passx)s
282 # set aliases for this apiurl
283 # aliases = foo, bar
284 # email used in .changes, unless the one from osc meta prj <user> will be used
285 # email =
286 # additional headers to pass to a request, e.g. for special authentication
287 #http_headers = Host: foofoobar,
288 #       User: mumblegack
289 # Force using of keyring for this API
290 #keyring = 1
291 """
292
293
294 account_not_configured_text ="""
295 Your user account / password are not configured yet.
296 You will be asked for them below, and they will be stored in
297 %s for future use.
298 """
299
300 config_incomplete_text = """
301
302 Your configuration file %s is not complete.
303 Make sure that it has a [general] section.
304 (You can copy&paste the below. Some commented defaults are shown.)
305
306 """
307
308 config_missing_apiurl_text = """
309 the apiurl \'%s\' does not exist in the config file. Please enter
310 your credentials for this apiurl.
311 """
312
313 cookiejar = None
314
315 def parse_apisrv_url(scheme, apisrv):
316     import urlparse
317     if apisrv.startswith('http://') or apisrv.startswith('https://'):
318         return urlparse.urlsplit(apisrv)[0:2]
319     elif scheme != None:
320         # the split/join is needed to get a proper url (e.g. without a trailing slash)
321         return urlparse.urlsplit(urljoin(scheme, apisrv))[0:2]
322     else:
323         from urllib2 import URLError
324         msg = 'invalid apiurl \'%s\' (specify the protocol (http:// or https://))' % apisrv
325         raise URLError(msg)
326
327 def urljoin(scheme, apisrv):
328     return '://'.join([scheme, apisrv])
329
330 def is_known_apiurl(url):
331     """returns true if url is a known apiurl"""
332     apiurl = urljoin(*parse_apisrv_url(None, url))
333     return config['api_host_options'].has_key(apiurl)
334
335 def get_apiurl_api_host_options(apiurl):
336     """
337     Returns all apihost specific options for the given apiurl, None if
338     no such specific optiosn exist.
339     """
340     # FIXME: in A Better World (tm) there was a config object which
341     # knows this instead of having to extract it from a url where it
342     # had been mingled into before.  But this works fine for now.
343
344     apiurl = urljoin(*parse_apisrv_url(None, apiurl))
345     if is_known_apiurl(apiurl):
346         return config['api_host_options'][apiurl]
347     raise oscerr.ConfigMissingApiurl('missing credentials for apiurl: \'%s\'' % apiurl,
348                                      '', apiurl)
349
350 def get_apiurl_usr(apiurl):
351     """
352     returns the user for this host - if this host does not exist in the
353     internal api_host_options the default user is returned.
354     """
355     # FIXME: maybe there should be defaults not just for the user but
356     # for all apihost specific options.  The ConfigParser class
357     # actually even does this but for some reason we don't use it
358     # (yet?).
359
360     import sys
361     try:
362         return get_apiurl_api_host_options(apiurl)['user']
363     except KeyError:
364         print >>sys.stderr, 'no specific section found in config file for host of [\'%s\'] - using default user: \'%s\'' \
365             % (apiurl, config['user'])
366         return config['user']
367
368 # workaround m2crypto issue:
369 # if multiple SSL.Context objects are created
370 # m2crypto only uses the last object which was created.
371 # So we need to build a new opener everytime we switch the
372 # apiurl (because different apiurls may have different
373 # cafile/capath locations)
374 def _build_opener(url):
375     from osc.core import __version__
376     import urllib2
377     import sys
378     global config
379     apiurl = urljoin(*parse_apisrv_url(None, url))
380     if not _build_opener.__dict__.has_key('last_opener'):
381         _build_opener.last_opener = (None, None)
382     if apiurl == _build_opener.last_opener[0]:
383         return _build_opener.last_opener[1]
384
385     # workaround for http://bugs.python.org/issue9639
386     authhandler_class = urllib2.HTTPBasicAuthHandler
387     if sys.version_info >= (2, 6, 6) and sys.version_info < (2, 7, 1) \
388         and not 'reset_retry_count' in dir(urllib2.HTTPBasicAuthHandler):
389         print >>sys.stderr, 'warning: your urllib2 version seems to be broken. ' \
390             'Using a workaround for http://bugs.python.org/issue9639'
391         class OscHTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
392             def http_error_401(self, *args):
393                 response = urllib2.HTTPBasicAuthHandler.http_error_401(self, *args)
394                 self.retried = 0
395                 return response
396
397         authhandler_class = OscHTTPBasicAuthHandler
398     elif sys.version_info >= (2, 6, 5) and sys.version_info < (2, 6, 6):
399         # workaround for broken urllib2 in python 2.6.5: wrong credentials
400         # lead to an infinite recursion
401         class OscHTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler):
402             def retry_http_basic_auth(self, host, req, realm):
403                 # don't retry if auth failed
404                 if req.get_header(self.auth_header, None) is not None:
405                     return None
406                 return urllib2.HTTPBasicAuthHandler.retry_http_basic_auth(self, host, req, realm)
407
408         authhandler_class = OscHTTPBasicAuthHandler
409
410     options = config['api_host_options'][apiurl]
411     # with None as first argument, it will always use this username/password
412     # combination for urls for which arg2 (apisrv) is a super-url
413     authhandler = authhandler_class( \
414         urllib2.HTTPPasswordMgrWithDefaultRealm())
415     authhandler.add_password(None, apiurl, options['user'], options['pass'])
416
417     if options['sslcertck']:
418         try:
419             import oscssl
420             from M2Crypto import m2urllib2
421         except ImportError, e:
422             print e
423             raise NoSecureSSLError('M2Crypto is needed to access %s in a secure way.\nPlease install python-m2crypto.' % apiurl)
424
425         cafile = options.get('cafile', None)
426         capath = options.get('capath', None)
427         if not cafile and not capath:
428             for i in ['/etc/pki/tls/cert.pem', '/etc/ssl/certs' ]:
429                 if os.path.isfile(i):
430                     cafile = i
431                     break
432                 elif os.path.isdir(i):
433                     capath = i
434                     break
435         ctx = oscssl.mySSLContext()
436         if ctx.load_verify_locations(capath=capath, cafile=cafile) != 1: raise Exception('No CA certificates found')
437         opener = m2urllib2.build_opener(ctx, oscssl.myHTTPSHandler(ssl_context = ctx, appname = 'osc'), urllib2.HTTPCookieProcessor(cookiejar), authhandler)
438     else:
439         import sys
440         print >>sys.stderr, "WARNING: SSL certificate checks disabled. Connection is insecure!\n"
441         opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar), authhandler)
442     opener.addheaders = [('User-agent', 'osc/%s' % __version__)]
443     _build_opener.last_opener = (apiurl, opener)
444     return opener
445
446 def init_basicauth(config):
447     """initialize urllib2 with the credentials for Basic Authentication"""
448
449     import cookielib
450     import urllib2
451     import sys
452     import httplib
453     def filterhdrs(meth, ishdr, *hdrs):
454         import re
455         import sys
456         import StringIO
457         # this is so ugly but httplib doesn't use
458         # a logger object or such
459         def new_method(*args, **kwargs):
460             stdout = sys.stdout
461             sys.stdout = StringIO.StringIO()
462             meth(*args, **kwargs)
463             hdr = sys.stdout.getvalue()
464             sys.stdout = stdout
465             for i in hdrs:
466                 if ishdr:
467                     hdr = re.sub(r'%s:[^\\r]*\\r\\n' % i, '', hdr)
468                 else:
469                     hdr = re.sub(i, '', hdr)
470             sys.stdout.write(hdr)
471         new_method.__name__ = meth.__name__
472         return new_method
473
474     if config['http_debug'] and not config['http_full_debug']:
475         httplib.HTTPConnection.send = filterhdrs(httplib.HTTPConnection.send, True, 'Cookie', 'Authorization')
476         httplib.HTTPResponse.begin = filterhdrs(httplib.HTTPResponse.begin, False, 'header: Set-Cookie.*\n')
477
478     if sys.version_info < (2, 6):
479         # HTTPS proxy is not supported in old urllib2. It only leads to an error
480         # or, at best, a warning.
481         if 'https_proxy' in os.environ:
482             del os.environ['https_proxy']
483         if 'HTTPS_PROXY' in os.environ:
484             del os.environ['HTTPS_PROXY']
485
486     if config['http_debug']:
487         # brute force
488         def urllib2_debug_init(self, debuglevel=0):
489             self._debuglevel = 1
490         urllib2.AbstractHTTPHandler.__init__ = urllib2_debug_init
491
492     cookie_file = os.path.expanduser(config['cookiejar'])
493     global cookiejar
494     cookiejar = cookielib.LWPCookieJar(cookie_file)
495     try:
496         cookiejar.load(ignore_discard=True)
497     except IOError:
498         try:
499             open(cookie_file, 'w').close()
500             os.chmod(cookie_file, 0600)
501         except:
502             #print 'Unable to create cookiejar file: \'%s\'. Using RAM-based cookies.' % cookie_file
503             cookiejar = cookielib.CookieJar()
504
505
506 def get_configParser(conffile=None, force_read=False):
507     """
508     Returns an ConfigParser() object. After its first invocation the
509     ConfigParser object is stored in a method attribute and this attribute
510     is returned unless you pass force_read=True.
511     """
512     conffile = conffile or os.environ.get('OSC_CONFIG', '~/.oscrc')
513     conffile = os.path.expanduser(conffile)
514     if not get_configParser.__dict__.has_key('conffile'):
515         get_configParser.conffile = conffile
516     if force_read or not get_configParser.__dict__.has_key('cp') or conffile != get_configParser.conffile:
517         get_configParser.cp = OscConfigParser.OscConfigParser(DEFAULTS)
518         get_configParser.cp.read(conffile)
519         get_configParser.conffile = conffile
520     return get_configParser.cp
521
522 def config_set_option(section, opt, val=None, delete=False, update=True, **kwargs):
523     """
524     Sets a config option. If val is not specified the current/default value is
525     returned. If val is specified, opt is set to val and the new value is returned.
526     If an option was modified get_config is called with **kwargs unless update is set
527     to False (override_conffile defaults to config['conffile']).
528     If val is not specified and delete is True then the option is removed from the
529     config/reset to the default value.
530     """
531     def write_config(fname, cp):
532         """write new configfile in a safe way"""
533         try:
534             f = open(fname + '.new', 'w')
535             cp.write(f, comments=True)
536             f.close()
537             os.rename(fname + '.new', fname)
538         except:
539             if os.path.exists(fname + '.new'):
540                 os.unlink(fname + '.new')
541             raise
542
543     cp = get_configParser(config['conffile'])
544     # don't allow "internal" options
545     general_opts = [i for i in DEFAULTS.keys() if not i in ['user', 'pass', 'passx']]
546     if section != 'general':
547         section = config['apiurl_aliases'].get(section, section)
548         scheme, host = \
549             parse_apisrv_url(config.get('scheme', 'https'), section)
550         section = urljoin(scheme, host)
551
552     sections = {}
553     for url in cp.sections():
554         if url == 'general':
555             sections[url] = url
556         else:
557             scheme, host = \
558                 parse_apisrv_url(config.get('scheme', 'https'), url)
559             apiurl = urljoin(scheme, host)
560             sections[apiurl] = url
561
562     section = sections.get(section.rstrip('/'), section)
563     if not section in cp.sections():
564         raise oscerr.ConfigError('unknown section \'%s\'' % section, config['conffile'])
565     if section == 'general' and not opt in general_opts or \
566        section != 'general' and not opt in api_host_options:
567         raise oscerr.ConfigError('unknown config option \'%s\'' % opt, config['conffile'])
568     run = False
569     if val:
570         cp.set(section, opt, val)
571         write_config(config['conffile'], cp)
572         run = True
573     elif delete and cp.has_option(section, opt):
574         cp.remove_option(section, opt)
575         write_config(config['conffile'], cp)
576         run = True
577     if run and update:
578         kw = {'override_conffile': config['conffile']}
579         kw.update(kwargs)
580         get_config(**kw)
581     if cp.has_option(section, opt):
582         return (opt, cp.get(section, opt, raw=True))
583     return (opt, None)
584
585 def write_initial_config(conffile, entries, custom_template = ''):
586     """
587     write osc's intial configuration file. entries is a dict which contains values
588     for the config file (e.g. { 'user' : 'username', 'pass' : 'password' } ).
589     custom_template is an optional configuration template.
590     """
591     import StringIO, sys, base64
592     conf_template = custom_template or new_conf_template
593     config = DEFAULTS.copy()
594     config.update(entries)
595     config['passx'] = base64.b64encode(config['pass'].encode('bz2'))
596     # at this point use_keyring and gnome_keyring are str objects
597     if config['use_keyring'] == '1' and GENERIC_KEYRING:
598         protocol, host = \
599             parse_apisrv_url(None, config['apiurl'])
600         keyring.set_password(host, config['user'], config['pass'])
601         config['pass'] = ''
602         config['passx'] = ''
603     elif config['gnome_keyring'] == '1' and GNOME_KEYRING:
604         protocol, host = \
605             parse_apisrv_url(None, config['apiurl'])
606         gnomekeyring.set_network_password_sync(
607             user = config['user'],
608             password = config['pass'],
609             protocol = protocol,
610             server = host)
611         config['user'] = ''
612         config['pass'] = ''
613         config['passx'] = ''
614     if not config['plaintext_passwd']:
615         config['pass'] = ''
616     sio = StringIO.StringIO(conf_template.strip() % config)
617     cp = OscConfigParser.OscConfigParser(DEFAULTS)
618     cp.readfp(sio)
619
620     file = None
621     try:
622         file = open(conffile, 'w')
623     except IOError, e:
624         raise oscerr.OscIOError(e, 'cannot open configfile \'%s\'' % conffile)
625     try:
626         try:
627             os.chmod(conffile, 0600)
628             cp.write(file, True)
629         except IOError, e:
630             raise oscerr.OscIOError(e, 'cannot write configfile \'s\'' % conffile)
631     finally:
632         if file: file.close()
633
634 def add_section(filename, url, user, passwd):
635     """
636     Add a section to config file for new api url.
637     """
638     import base64
639     global config
640     cp = get_configParser(filename)
641     try:
642         cp.add_section(url)
643     except OscConfigParser.ConfigParser.DuplicateSectionError:
644         # Section might have existed, but was empty
645         pass
646     if config['use_keyring'] and GENERIC_KEYRING:
647         protocol, host = \
648             parse_apisrv_url(None, url)
649         keyring.set_password(host, user, passwd)
650         cp.set(url, 'keyring', '1')
651         cp.set(url, 'user', user)
652         cp.remove_option(url, 'pass')
653         cp.remove_option(url, 'passx')
654     elif config['gnome_keyring'] and GNOME_KEYRING:
655         protocol, host = \
656             parse_apisrv_url(None, url)
657         gnomekeyring.set_network_password_sync(
658             user = user,
659             password = passwd,
660             protocol = protocol,
661             server = host)
662         cp.set(url, 'keyring', '1')
663         cp.remove_option(url, 'pass')
664         cp.remove_option(url, 'passx')
665     else:
666         cp.set(url, 'user', user)
667         if not config['plaintext_passwd']:
668             cp.remove_option(url, 'pass')
669         cp.set(url, 'passx', base64.b64encode(passwd.encode('bz2')))
670     file = open(filename, 'w')
671     cp.write(file, True)
672     if file: file.close()
673
674
675 def get_config(override_conffile = None,
676                override_apiurl = None,
677                override_debug = None,
678                override_http_debug = None,
679                override_http_full_debug = None,
680                override_traceback = None,
681                override_post_mortem = None,
682                override_no_keyring = None,
683                override_no_gnome_keyring = None,
684                override_verbose = None):
685     """do the actual work (see module documentation)"""
686     import sys
687     import re
688     global config
689
690     conffile = override_conffile or os.environ.get('OSC_CONFIG', '~/.oscrc')
691     conffile = os.path.expanduser(conffile)
692
693     if not os.path.exists(conffile):
694         raise oscerr.NoConfigfile(conffile, \
695                                   account_not_configured_text % conffile)
696
697     # okay, we made sure that .oscrc exists
698
699     # make sure it is not world readable, it may contain a password.
700     os.chmod(conffile, 0600)
701
702     cp = get_configParser(conffile)
703
704     if not cp.has_section('general'):
705         # FIXME: it might be sufficient to just assume defaults?
706         msg = config_incomplete_text % conffile
707         msg += new_conf_template % DEFAULTS
708         raise oscerr.ConfigError(msg, conffile)
709
710     config = dict(cp.items('general', raw=1))
711     config['conffile'] = conffile
712
713     for i in boolean_opts:
714         try:
715             config[i] = cp.getboolean('general', i)
716         except ValueError, e:
717             raise oscerr.ConfigError('cannot parse \'%s\' setting: ' % i + str(e), conffile)
718
719     config['packagecachedir'] = os.path.expanduser(config['packagecachedir'])
720     config['exclude_glob'] = config['exclude_glob'].split()
721
722     re_clist = re.compile('[, ]+')
723     config['extra-pkgs'] = [ i.strip() for i in re_clist.split(config['extra-pkgs'].strip()) if i ]
724
725     # collect the usernames, passwords and additional options for each api host
726     api_host_options = {}
727
728     # Regexp to split extra http headers into a dictionary
729     # the text to be matched looks essentially looks this:
730     # "Attribute1: value1, Attribute2: value2, ..."
731     # there may be arbitray leading and intermitting whitespace.
732     # the following regexp does _not_ support quoted commas within the value.
733     http_header_regexp = re.compile(r"\s*(.*?)\s*:\s*(.*?)\s*(?:,\s*|\Z)")
734
735     # override values which we were called with
736     # This needs to be done before processing API sections as it might be already used there
737     if override_no_keyring:
738         config['use_keyring'] = False
739     if override_no_gnome_keyring:
740         config['gnome_keyring'] = False
741
742     aliases = {}
743     for url in [ x for x in cp.sections() if x != 'general' ]:
744         # backward compatiblity
745         scheme, host = \
746             parse_apisrv_url(config.get('scheme', 'https'), url)
747         apiurl = urljoin(scheme, host)
748         user = None
749         if config['use_keyring'] and GENERIC_KEYRING:
750             try:
751                 # Read from keyring lib if available
752                 user = cp.get(url, 'user', raw=True)
753                 password = keyring.get_password(host, user)
754             except:
755                 # Fallback to file based auth.
756                 pass
757         elif config['gnome_keyring'] and GNOME_KEYRING:
758             # Read from gnome keyring if available
759             try:
760                 gk_data = gnomekeyring.find_network_password_sync(
761                     protocol = scheme,
762                     server = host)
763                 password = gk_data[0]['password']
764                 user = gk_data[0]['user']
765             except gnomekeyring.NoMatchError:
766                 # Fallback to file based auth.
767                 pass
768         # Read credentials from config
769         if user is None:
770             #FIXME: this could actually be the ideal spot to take defaults
771             #from the general section.
772             user         = cp.get(url, 'user', raw=True) # need to set raw to prevent '%' expansion
773             password     = cp.get(url, 'pass', raw=True) # especially on password!
774             passwordx    = cp.get(url, 'passx', raw=True) # especially on password!
775             if password is None or password == 'your_password':
776                 try:
777                     password = passwordx.decode('base64').decode('bz2')
778                 except:
779                     print "%s: no credentials known" % url
780                     password = 'your_password'
781             elif password != passwordx.decode('base64').decode('bz2'):
782                 if not passwordx:
783                     # passx not present
784                     print '%s: rewriting from plain pass to encoded pass\n' % url
785                 else:
786                     print '%s: pass and passx mismatch (rewriting from plain pass to encoded pass)\n' % url
787                 add_section(conffile, url, user, password)
788
789         if cp.has_option(url, 'http_headers'):
790             http_headers = cp.get(url, 'http_headers')
791             http_headers = http_header_regexp.findall(http_headers)
792         else:
793             http_headers = []
794         if cp.has_option(url, 'aliases'):
795             for i in cp.get(url, 'aliases').split(','):
796                 key = i.strip()
797                 if key == '':
798                     continue
799                 if aliases.has_key(key):
800                     msg = 'duplicate alias entry: \'%s\' is already used for another apiurl' % key
801                     raise oscerr.ConfigError(msg, conffile)
802                 aliases[key] = url
803
804         api_host_options[apiurl] = { 'user': user,
805                                      'pass': password,
806                                      'http_headers': http_headers}
807
808         optional = ('email', 'sslcertck', 'cafile', 'capath')
809         for key in optional:
810             if cp.has_option(url, key):
811                 if key == 'sslcertck':
812                     api_host_options[apiurl][key] = cp.getboolean(url, key)
813                 else:
814                     api_host_options[apiurl][key] = cp.get(url, key)
815
816         if not 'sslcertck' in api_host_options[apiurl]:
817             api_host_options[apiurl]['sslcertck'] = True
818
819         if scheme == 'http':
820             api_host_options[apiurl]['sslcertck'] = False
821
822         if cp.has_option(url, 'trusted_prj'):
823             api_host_options[apiurl]['trusted_prj'] = cp.get(url, 'trusted_prj').split(' ')
824         else:
825             api_host_options[apiurl]['trusted_prj'] = []
826
827     # add the auth data we collected to the config dict
828     config['api_host_options'] = api_host_options
829     config['apiurl_aliases'] = aliases
830
831     apiurl = aliases.get(config['apiurl'], config['apiurl'])
832     config['apiurl'] = urljoin(*parse_apisrv_url(None, apiurl))
833     # backward compatibility
834     if config.has_key('apisrv'):
835         apisrv = config['apisrv'].lstrip('http://')
836         apisrv = apisrv.lstrip('https://')
837         scheme = config.get('scheme', 'https')
838         config['apiurl'] = urljoin(scheme, apisrv)
839     if config.has_key('apisrv') or config.has_key('scheme'):
840         print >>sys.stderr, 'Warning: Use of the \'scheme\' or \'apisrv\' in ~/.oscrc is deprecated!\n' \
841                             'Warning: See README for migration details.'
842     if config.has_key('build_platform'):
843         print >>sys.stderr, 'Warning: Use of \'build_platform\' config option is deprecated! (use \'build_repository\' instead)'
844         config['build_repository'] = config['build_platform']
845
846     config['verbose'] = int(config['verbose'])
847     # override values which we were called with
848     if override_verbose:
849         config['verbose'] = override_verbose + 1
850
851     if override_debug:
852         config['debug'] = override_debug
853     if override_http_debug:
854         config['http_debug'] = override_http_debug
855     if override_http_full_debug:
856         config['http_debug'] = override_http_full_debug or config['http_debug']
857         config['http_full_debug'] = override_http_full_debug
858     if override_traceback:
859         config['traceback'] = override_traceback
860     if override_post_mortem:
861         config['post_mortem'] = override_post_mortem
862     if override_apiurl:
863         apiurl = aliases.get(override_apiurl, override_apiurl)
864         # check if apiurl is a valid url
865         config['apiurl'] = urljoin(*parse_apisrv_url(None, apiurl))
866
867     # XXX unless config['user'] goes away (and is replaced with a handy function, or
868     # config becomes an object, even better), set the global 'user' here as well,
869     # provided that there _are_ credentials for the chosen apiurl:
870     try:
871         config['user'] = get_apiurl_usr(config['apiurl'])
872     except oscerr.ConfigMissingApiurl, e:
873         e.msg = config_missing_apiurl_text % config['apiurl']
874         e.file = conffile
875         raise e
876
877     # finally, initialize urllib2 for to use the credentials for Basic Authentication
878     init_basicauth(config)
879
880 # vim: sw=4 et