home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / jockey / oslib.py < prev    next >
Encoding:
Python Source  |  2009-04-07  |  21.3 KB  |  595 lines

  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
  21. from glob import glob
  22.  
  23. import warnings
  24. warnings.simplefilter('ignore', FutureWarning)
  25. import apt
  26.  
  27. class _CapturedInstallProgress(apt.InstallProgress):
  28.     def fork(self):
  29.         '''Reroute stdout/stderr to files, so that we can log them'''
  30.  
  31.         self.stdout = tempfile.TemporaryFile()
  32.         self.stderr = tempfile.TemporaryFile()
  33.         p = os.fork()
  34.         if p == 0:
  35.             os.dup2(self.stdout.fileno(), sys.stdout.fileno())
  36.             os.dup2(self.stderr.fileno(), sys.stderr.fileno())
  37.         return p
  38.  
  39. class OSLib:
  40.     '''Encapsulation of operating system/Linux distribution specific operations.'''
  41.  
  42.     # global default instance
  43.     inst = None
  44.  
  45.     def __init__(self, client_only=False):
  46.         '''Set default paths and load the module blacklist.
  47.         
  48.         Distributors might want to override some default paths.
  49.         If client_only is True, this only initializes functionality which is
  50.         needed by clients, and which can be done without special privileges.
  51.         '''
  52.         # relevant stuff for clients and backend
  53.         self._get_os_version()
  54.         self.hal_get_property_path = '/usr/bin/hal-get-property'
  55.  
  56.         if client_only:
  57.             return
  58.  
  59.         # below follows stuff which is only necessary for the backend
  60.  
  61.         # default paths
  62.  
  63.         # /sys/ path; the main purpose of changing this is for test
  64.         # suites, but some vendors might have /sys in a nonstandard place
  65.         self.sys_dir = '/sys'
  66.  
  67.         # path to a modprobe.d configuration file where kernel modules are
  68.         # enabled and disabled with blacklisting
  69.         self.module_blacklist_file = '/etc/modprobe.d/blacklist-local.conf'
  70.  
  71.         # path to modinfo binary
  72.         self.modinfo_path = '/sbin/modinfo'
  73.  
  74.         # path to modprobe binary
  75.         self.modprobe_path = '/sbin/modprobe'
  76.  
  77.         # path to kernel's list of loaded modules
  78.         self.proc_modules = '/proc/modules'
  79.  
  80.         # default path to custom handlers
  81.         self.handler_dir = '/usr/share/jockey/handlers'
  82.  
  83.         # default paths to modalias files (directory entries will consider all
  84.         # files in them)
  85.         self.modaliases = [
  86.             '/lib/modules/%s/modules.alias' % os.uname()[2],
  87.             '/usr/share/jockey/modaliases/',
  88.         ]
  89.  
  90.         # path to X.org configuration file
  91.         self.xorg_conf_path = '/etc/X11/xorg.conf'
  92.  
  93.         self.set_backup_dir()
  94.  
  95.         # cache file for previously seen/newly used handlers lists (for --check)
  96.         self.check_cache = os.path.join(self.backup_dir, 'check')
  97.  
  98.         self._load_module_blacklist()
  99.  
  100.         self.apt_show_cache = {}
  101.         self.apt_sources = '/etc/apt/sources.list'
  102.         self.apt_jockey_source = '/etc/apt/sources.list.d/jockey.list'
  103.  
  104.     # 
  105.     # The following package related functions use PackageKit; if that does not
  106.     # work for your distribution, they must be reimplemented
  107.     #
  108.  
  109.     def _apt_show(self, package):
  110.         '''Return apt-cache show output, with caching.
  111.         
  112.         Return None if the package does not exist.
  113.         '''
  114.         try:
  115.             return self.apt_show_cache[package]
  116.         except KeyError:
  117.             apt = subprocess.Popen(['apt-cache', 'show', package],
  118.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  119.             out = apt.communicate()[0].strip()
  120.             if apt.returncode == 0 and out:
  121.                 result = out
  122.             else:
  123.                 result = None
  124.             self.apt_show_cache[package] = result
  125.             return result
  126.  
  127.     def is_package_free(self, package):
  128.         '''Return if given package is free software.'''
  129.  
  130.         # TODO: this only works for packages in the official archive
  131.         out = self._apt_show(package)
  132.         if out:
  133.             for l in out.splitlines():
  134.                 if l.startswith('Section:'):
  135.                     s = l.split()[-1]
  136.                     return not (s.startswith('restricted') or s.startswith('multiverse'))
  137.  
  138.         raise ValueError, 'package %s does not exist' % package
  139.  
  140.     def package_installed(self, package):
  141.         '''Return if the given package is installed.'''
  142.  
  143.         dpkg = subprocess.Popen(["dpkg-query", "-W", "-f${Status}", package],
  144.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  145.         out = dpkg.communicate()[0]
  146.         return dpkg.returncode == 0 and out.split()[-1] == "installed"
  147.  
  148.     def package_description(self, package):
  149.         '''Return a tuple (short_description, long_description) for a package.
  150.         
  151.         This should raise a ValueError if the package is not available.
  152.         '''
  153.         out = self._apt_show(package)
  154.         if out:
  155.             lines = out.splitlines()
  156.             start = 0
  157.             while start < len(lines)-1:
  158.                 if lines[start].startswith('Description:'):
  159.                     break
  160.                 start += 1
  161.  
  162.             short = lines[start].split(' ', 1)[1]
  163.             long = ''
  164.             for l in lines[start+1:]:
  165.                 if l == ' .':
  166.                     long += '\n\n'
  167.                 elif l.startswith(' '):
  168.                     long += l.lstrip()
  169.                 else:
  170.                     break
  171.  
  172.             return (short, long)
  173.  
  174.         raise ValueError, 'package %s does not exist' % package
  175.  
  176.     def package_files(self, package):
  177.         '''Return a list of files shipped by a package.
  178.         
  179.         This should raise a ValueError if the package is not installed.
  180.         '''
  181.         pkcon = subprocess.Popen(['dpkg', '-L', package],
  182.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  183.         out = pkcon.communicate()[0]
  184.         if pkcon.returncode == 0:
  185.             return out.splitlines()
  186.         else:
  187.             raise ValueError, 'package %s is not installed' % package
  188.  
  189.     # 
  190.     # The following functions MUST be implemented by distributors
  191.     #
  192.  
  193.     def install_package(self, package, progress_cb):
  194.         '''Install the given package.
  195.  
  196.         As this is called in the backend, this must happen noninteractively.
  197.         For progress reporting, progress_cb(phase, current, total) is called
  198.         regularly, with 'phase' being 'download' or 'install'. If the callback
  199.         returns True, the installation is attempted to get cancelled (this
  200.         will probably succeed in the 'download' phase, but not in 'install').
  201.         Passes '-1' for current and/or total if time cannot be determined.
  202.  
  203.         If this succeeds, subsequent package_installed(package) calls must
  204.         return True.
  205.  
  206.         Any installation failure should be raised as a SystemError.
  207.         '''
  208.         class MyFetchProgress(apt.FetchProgress):
  209.             def __init__(self, callback):
  210.                 apt.FetchProgress.__init__(self)
  211.                 self.callback = callback
  212.  
  213.             def pulse(self):
  214.                 return not self.callback('download', int(self.percent/2+.5), 100)
  215.  
  216.         class MyInstallProgress(_CapturedInstallProgress):
  217.             def __init__(self, callback):
  218.                 _CapturedInstallProgress.__init__(self)
  219.                 self.callback = callback
  220.  
  221.             def statusChange(self, pkg, percent, status):
  222.                 logging.debug('install progress statusChange %s %f' % (pkg, percent))
  223.                 self.callback('install', int(percent/2+50.5), 100)
  224.  
  225.         logging.debug('Installing package: %s', package)
  226.         if progress_cb:
  227.             progress_cb('download', 0, 100.0)
  228.  
  229.         os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
  230.         os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
  231.         apt.apt_pkg.Config.Set('DPkg::options::','--force-confnew')
  232.  
  233.         c = apt.Cache()
  234.         try:
  235.             try:
  236.                 c[package].markInstall()
  237.             except KeyError:
  238.                 logging.debug('Package %s does not exist, aborting', package)
  239.                 return False
  240.             inst_p = progress_cb and MyInstallProgress(progress_cb) or None
  241.             c.commit(progress_cb and MyFetchProgress(progress_cb) or None, inst_p)
  242.             if inst_p:
  243.                 inst_p.stdout.seek(0)
  244.                 out = inst_p.stdout.read()
  245.                 inst_p.stdout.close()
  246.                 inst_p.stderr.seek(0)
  247.                 err = inst_p.stderr.read()
  248.                 inst_p.stderr.close()
  249.  
  250.                 if out:
  251.                     logging.debug(out)
  252.                 if err:
  253.                     logging.error(err)
  254.         except apt.cache.FetchCancelledException, e:
  255.             return False
  256.         except (apt.cache.LockFailedException, apt.cache.FetchFailedException), e:
  257.             logging.warning('Package fetching failed: %s', str(e))
  258.             raise SystemError, str(e)
  259.         return True
  260.  
  261.     def remove_package(self, package, progress_cb):
  262.         '''Uninstall the given package.
  263.  
  264.         As this is called in the backend, this must happen noninteractively.
  265.         For progress reporting, progress_cb(current, total) is called
  266.         regularly. Passes '-1' for current and/or total if time cannot be
  267.         determined.
  268.  
  269.         If this succeeds, subsequent package_installed(package) calls must
  270.         return False.
  271.  
  272.         Any removal failure should be raised as a SystemError.
  273.         '''
  274.         os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
  275.         os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
  276.         
  277.         class MyInstallProgress(_CapturedInstallProgress):
  278.             def __init__(self, callback):
  279.                 _CapturedInstallProgress.__init__(self)
  280.                 self.callback = callback
  281.  
  282.             def statusChange(self, pkg, percent, status):
  283.                 logging.debug('remove progress statusChange %s %f' % (pkg, percent))
  284.                 self.callback(percent, 100.0)
  285.  
  286.         logging.debug('Removing package: %s', package)
  287.  
  288.         c = apt.Cache()
  289.         try:
  290.             try:
  291.                 c[package].markDelete()
  292.             except KeyError:
  293.                 logging.debug('Package %s does not exist, aborting', package)
  294.                 return False
  295.             inst_p = progress_cb and MyInstallProgress(progress_cb) or None
  296.             c.commit(None, inst_p)
  297.             if inst_p:
  298.                 inst_p.stdout.seek(0)
  299.                 out = inst_p.stdout.read()
  300.                 inst_p.stdout.close()
  301.                 inst_p.stderr.seek(0)
  302.                 err = inst_p.stderr.read()
  303.                 inst_p.stderr.close()
  304.  
  305.                 if out:
  306.                     logging.debug(out)
  307.                 if err:
  308.                     logging.error(err)
  309.         except apt.cache.LockFailedException, e:
  310.             logging.debug('could not lock apt cache, aborting: %s', str(e))
  311.             raise SystemError, str(e)
  312.  
  313.         return True
  314.  
  315.     def packaging_system(self):
  316.         '''Return packaging system.
  317.  
  318.         Currently defined values: apt
  319.         '''
  320.         # apt
  321.         if os.path.exists('/etc/apt/sources.list') or os.path.exists(
  322.             '/etc/apt/sources.list.d'):
  323.             return 'apt'
  324.  
  325.         raise NotImplementedError, 'local packaging system is unknown'
  326.  
  327.     def add_repository(self, repository):
  328.         '''Add a repository.
  329.  
  330.         The format for repository is distribution specific. This function
  331.         should also download/update the package index for this repository.
  332.  
  333.         This should throw a ValueError if the repository is invalid or
  334.         inaccessible.
  335.         '''
  336.         if self.repository_enabled(repository):
  337.             logging.debug('add_repository(%s): already active', repository)
  338.             return
  339.  
  340.         if os.path.exists(self.apt_jockey_source):
  341.             backup = self.apt_jockey_source + '.bak'
  342.             os.rename(self.apt_jockey_source, backup)
  343.         else:
  344.             backup = None
  345.         f = open(self.apt_jockey_source, 'w')
  346.         print >> f, repository.strip()
  347.         f.close()
  348.  
  349.         try:
  350.             # TODO: progress feedback
  351.             c = apt.Cache()
  352.             c.update()
  353.         except SystemError, e:
  354.             logging.error('add_repository(%s): Invalid repository', repository)
  355.             if backup:
  356.                 os.rename(backup, self.apt_jockey_source)
  357.             else:
  358.                 os.unlink(self.apt_jockey_source)
  359.             raise ValueError(e.message)
  360.         except apt.cache.FetchCancelledException, e:
  361.             return False
  362.         except (apt.cache.LockFailedException, apt.cache.FetchFailedException), e:
  363.             logging.warning('Package fetching failed: %s', str(e))
  364.             raise SystemError, str(e)
  365.  
  366.     def remove_repository(self, repository):
  367.         '''Remove a repository.
  368.  
  369.         The format for repository is distribution specific.
  370.         '''
  371.         if not os.path.exists(self.apt_jockey_source):
  372.             return
  373.         result = []
  374.         for line in open(self.apt_jockey_source):
  375.             if line.strip() != repository:
  376.                 result.append(line)
  377.         if result:
  378.             f = open(self.apt_jockey_source, 'w')
  379.             f.write('\n'.join(result))
  380.             f.close()
  381.         else:
  382.             os.unlink(self.apt_jockey_source)
  383.  
  384.     def repository_enabled(self, repository):
  385.         '''Check if given repository is enabled.'''
  386.  
  387.         for f in [self.apt_sources] + glob(self.apt_sources + '.d/*.list'):
  388.             try:
  389.                 logging.debug('repository_enabled(%s): checking %s', repository, f)
  390.                 for line in open(f):
  391.                     if line.strip() == repository:
  392.                         logging.debug('repository_enabled(%s): match', repository)
  393.                         return True
  394.             except IOError:
  395.                 pass
  396.         logging.debug('repository_enabled(%s): no match', repository)
  397.         return False
  398.  
  399.     def ui_help_available(self, ui):
  400.         '''Return if help is available.
  401.  
  402.         This gets the current UI object passed, which can be used to determine
  403.         whether GTK/KDE is used, etc.
  404.         '''
  405.         return os.access('/usr/bin/yelp', os.X_OK)
  406.  
  407.     def ui_help(self, ui):
  408.         '''The UI's help button was clicked.
  409.  
  410.         This should open a help HTML page or website, call yelp with an
  411.         appropriate topic, etc. This gets the current UI object passed, which
  412.         can be used to determine whether GTK/KDE is used, etc.
  413.         '''
  414.         if 'gtk' in str(ui.__class__).lower():
  415.             import gobject
  416.             gobject.spawn_async(["yelp", "ghelp:hardware#restricted-manager"],
  417.                 flags=gobject.SPAWN_SEARCH_PATH)
  418.  
  419.     # 
  420.     # The following functions have a reasonable default implementation for
  421.     # Linux, but can be tweaked by distributors
  422.     #
  423.  
  424.     def set_backup_dir(self):
  425.         '''Setup self.backup_dir, directory where backup files are stored.
  426.         
  427.         This is used for old xorg.conf, DriverDB caches, etc.
  428.         '''
  429.         self.backup_dir = '/var/cache/jockey'
  430.         if not os.path.isdir(self.backup_dir):
  431.             try:
  432.                 os.makedirs(self.backup_dir)
  433.             except OSError, e:
  434.                 logging.error('Could not create %s: %s, using temporary '
  435.                     'directory; all your caches will be lost!',
  436.                     self.backup_dir, str(e))
  437.                 self.backup_dir = tempfile.mkdtemp(prefix='jockey_cache')
  438.  
  439.     def ignored_modules(self):
  440.         '''Return a set of kernel modules which should be ignored.
  441.  
  442.         This particularly effects free kernel modules which are shipped by the
  443.         OS vendor by default, and thus should not be controlled with this
  444.         program.  Since this will include the large majority of existing kernel
  445.         modules, implementing this is also important for speed reasons; without
  446.         it, detecting existing modules will take quite long.
  447.         
  448.         Note that modules which are ignored here, but covered by a custom
  449.         handler will still be considered.
  450.         '''
  451.         # try to get a *.ko file list from the main kernel package to avoid testing
  452.         # known-free drivers
  453.         dpkg = subprocess.Popen(['dpkg', '-L', 'linux-image-' + os.uname()[2]],
  454.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  455.         out = dpkg.communicate()[0]
  456.         result = set()
  457.         if dpkg.returncode == 0:
  458.             for l in out.splitlines():
  459.                 if l.endswith('.ko'):
  460.                     result.add(os.path.splitext(os.path.basename(l))[0].replace('-', '_'))
  461.  
  462.         dpkg = subprocess.Popen(['dpkg', '-L', 'linux-ubuntu-modules-' + os.uname()[2]],
  463.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  464.         out = dpkg.communicate()[0]
  465.         if dpkg.returncode == 0:
  466.             for l in out.splitlines():
  467.                 if l.endswith('.ko'):
  468.                     result.add(os.path.splitext(os.path.basename(l))[0].replace('-', '_'))
  469.  
  470.         return result
  471.  
  472.     def module_blacklisted(self, module):
  473.         '''Check if a module is on the modprobe blacklist.'''
  474.  
  475.         return module in self._module_blacklist or \
  476.             module in self._module_blacklist_system
  477.  
  478.     def blacklist_module(self, module, blacklist):
  479.         '''Add or remove a kernel module from the modprobe blacklist.
  480.         
  481.         If blacklist is True, the module is blacklisted, otherwise it is
  482.         removed from the blacklist.
  483.         '''
  484.         if blacklist:
  485.             self._module_blacklist.add(module)
  486.         else:
  487.             try:
  488.                 self._module_blacklist.remove(module)
  489.             except KeyError:
  490.                 return # no need to save the blacklist
  491.  
  492.         self._save_module_blacklist()
  493.  
  494.     def _load_module_blacklist(self):
  495.         '''Initialize self._module_blacklist{,_system}.'''
  496.  
  497.         self._module_blacklist = set()
  498.         self._module_blacklist_system = set()
  499.  
  500.         self._read_blacklist_file(self.module_blacklist_file, self._module_blacklist)
  501.  
  502.         # read other blacklist files (which we will not touch, but evaluate)
  503.         for f in glob('%s/blacklist*' % os.path.dirname(self.module_blacklist_file)):
  504.             if f != self.module_blacklist_file:
  505.                 self._read_blacklist_file(f, self._module_blacklist_system)
  506.  
  507.     @classmethod
  508.     def _read_blacklist_file(klass, path, blacklist_set):
  509.         '''Read a blacklist file and add modules to blacklist_set.'''
  510.  
  511.         try:
  512.             f = open(path)
  513.         except IOError:
  514.             return
  515.  
  516.         try:
  517.             fcntl.flock(f.fileno(), fcntl.LOCK_SH)
  518.             for line in f:
  519.                 # strip off comments
  520.                 line = line[:line.find('#')].strip()
  521.  
  522.                 if not line.startswith('blacklist'):
  523.                     continue
  524.  
  525.                 module = line[len('blacklist'):].strip()
  526.                 if module:
  527.                     blacklist_set.add(module)
  528.         finally:
  529.             f.close()
  530.  
  531.     def _save_module_blacklist(self):
  532.         '''Save module blacklist.'''
  533.  
  534.         if len(self._module_blacklist) == 0 and \
  535.             os.path.exists(self.module_blacklist_file):
  536.                 os.unlink(self.module_blacklist_file)
  537.                 return
  538.  
  539.         os.umask(022)
  540.         # create directory if it does not exist
  541.         d = os.path.dirname(self.module_blacklist_file)
  542.         if not os.path.exists(d):
  543.             os.makedirs(d)
  544.  
  545.         f = open(self.module_blacklist_file, 'w')
  546.         try:
  547.             fcntl.flock(f.fileno(), fcntl.LOCK_EX)
  548.             for module in sorted(self._module_blacklist):
  549.                 print >> f, 'blacklist', module
  550.         finally:
  551.             f.close()
  552.  
  553.     def _get_os_version(self):
  554.         '''Initialize self.os_vendor and self.os_version.
  555.  
  556.         This defaults to reading the values from lsb_release.
  557.         '''
  558.         p = subprocess.Popen(['lsb_release', '-si'], stdout=subprocess.PIPE,
  559.             stderr=subprocess.PIPE, close_fds=True)
  560.         self.os_vendor = p.communicate()[0].strip()
  561.         p = subprocess.Popen(['lsb_release', '-sr'], stdout=subprocess.PIPE,
  562.             stderr=subprocess.PIPE, close_fds=True)
  563.         self.os_version = p.communicate()[0].strip()
  564.         assert p.returncode == 0
  565.  
  566.     def get_system_vendor_product(self):
  567.         '''Return (vendor, product) of the system hardware.
  568.  
  569.         Either or both can be '' if they cannot be determined.
  570.  
  571.         The default implementation queries hal.
  572.         '''
  573.  
  574.         try:
  575.             hal = subprocess.Popen([self.hal_get_property_path, '--udi',
  576.                 '/org/freedesktop/Hal/devices/computer', '--key',
  577.                 'system.hardware.vendor'], stdout=subprocess.PIPE,
  578.                 close_fds=True)
  579.             vendor = hal.communicate()[0].strip()
  580.             assert hal.returncode == 0
  581.         except (OSError, AssertionError):
  582.             vendor = ''
  583.  
  584.         try:
  585.             hal = subprocess.Popen([self.hal_get_property_path, '--udi',
  586.                 '/org/freedesktop/Hal/devices/computer', '--key',
  587.                 'system.hardware.product'], stdout=subprocess.PIPE,
  588.                 close_fds=True)
  589.             product = hal.communicate()[0].strip()
  590.             assert hal.returncode == 0
  591.         except (OSError, AssertionError):
  592.             product = ''
  593.  
  594.         return (vendor, product)
  595.