~ubuntu-branches/ubuntu/trusty/jockey/trusty

« back to all changes in this revision

Viewing changes to plain

  • Committer: Package Import Robot
  • Author(s): Jonathan Riddell, Olivier van der Toorn
  • Date: 2013-09-10 16:25:49 UTC
  • Revision ID: package-import@ubuntu.com-20130910162549-xuf6m5y2ic8xcx2u
Tags: 0.9.7-0ubuntu14
[ Olivier van der Toorn ]
jockey/oslib.py Fixes for current apt API

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# (c) 2007 Canonical Ltd.
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License along
 
15
# with this program; if not, write to the Free Software Foundation, Inc.,
 
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
17
 
 
18
'''Encapsulate operations which are Linux distribution specific.'''
 
19
 
 
20
import fcntl, os, subprocess, sys, logging, re, tempfile, time, shutil
 
21
from glob import glob
 
22
 
 
23
import warnings
 
24
warnings.simplefilter('ignore', FutureWarning)
 
25
import apt
 
26
 
 
27
class _CapturedInstallProgress(apt.progress.base.InstallProgress):
 
28
    def __init__(self):
 
29
        apt.progress.base.InstallProgress().__init__(self)
 
30
        self.out = None
 
31
 
 
32
    def fork(self):
 
33
        '''Reroute stdout/stderr to files, so that we can log them'''
 
34
 
 
35
        self.out = tempfile.TemporaryFile()
 
36
        p = os.fork()
 
37
        if p == 0:
 
38
            os.dup2(self.out.fileno(), sys.stdout.fileno())
 
39
            os.dup2(self.out.fileno(), sys.stderr.fileno())
 
40
        return p
 
41
 
 
42
class OSLib:
 
43
    '''Encapsulation of operating system/Linux distribution specific operations.'''
 
44
 
 
45
    # global default instance
 
46
    inst = None
 
47
 
 
48
    def __init__(self, client_only=False, target_kernel=None):
 
49
        '''Set default paths and load the module blacklist.
 
50
        
 
51
        Distributors might want to override some default paths.
 
52
        If client_only is True, this only initializes functionality which is
 
53
        needed by clients, and which can be done without special privileges.
 
54
 
 
55
        If target_kernel is given, it is used instead of the default
 
56
        os.uname()[2]. This is primarily useful for distribution installers
 
57
        where the target system kernel differs from the installer kernel.
 
58
        '''
 
59
        # relevant stuff for clients and backend
 
60
        self._get_os_version()
 
61
 
 
62
        # /sys/ path; the main purpose of changing this is for test
 
63
        # suites, but some vendors might have /sys in a nonstandard place
 
64
        self.sys_dir = '/sys'
 
65
 
 
66
        if client_only:
 
67
            return
 
68
 
 
69
        # below follows stuff which is only necessary for the backend
 
70
 
 
71
        # default paths
 
72
 
 
73
        # path to a modprobe.d configuration file where kernel modules are
 
74
        # enabled and disabled with blacklisting
 
75
        self.module_blacklist_file = '/etc/modprobe.d/blacklist-local.conf'
 
76
 
 
77
        # path to modinfo binary
 
78
        self.modinfo_path = '/sbin/modinfo'
 
79
 
 
80
        # path to modprobe binary
 
81
        self.modprobe_path = '/sbin/modprobe'
 
82
 
 
83
        # path to kernel's list of loaded modules
 
84
        self.proc_modules = '/proc/modules'
 
85
 
 
86
        # default path to custom handlers
 
87
        self.handler_dir = '/usr/share/jockey/handlers'
 
88
 
 
89
        if target_kernel:
 
90
            self.target_kernel = target_kernel
 
91
        else:
 
92
            self.target_kernel = os.uname()[2]
 
93
 
 
94
        # default paths to modalias files (directory entries will consider all
 
95
        # files in them)
 
96
        self.modaliases = [
 
97
            '/lib/modules/%s/modules.alias' % self.target_kernel,
 
98
            '/usr/share/jockey/modaliases/',
 
99
        ]
 
100
 
 
101
        # path to X.org configuration file
 
102
        self.xorg_conf_path = '/etc/X11/xorg.conf'
 
103
 
 
104
        self.set_backup_dir()
 
105
 
 
106
        # cache file for previously seen/newly used handlers lists (for --check)
 
107
        self.check_cache = os.path.join(self.backup_dir, 'check')
 
108
 
 
109
        self._load_module_blacklist()
 
110
 
 
111
        # Possible paths of a file with a set of SSL certificates which are
 
112
        # considered trustworthy. The first one that exists will be used.
 
113
        # This is used for downloading GPG key fingerprints for
 
114
        # openprinting.org driver packages.
 
115
        self.ssl_cert_file_paths = [
 
116
                # Debian/Ubuntu use the ca-certificates package:
 
117
                '/etc/ssl/certs/ca-certificates.crt'
 
118
                ]
 
119
 
 
120
        # default GPG key server
 
121
        # this is the generally recommended DNS round-robin, but usually very
 
122
        # slow:
 
123
        #self.gpg_key_server = 'keys.gnupg.net'
 
124
        self.gpg_key_server = 'hkp://keyserver.ubuntu.com:80'
 
125
 
 
126
        # Package which provides include files for the currently running
 
127
        # kernel.  If the system ensures that kernel headers are always
 
128
        # available, or being pulled in via dependencies (and there are not
 
129
        # multiple kernel flavors), it is ok to set this to "None". This should
 
130
        # use self.target_kernel instead of os.uname()[2].
 
131
        # Note that we want to install the metapackage here, to ensure upgrades
 
132
        # will keep working.
 
133
        flavour = '-'.join(self.target_kernel.split('-')[2:])
 
134
        self.kernel_header_package = 'linux-headers-' + flavour
 
135
 
 
136
        self.apt_show_cache = {}
 
137
        self.apt_sources = '/etc/apt/sources.list'
 
138
        self.apt_jockey_source = '/etc/apt/sources.list.d/jockey.list'
 
139
        self.apt_trusted_keyring = '/etc/apt/trusted.gpg.d/jockey-drivers.gpg'
 
140
 
 
141
        self._current_xorg_video_abi = None
 
142
 
 
143
    # 
 
144
    # The following package related functions use PackageKit; if that does not
 
145
    # work for your distribution, they must be reimplemented
 
146
    #
 
147
 
 
148
    def _apt_show(self, package):
 
149
        '''Return apt-cache show output, with caching.
 
150
        
 
151
        Return None if the package does not exist.
 
152
        '''
 
153
        try:
 
154
            return self.apt_show_cache[package]
 
155
        except KeyError:
 
156
            apt = subprocess.Popen(['apt-cache', 'show', package],
 
157
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
158
            out = apt.communicate()[0].strip()
 
159
            if apt.returncode == 0 and out:
 
160
                result = out
 
161
            else:
 
162
                result = None
 
163
            self.apt_show_cache[package] = result
 
164
            return result
 
165
 
 
166
    def is_package_free(self, package):
 
167
        '''Return if given package is free software.'''
 
168
 
 
169
        # TODO: this only works for packages in the official archive
 
170
        out = self._apt_show(package)
 
171
        if out:
 
172
            for l in out.splitlines():
 
173
                if l.startswith('Section:'):
 
174
                    s = l.split()[-1]
 
175
                    return not (s.startswith('restricted') or s.startswith('multiverse'))
 
176
 
 
177
        raise ValueError('package %s does not exist' % package)
 
178
 
 
179
    def package_installed(self, package):
 
180
        '''Return if the given package is installed.'''
 
181
 
 
182
        dpkg = subprocess.Popen(["dpkg-query", "-W", "-f${Status}", package],
 
183
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
184
        out = dpkg.communicate()[0]
 
185
        return dpkg.returncode == 0 and out.split()[-1] == "installed"
 
186
 
 
187
    def package_description(self, package):
 
188
        '''Return a tuple (short_description, long_description) for a package.
 
189
        
 
190
        This should raise a ValueError if the package is not available.
 
191
        '''
 
192
        out = self._apt_show(package)
 
193
        if out:
 
194
            lines = out.splitlines()
 
195
            start = 0
 
196
            while start < len(lines)-1:
 
197
                if lines[start].startswith('Description'):
 
198
                    break
 
199
                start += 1
 
200
            else:
 
201
                raise SystemError('failed to parse package description from ' + '\n'.join(lines))
 
202
 
 
203
            short = lines[start].split(' ', 1)[1]
 
204
            long = ''
 
205
            for l in lines[start+1:]:
 
206
                if l == ' .':
 
207
                    long += '\n\n'
 
208
                elif l.startswith(' '):
 
209
                    long += l.lstrip()
 
210
                else:
 
211
                    break
 
212
 
 
213
            return (short, long)
 
214
 
 
215
        raise ValueError('package %s does not exist' % package)
 
216
 
 
217
    def package_files(self, package):
 
218
        '''Return a list of files shipped by a package.
 
219
        
 
220
        This should raise a ValueError if the package is not installed.
 
221
        '''
 
222
        pkcon = subprocess.Popen(['dpkg', '-L', package],
 
223
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
224
        out = pkcon.communicate()[0]
 
225
        if pkcon.returncode == 0:
 
226
            return out.splitlines()
 
227
        else:
 
228
            raise ValueError('package %s is not installed' % package)
 
229
 
 
230
    def install_package(self, package, progress_cb, repository=None,
 
231
            fingerprint=None):
 
232
        '''Install the given package.
 
233
 
 
234
        As this is called in the backend, this must happen noninteractively.
 
235
        For progress reporting, progress_cb(phase, current, total) is called
 
236
        regularly, with 'phase' being 'download' or 'install'. If the callback
 
237
        returns True, the installation is attempted to get cancelled (this
 
238
        will probably succeed in the 'download' phase, but not in 'install').
 
239
        Passes '-1' for current and/or total if time cannot be determined.
 
240
 
 
241
        If this succeeds, subsequent package_installed(package) calls must
 
242
        return True.
 
243
 
 
244
        If a repository URL is given, that repository is added to the system
 
245
        first. The format for repository is distribution specific. This function
 
246
        should also download/update the package index for this repository when
 
247
        adding it.
 
248
        .
 
249
        fingerprint, if not None, is a GPG-style fingerprint of that
 
250
        repository; if present, this method should also retrieve that GPG key
 
251
        from the keyservers, install it into the packaging system, and ensure
 
252
        that the repository is signed with that key.
 
253
 
 
254
        An unknown package should raise a ValueError. Any installation failure
 
255
        due to bad packages should be logged, but not raise an exception, as
 
256
        this would just crash the backend.
 
257
        '''
 
258
        class MyFetchProgress(apt.FetchProgress):
 
259
            def __init__(self, callback):
 
260
                apt.FetchProgress.__init__(self)
 
261
                self.callback = callback
 
262
 
 
263
            def pulse(self):
 
264
                # consider download as 40% of the total progress for installation
 
265
                logging.debug('download progress %s %f' % (pkg, self.percent))
 
266
                return not self.callback('download', int(self.percent/2.5+10.5), 100)
 
267
 
 
268
        class MyInstallProgress(_CapturedInstallProgress):
 
269
            def __init__(self, callback):
 
270
                _CapturedInstallProgress.__init__(self)
 
271
                self.callback = callback
 
272
 
 
273
            def statusChange(self, pkg, percent, status):
 
274
                # consider install as 50% of the total progress for installation
 
275
                logging.debug('install progress %s %f' % (pkg, percent))
 
276
                self.callback('install', int(percent/4+50.5), 100)
 
277
 
 
278
        logging.debug('Installing package: %s', package)
 
279
        if progress_cb:
 
280
            progress_cb('download', 0, 100.0)
 
281
 
 
282
        if repository:
 
283
            if not self.repository_enabled(repository):
 
284
                logging.debug('install_package(): adding repository %s', repository)
 
285
                self._add_repository(repository, fingerprint, progress_cb)
 
286
                repository_added = True
 
287
            else:
 
288
                logging.debug('install_package(): repository %s already active', repository)
 
289
                repository_added = False
 
290
 
 
291
        os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
 
292
        # Disconnect from any running Debconf instance.
 
293
        try:
 
294
            del os.environ['DEBIAN_HAS_FRONTEND']
 
295
        except KeyError:
 
296
            pass
 
297
        try:
 
298
            del os.environ['DEBCONF_USE_CDEBCONF']
 
299
        except KeyError:
 
300
            pass
 
301
        os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
 
302
        apt.apt_pkg.config.set('DPkg::options::','--force-confnew')
 
303
 
 
304
        c = apt.Cache()
 
305
        try:
 
306
            try:
 
307
                pkg = c[package]
 
308
                origins = pkg.candidate.origins
 
309
            except (KeyError, AttributeError):
 
310
                raise ValueError('Package %s does not exist' % package)
 
311
 
 
312
            # if we have a binary package, we require a trusted origin; if we
 
313
            # don't have one, and we added a repository, remove it again
 
314
            # note: pkg.candidate.architecture switched away from "all" in Ubuntu 11.04
 
315
            if pkg.candidate.record['Architecture'] != 'all' and \
 
316
                    not pkg.candidate.uri.startswith('file:/'):
 
317
                for o in origins:
 
318
                    if o.trusted:
 
319
                        break
 
320
                else:
 
321
                    logging.error('Binary package %s has no trusted origin, rejecting', package)
 
322
                    if repository and repository_added:
 
323
                        self._remove_repository(repository)
 
324
                    return
 
325
 
 
326
            pkg.markInstall()
 
327
            inst_p = progress_cb and MyInstallProgress(progress_cb) or None
 
328
            try:
 
329
                try:
 
330
                    orig_excepthook = sys.excepthook
 
331
                    sys.excepthook = sys.__excepthook__
 
332
                    c.commit(progress_cb and MyFetchProgress(progress_cb) or None, inst_p)
 
333
                finally:
 
334
                    sys.excepthook = orig_excepthook
 
335
                    if inst_p and inst_p.out:
 
336
                        inst_p.out.seek(0)
 
337
                        apt_out = inst_p.out.read().decode('UTF-8', errors='replace')
 
338
                        inst_p.out.close()
 
339
                        logging.debug(apt_out)
 
340
                    else:
 
341
                        apt_out = ''
 
342
            except SystemError as e:
 
343
                logging.error('Package failed to install:\n' + apt_out)
 
344
                return
 
345
 
 
346
        except apt.cache.FetchCancelledException as e:
 
347
            return
 
348
        except (apt.cache.LockFailedException, apt.cache.FetchFailedException) as e:
 
349
            logging.error('Package fetching failed: %s', str(e))
 
350
 
 
351
    def remove_package(self, package, progress_cb):
 
352
        '''Uninstall the given package.
 
353
 
 
354
        As this is called in the backend, this must happen noninteractively.
 
355
        For progress reporting, progress_cb(current, total) is called
 
356
        regularly. Passes '-1' for current and/or total if time cannot be
 
357
        determined.
 
358
 
 
359
        If this succeeds, subsequent package_installed(package) calls must
 
360
        return False.
 
361
 
 
362
        Any removal failure should be raised as a SystemError.
 
363
        '''
 
364
        os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
 
365
        os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
 
366
        
 
367
        class MyInstallProgress(_CapturedInstallProgress):
 
368
            def __init__(self, callback):
 
369
                _CapturedInstallProgress.__init__(self)
 
370
                self.callback = callback
 
371
 
 
372
            def statusChange(self, pkg, percent, status):
 
373
                logging.debug('remove progress statusChange %s %f' % (pkg, percent))
 
374
                self.callback(percent, 100.0)
 
375
 
 
376
        logging.debug('Removing package: %s', package)
 
377
 
 
378
        c = apt.Cache()
 
379
        try:
 
380
            try:
 
381
                c[package].markDelete()
 
382
            except KeyError:
 
383
                logging.debug('Package %s does not exist, aborting', package)
 
384
                return False
 
385
            inst_p = progress_cb and MyInstallProgress(progress_cb) or None
 
386
            try:
 
387
                try:
 
388
                    c.commit(None, inst_p)
 
389
                finally:
 
390
                    if inst_p and inst_p.out:
 
391
                        inst_p.out.seek(0)
 
392
                        apt_out = inst_p.out.read().decode('UTF-8', errors='replace')
 
393
                        inst_p.out.close()
 
394
                        logging.debug(apt_out)
 
395
                    else:
 
396
                        apt_out = None
 
397
            except SystemError as e:
 
398
                if apt_out:
 
399
                    raise SystemError('Package failed to remove:\n' + apt_out)
 
400
                else:
 
401
                    raise
 
402
        except apt.cache.LockFailedException as e:
 
403
            logging.debug('could not lock apt cache, aborting: %s', str(e))
 
404
            raise SystemError, str(e)
 
405
 
 
406
        return True
 
407
 
 
408
    def has_repositories(self):
 
409
        '''Check if package repositories are available.
 
410
 
 
411
        This might not be the case after a fresh installation, when package
 
412
        indexes haven't been downloaded yet.
 
413
        '''
 
414
        apt_policy = subprocess.Popen(['apt-cache', 'policy', 'dkms'],
 
415
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
416
        out = apt_policy.communicate()[0]
 
417
        return '://' in out or 'file:/' in out
 
418
 
 
419
    def update_repository_indexes(self, progress_cb):
 
420
        '''Download package repository indexes.
 
421
 
 
422
        Return True on success, False otherwise.
 
423
 
 
424
        As this is called in the backend, this must happen noninteractively.
 
425
        For progress reporting, progress_cb(current, total) is called
 
426
        regularly. Passes '-1' for current and/or total if time cannot be
 
427
        determined.
 
428
        '''
 
429
        os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
 
430
        
 
431
        class MyProgress(apt.FetchProgress):
 
432
            def __init__(self, callback):
 
433
                apt.FetchProgress.__init__(self)
 
434
                self.callback = callback
 
435
 
 
436
            def pulse(self):
 
437
                #logging.debug('index download progress %f' % self.percent)
 
438
                self.callback(self.percent, 100.0)
 
439
 
 
440
        c = apt.Cache()
 
441
        try:
 
442
            c.update(progress_cb and MyProgress(progress_cb) or None)
 
443
        except apt.cache.LockFailedException as e:
 
444
            logging.debug('could not lock apt cache, aborting: %s', str(e))
 
445
            raise SystemError(str(e))
 
446
 
 
447
        return self.has_repositories()
 
448
 
 
449
    def packaging_system(self):
 
450
        '''Return packaging system.
 
451
 
 
452
        Currently defined values: apt, yum
 
453
        '''
 
454
        if os.path.exists('/etc/apt/sources.list') or os.path.exists(
 
455
            '/etc/apt/sources.list.d'):
 
456
            return 'apt'
 
457
        elif os.path.exists('/etc/yum.conf'):
 
458
            return 'yum'
 
459
 
 
460
        raise NotImplementedError('local packaging system is unknown')
 
461
 
 
462
    def import_gpg_key(self, keyring, fingerprint):
 
463
        '''Download and install a GPG key identified by fingerprint.
 
464
 
 
465
        This verifies that the fingerprint matches, and if so, installs it into
 
466
        the given keyring file (will be created if it does not exist).
 
467
 
 
468
        Raise a SystemError if anything goes wrong.
 
469
        '''
 
470
        if fingerprint in self._gpg_keyring_fingerprints(keyring):
 
471
            return
 
472
 
 
473
        # gpg likes to write trustdb and temporary files, so create a temporary
 
474
        # home directory
 
475
        keyid = fingerprint.replace(' ', '')[-8:]
 
476
        gpghome = tempfile.mkdtemp()
 
477
        default_keyring = os.path.join(gpghome, 'pubring.gpg')
 
478
        try:
 
479
            # we first import into a temporary keyring, as we need to verify
 
480
            # the fingerprint
 
481
            gpg = subprocess.Popen(['gpg', '--homedir', gpghome,
 
482
                '--no-default-keyring', '--primary-keyring', default_keyring,
 
483
                '--keyserver', self.gpg_key_server, '--recv-key', keyid], 
 
484
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, 
 
485
                env={'PATH': os.environ.get('PATH', ''), 
 
486
                     'http_proxy': os.environ.get('http_proxy', '')})
 
487
            (out, err) = gpg.communicate()
 
488
 
 
489
            if fingerprint not in self._gpg_keyring_fingerprints(default_keyring):
 
490
                raise SystemError('gpg failed to import key: ' + err)
 
491
 
 
492
            # now move over to the actual keyring; for multiple matches of the
 
493
            # same key ID it would be great to be able to select the one that
 
494
            # we want, but unfortunately the GPG command line doesn't really
 
495
            # allow that; fortunately key ID conflicts are very rare.
 
496
            gpg = subprocess.Popen(['gpg', '--homedir', gpghome,
 
497
                '--no-default-keyring', '--primary-keyring', keyring,
 
498
                '--import', default_keyring], 
 
499
                stdout=subprocess.PIPE, stderr=subprocess.PIPE,
 
500
                env={'PATH': os.environ.get('PATH', '')})
 
501
            (out, err) = gpg.communicate()
 
502
 
 
503
            if fingerprint not in self._gpg_keyring_fingerprints(default_keyring):
 
504
                raise SystemError('gpg failed to import key: ' + err)
 
505
 
 
506
            logging.debug('import_gpg_key(): Successfully imported key' + keyid)
 
507
        except OSError as e:
 
508
            raise SystemError('failed to call gpg: ' + str(e))
 
509
        finally:
 
510
            shutil.rmtree(gpghome)
 
511
 
 
512
    def _gpg_keyring_fingerprints(self, keyring):
 
513
        '''Return list of fingerprints in given keyring'''
 
514
 
 
515
        # gpg likes to write trustdb and temporary files, so create a temporary
 
516
        # home directory
 
517
        gpghome = tempfile.mkdtemp()
 
518
        try:
 
519
            result = []
 
520
            gpg = subprocess.Popen(['gpg', '--homedir', gpghome,
 
521
                '--no-default-keyring', '--primary-keyring', keyring,
 
522
                '--fingerprint'], stdout=subprocess.PIPE,
 
523
                stderr=subprocess.PIPE, env={})
 
524
            (out, err) = gpg.communicate()
 
525
            if gpg.returncode != 0:
 
526
                logging.error('failed to call gpg for reading fingerprints: ' + err)
 
527
                return []
 
528
 
 
529
            for l in out.splitlines():
 
530
                if 'fingerprint =' in l:
 
531
                    result.append(l.split('=')[1].strip())
 
532
            return result
 
533
        except OSError as e:
 
534
            logging.error('failed to call gpg: ' + str(e))
 
535
            return []
 
536
        finally:
 
537
            shutil.rmtree(gpghome)
 
538
 
 
539
    # 
 
540
    # The following functions MUST be implemented by distributors
 
541
    # Note that apt and yum PackageKit backends currently do not implement
 
542
    # RepoSetData(), so those need to remain package system specific
 
543
    #
 
544
 
 
545
    def repository_enabled(self, repository):
 
546
        '''Check if given repository is enabled.'''
 
547
 
 
548
        for f in [self.apt_sources] + glob(self.apt_sources + '.d/*.list'):
 
549
            try:
 
550
                logging.debug('repository_enabled(%s): checking %s', repository, f)
 
551
                for line in open(f):
 
552
                    if line.strip() == repository:
 
553
                        logging.debug('repository_enabled(%s): match', repository)
 
554
                        return True
 
555
            except IOError:
 
556
                pass
 
557
        logging.debug('repository_enabled(%s): no match', repository)
 
558
        return False
 
559
 
 
560
    def ui_help_available(self, ui):
 
561
        '''Return if help is available.
 
562
 
 
563
        This gets the current UI object passed, which can be used to determine
 
564
        whether GTK/KDE is used, etc.
 
565
        '''
 
566
        return os.access('/usr/bin/yelp', os.X_OK)
 
567
 
 
568
    def ui_help(self, ui):
 
569
        '''The UI's help button was clicked.
 
570
 
 
571
        This should open a help HTML page or website, call yelp with an
 
572
        appropriate topic, etc. This gets the current UI object passed, which
 
573
        can be used to determine whether GTK/KDE is used, etc.
 
574
        '''
 
575
        if 'gtk' in str(ui.__class__).lower():
 
576
            import gobject
 
577
            gobject.spawn_async(['yelp', 'help:ubuntu-help/hardware-driver-proprietary'],
 
578
                flags=gobject.SPAWN_SEARCH_PATH)
 
579
 
 
580
    # 
 
581
    # The following functions have a reasonable default implementation for
 
582
    # Linux, but can be tweaked by distributors
 
583
    #
 
584
 
 
585
    def set_backup_dir(self):
 
586
        '''Setup self.backup_dir, directory where backup files are stored.
 
587
        
 
588
        This is used for old xorg.conf, DriverDB caches, etc.
 
589
        '''
 
590
        self.backup_dir = '/var/cache/jockey'
 
591
        if not os.path.isdir(self.backup_dir):
 
592
            try:
 
593
                os.makedirs(self.backup_dir)
 
594
            except OSError as e:
 
595
                logging.error('Could not create %s: %s, using temporary '
 
596
                    'directory; all your caches will be lost!',
 
597
                    self.backup_dir, str(e))
 
598
                self.backup_dir = tempfile.mkdtemp(prefix='jockey_cache')
 
599
 
 
600
    def ignored_modules(self):
 
601
        '''Return a set of kernel modules which should be ignored.
 
602
 
 
603
        This particularly effects free kernel modules which are shipped by the
 
604
        OS vendor by default, and thus should not be controlled with this
 
605
        program.  Since this will include the large majority of existing kernel
 
606
        modules, implementing this is also important for speed reasons; without
 
607
        it, detecting existing modules will take quite long.
 
608
        
 
609
        Note that modules which are ignored here, but covered by a custom
 
610
        handler will still be considered.
 
611
        '''
 
612
        # try to get a *.ko file list from the main kernel package to avoid testing
 
613
        # known-free drivers
 
614
        kver = os.uname()[2]
 
615
        pkgs = ['linux-image-' + kver]
 
616
        linux_extra = 'linux-image-extra-' + kver
 
617
        if self.package_installed(linux_extra):
 
618
            pkgs.append(linux_extra)
 
619
        dpkg = subprocess.Popen(['dpkg', '-L'] + pkgs,
 
620
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
621
        out = dpkg.communicate()[0]
 
622
        result = set()
 
623
        if dpkg.returncode == 0:
 
624
            for l in out.splitlines():
 
625
                if l.endswith('.ko'):
 
626
                    result.add(os.path.splitext(os.path.basename(l))[0].replace('-', '_'))
 
627
 
 
628
        return result
 
629
 
 
630
    def module_blacklisted(self, module):
 
631
        '''Check if a module is on the modprobe blacklist.'''
 
632
 
 
633
        return module in self._module_blacklist or \
 
634
            module in self._module_blacklist_system
 
635
 
 
636
    def blacklist_module(self, module, blacklist):
 
637
        '''Add or remove a kernel module from the modprobe blacklist.
 
638
        
 
639
        If blacklist is True, the module is blacklisted, otherwise it is
 
640
        removed from the blacklist.
 
641
        '''
 
642
        if blacklist:
 
643
            self._module_blacklist.add(module)
 
644
        else:
 
645
            try:
 
646
                self._module_blacklist.remove(module)
 
647
            except KeyError:
 
648
                return # no need to save the blacklist
 
649
 
 
650
        self._save_module_blacklist()
 
651
 
 
652
    def _load_module_blacklist(self):
 
653
        '''Initialize self._module_blacklist{,_system}.'''
 
654
 
 
655
        self._module_blacklist = set()
 
656
        self._module_blacklist_system = set()
 
657
 
 
658
        self._read_blacklist_file(self.module_blacklist_file, self._module_blacklist)
 
659
 
 
660
        # read other blacklist files (which we will not touch, but evaluate)
 
661
        for f in glob('%s/blacklist*' % os.path.dirname(self.module_blacklist_file)):
 
662
            if f != self.module_blacklist_file:
 
663
                self._read_blacklist_file(f, self._module_blacklist_system)
 
664
 
 
665
    @classmethod
 
666
    def _read_blacklist_file(klass, path, blacklist_set):
 
667
        '''Read a blacklist file and add modules to blacklist_set.'''
 
668
 
 
669
        try:
 
670
            f = open(path)
 
671
        except IOError:
 
672
            return
 
673
 
 
674
        try:
 
675
            fcntl.flock(f.fileno(), fcntl.LOCK_SH)
 
676
            for line in f:
 
677
                # strip off comments
 
678
                line = line[:line.find('#')].strip()
 
679
 
 
680
                if not line.startswith('blacklist'):
 
681
                    continue
 
682
 
 
683
                module = line[len('blacklist'):].strip()
 
684
                if module:
 
685
                    blacklist_set.add(module)
 
686
        finally:
 
687
            f.close()
 
688
 
 
689
    def _save_module_blacklist(self):
 
690
        '''Save module blacklist.'''
 
691
 
 
692
        if len(self._module_blacklist) == 0 and \
 
693
            os.path.exists(self.module_blacklist_file):
 
694
                os.unlink(self.module_blacklist_file)
 
695
                return
 
696
 
 
697
        os.umask(0o22)
 
698
        # create directory if it does not exist
 
699
        d = os.path.dirname(self.module_blacklist_file)
 
700
        if not os.path.exists(d):
 
701
            os.makedirs(d)
 
702
 
 
703
        f = None
 
704
        try:
 
705
            f = open(self.module_blacklist_file, 'w')
 
706
            fcntl.flock(f.fileno(), fcntl.LOCK_EX)
 
707
            for module in sorted(self._module_blacklist):
 
708
                print >> f, 'blacklist', module
 
709
        except IOError as e:
 
710
            logging.error('Failed to write to module blacklist: ' + str(e))
 
711
        finally:
 
712
            if f:
 
713
                f.close()
 
714
 
 
715
    def _get_os_version(self):
 
716
        '''Initialize self.os_vendor and self.os_version.
 
717
 
 
718
        This defaults to reading the values from lsb_release.
 
719
        '''
 
720
        p = subprocess.Popen(['lsb_release', '-si'], stdout=subprocess.PIPE,
 
721
            stderr=subprocess.PIPE, close_fds=True)
 
722
        self.os_vendor = p.communicate()[0].strip()
 
723
        p = subprocess.Popen(['lsb_release', '-sr'], stdout=subprocess.PIPE,
 
724
            stderr=subprocess.PIPE, close_fds=True)
 
725
        self.os_version = p.communicate()[0].strip()
 
726
        assert p.returncode == 0
 
727
 
 
728
    def get_system_vendor_product(self):
 
729
        '''Return (vendor, product) of the system hardware.
 
730
 
 
731
        Either or both can be '' if they cannot be determined.
 
732
 
 
733
        The default implementation queries sysfs.
 
734
        '''
 
735
        try:
 
736
            vendor = open(os.path.join(self.sys_dir,
 
737
                'class', 'dmi', 'id', 'sys_vendor')).read().strip()
 
738
        except IOError:
 
739
            vendor = ''
 
740
 
 
741
        try:
 
742
            product = open(os.path.join(self.sys_dir,
 
743
                'class', 'dmi', 'id', 'product_name')).read().strip()
 
744
        except IOError:
 
745
            product = ''
 
746
 
 
747
        return (vendor, product)
 
748
 
 
749
    def notify_reboot_required(self):
 
750
        '''Notify the system that a reboot is required.
 
751
 
 
752
        This can be used as an extra indication when installing a driver which
 
753
        needs a reboot to get active.
 
754
 
 
755
        The default implementation does nothing.
 
756
        '''
 
757
        try:
 
758
            subprocess.call(['/usr/share/update-notifier/notify-reboot-required'])
 
759
        except OSError:
 
760
            pass
 
761
 
 
762
    def package_header_modaliases(self, cache=None):
 
763
        '''Get modalias map from package headers.
 
764
 
 
765
        Driver packages may declare the modaliases that they support in a
 
766
        package header field, so that they do not need to have a separate
 
767
        modalias file list already installed. The map must have the following
 
768
        structure: package_name -> module_name -> [list of modaliases]
 
769
 
 
770
        If this is not supported, simply return an empty dictionary here.
 
771
        '''
 
772
        result = {}
 
773
        if cache is None:
 
774
            cache = apt.Cache()
 
775
 
 
776
        # get the system architecture, to avoid getting non-native multi-arch
 
777
        # packages
 
778
        dpkg = subprocess.Popen(['dpkg', '--print-architecture'],
 
779
            stdout=subprocess.PIPE)
 
780
        system_architecture = dpkg.communicate()[0].strip()
 
781
        assert dpkg.returncode == 0
 
782
 
 
783
        for package in cache:
 
784
          pkg = cache[package.name]
 
785
          if pkg.candidate != None:
 
786
            if pkg.candidate.architecture != system_architecture:
 
787
                  continue
 
788
              try:
 
789
                  m = package.candidate.record['Modaliases']
 
790
              except (KeyError, AttributeError):
 
791
                  continue
 
792
 
 
793
              try:
 
794
                  for part in m.split(')'):
 
795
                      part = part.strip(', ')
 
796
                      if not part:
 
797
                          continue
 
798
                      module, lst = part.split('(')
 
799
                      for alias in lst.split(','):
 
800
                          result.setdefault(package.name, {}).setdefault(module,
 
801
                              []).append(alias.strip())
 
802
              except ValueError:
 
803
                  logging.error('Package %s has invalid modalias header: %s' % (
 
804
                      package.name, m))
 
805
 
 
806
        return result
 
807
 
 
808
    def ssl_cert_file(self):
 
809
        '''Get file with trusted SSL certificates.
 
810
        
 
811
        This is used for downloading GPG key fingerprints for
 
812
        openprinting.org driver packages.
 
813
 
 
814
        Return None if no certificates file is available.
 
815
        '''
 
816
        for f in self.ssl_cert_file_paths:
 
817
            if os.path.exists(f):
 
818
                return f
 
819
 
 
820
        return None
 
821
 
 
822
    @classmethod
 
823
    def has_defaultroute(klass):
 
824
        '''Return if there is a default route.
 
825
 
 
826
        This is a reasonable indicator that online tests can be run.
 
827
        '''
 
828
        if klass._has_defaultroute_cache is None:
 
829
            klass._has_defaultroute_cache = False
 
830
            route = subprocess.Popen(['/sbin/route', '-n'],
 
831
                stdout=subprocess.PIPE)
 
832
            for l in route.stdout:
 
833
                if l.startswith('0.0.0.0 '):
 
834
                    klass._has_defaultroute_cache = True
 
835
            route.wait()
 
836
 
 
837
        return klass._has_defaultroute_cache
 
838
 
 
839
    _has_defaultroute_cache = None
 
840
 
 
841
    def current_xorg_video_abi(self):
 
842
        '''Return current X.org video ABI.
 
843
 
 
844
        For an X.org video driver to actually work it must be built against the
 
845
        currently used X.org driver ABI, otherwise it will cause crashes. This
 
846
        method returns the currently expected video driver ABI from the X
 
847
        server. If it is not None, it must match video_driver_abi() of a driver
 
848
        package for this driver to be offered for installation.
 
849
        
 
850
        If this returns None, ABI checking is disabled.
 
851
        '''
 
852
        if not self._current_xorg_video_abi:
 
853
            dpkg = subprocess.Popen(['dpkg', '-s', 'xserver-xorg-core'],
 
854
                    stdout=subprocess.PIPE)
 
855
            out = dpkg.communicate()[0]
 
856
            if dpkg.returncode == 0:
 
857
                m = re.search('^Provides: .*(xorg-video-abi-\w+)', out, re.M)
 
858
                if m:
 
859
                    self._current_xorg_video_abi = m.group(1)
 
860
 
 
861
        return self._current_xorg_video_abi
 
862
 
 
863
    def video_driver_abi(self, package):
 
864
        '''Return video ABI for an X.org driver package.
 
865
 
 
866
        For an X.org video driver to actually work it must be built against the
 
867
        currently used X.org driver ABI, otherwise it will cause crashes. This
 
868
        method returns the video ABI for a driver package. If it is not None,
 
869
        it must match current_xorg_video_abi() for this driver to be offered
 
870
        for installation.
 
871
        
 
872
        If this returns None, ABI checking is disabled.
 
873
        '''
 
874
        abi = None
 
875
        dpkg = subprocess.Popen(['apt-cache', 'show', package],
 
876
                stdout=subprocess.PIPE)
 
877
        out = dpkg.communicate()[0]
 
878
        if dpkg.returncode == 0:
 
879
            m = re.search('^Depends: .*(xorg-video-abi-\w+)', out, re.M)
 
880
            if m:
 
881
                abi = m.group(1)
 
882
 
 
883
        return abi
 
884
 
 
885
    #
 
886
    # Internal helper methods
 
887
    #
 
888
 
 
889
    def _add_repository(self, repository, fingerprint, progress_cb):
 
890
        '''Add a repository.
 
891
 
 
892
        The format for repository is distribution specific. This function
 
893
        should also download/update the package index for this repository.
 
894
 
 
895
        This should throw a ValueError if the repository is invalid or
 
896
        inaccessible.
 
897
 
 
898
        fingerprint, if not None, is a GPG-style fingerprint of that
 
899
        repository; if present, this method also retrieves that GPG key
 
900
        from the keyservers and installs it into the packaging system.
 
901
        '''
 
902
        if fingerprint:
 
903
            self.import_gpg_key(self.apt_trusted_keyring, fingerprint)
 
904
 
 
905
        if os.path.exists(self.apt_jockey_source):
 
906
            backup = self.apt_jockey_source + '.bak'
 
907
            os.rename(self.apt_jockey_source, backup)
 
908
        else:
 
909
            backup = None
 
910
        f = open(self.apt_jockey_source, 'a')
 
911
        print >> f, repository.strip()
 
912
        f.close()
 
913
 
 
914
        class MyFetchProgress(apt.FetchProgress):
 
915
            def __init__(self, callback):
 
916
                apt.FetchProgress.__init__(self)
 
917
                self.callback = callback
 
918
 
 
919
            def pulse(self):
 
920
                self.callback
 
921
                logging.debug('index download progress %f' % self.percent)
 
922
                # consider update as 10% of the total progress for installation
 
923
                return not self.callback('download', int(self.percent/10+.5), 100)
 
924
 
 
925
        c = apt.Cache()
 
926
        try:
 
927
            logging.debug('_add_repository(): Updating apt lists')
 
928
            c.update(progress_cb and MyFetchProgress(progress_cb) or None,
 
929
                    sources_list=self.apt_jockey_source)
 
930
        except SystemError, e:
 
931
            logging.error('_add_repository(%s): Invalid repository', repository)
 
932
            if backup:
 
933
                os.rename(backup, self.apt_jockey_source)
 
934
            else:
 
935
                os.unlink(self.apt_jockey_source)
 
936
            raise ValueError(e.message)
 
937
        except apt.cache.FetchCancelledException, e:
 
938
            return False
 
939
        except (apt.cache.LockFailedException, apt.cache.FetchFailedException), e:
 
940
            logging.warning('Package fetching failed: %s', str(e))
 
941
            raise SystemError(str(e))
 
942
 
 
943
    def _remove_repository(self, repository):
 
944
        '''Remove a repository.
 
945
 
 
946
        The format for repository is distribution specific.
 
947
        '''
 
948
        if not os.path.exists(self.apt_jockey_source):
 
949
            return
 
950
        result = []
 
951
        for line in open(self.apt_jockey_source):
 
952
            if line.strip() != repository:
 
953
                result.append(line)
 
954
        if result:
 
955
            f = open(self.apt_jockey_source, 'w')
 
956
            f.write('\n'.join(result))
 
957
            f.close()
 
958
        else:
 
959
            os.unlink(self.apt_jockey_source)