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