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