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