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