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