Ticket #1086: Portage.py

File Portage.py, 10.9 KB (added by casso, 10 years ago)
Line 
1"""This is the Bcfg2 tool for the Gentoo Portage system."""
2__revision__ = '$Revision$'
3
4import re
5import Bcfg2.Client.Tools
6from Bcfg2.Bcfg2Py3k import ConfigParser
7
8class Portage(Bcfg2.Client.Tools.PkgTool):
9    """The Gentoo toolset implements package and service operations and
10    inherits the rest from Toolset.Toolset."""
11    name = 'Portage'
12    __execs__ = ['/usr/bin/emerge', '/usr/bin/equery']
13    __handles__ = [('Package', 'ebuild')]
14    __req__ = {'Package': ['name', 'version']}
15    pkgtype = 'ebuild'
16    # requires a working PORTAGE_BINHOST in make.conf
17    _binpkgtool = ('emerge --getbinpkgonly %s', ('=%s-%s', \
18                                     ['name', 'version']))
19    pkgtool = ('emerge %s', ('=%s-%s',['name', 'version']))
20
21    def __init__(self, logger, cfg, setup):
22        self._initialised = False
23        Bcfg2.Client.Tools.PkgTool.__init__(self, logger, cfg, setup)
24        self._initialised = True
25        self.__important__ = self.__important__ + ['/etc/make.conf']
26        self._pkg_pattern = re.compile('(.*)-(\d.*)')
27        self._ebuild_pattern = re.compile('(ebuild|binary)')
28        self.cfg = cfg
29        self.installed     = {}
30        self._can_update   = {}
31        self._extras       = []
32        self._deps_checked = 0
33        self._binpkgonly   = True
34        self._get_deps     = False
35
36        # Used to get options from configuration file
37        parser = ConfigParser.ConfigParser()
38        parser.read(self.setup.get('setup'))
39        for opt in ('binpkgonly', 'get_deps'):
40          if parser.has_option(self.name, opt):
41            setattr(self, ('_%s' % opt), \
42                    self._StrToBoolIfBool(parser.get(self.name, opt)))
43
44        if self._binpkgonly:
45          self.pkgtool = self._binpkgtool
46        self.RefreshPackages()
47        self._CheckForUpdates()
48
49    def _StrToBoolIfBool(self, str):
50        """Returns a boolean if the string specifies a boolean value.
51           Returns a string otherwise"""
52        if str.lower() in ('true', 'yes', 't', 'y', '1'):
53          return True
54        elif str.lower() in ('false', 'no', 'f', 'n', '0'):
55          return False
56        else:
57          return str
58
59    def _CheckForUpdates(self):
60        """Checks if packages with version auto can be updated"""
61        check_pkgs = ''
62
63        # Build a list of packages to check
64        for element in self.getSupportedEntries():
65          if element.get('version') == 'auto':
66            check_pkgs += '%s ' % element.get('name')
67          elif (element.get('version') == 'any') and \
68               (not element.get('name') in self.installed):
69            # Add non-installed packages to the list if version unspecified
70            check_pkgs += '%s ' % element.get('name')
71
72        if check_pkgs == '':
73          return
74
75        # Check for package updates in a single pass
76        self.logger.info('Checking for package updates')
77        out = self.cmd.run('/usr/bin/emerge --update --newuse --pretend ' + \
78                  '--nodeps --quiet ' + check_pkgs)
79        if out[0] == 0:
80          for line in out[1]:
81            if not self._ebuild_pattern.search(line):
82              continue
83            # Each line contains a new package version
84            # Output is of the following forms:
85            # [ebuild  U ] category/package-ver [old-ver]
86            # [ebuild  N ] category/package-ver USE="..."
87            # [ebuild  N ] category/package-ver
88            #
89            # Want category/package-ver
90
91            str   = line.split(']')[1]
92            try:
93              str = str.split()[0]
94            except:
95              pass
96            name    = self._pkg_pattern.match(str).group(1)
97            version = self._pkg_pattern.match(str).group(2)
98            self._can_update[name] = version
99        else:
100          if not out[1] == '':
101            self.logger.error('Could not check for package updates. ' + \
102                              'Please run again in debug mode')
103
104    def Install(self, packages, states):
105        """Reworks the package version tags and calls parent Install method"""
106        new_packages = []
107        for pkg in packages:
108          # Create a completely new package list
109          new_pkg = Bcfg2.Client.XML.Element('Package')
110          for attrib in pkg.keys():
111            # Copy across all package attributes
112            new_pkg.set(attrib, pkg.get(attrib))
113            if pkg.get('version') == 'auto' or \
114                      (pkg.get('version') == 'any' and \
115                      (not pkg.get('name') in self.installed)):
116              # Specify a fixed version to be installed
117              new_pkg.set('version', self._can_update[pkg.get('name')])
118          new_packages.append(new_pkg)
119        return Bcfg2.Client.Tools.PkgTool.Install(self, new_packages, states)
120
121    def FindExtraPackages(self):
122        """Conditionally finds extra packages that are not dependencies"""
123        packages        = ''
124        fallback_method = False
125
126        if not self._initialised:
127          # Must run after checking configuration file
128          return []
129        elif not self._get_deps:
130          # Use parent's method instead
131          return Bcfg2.Client.Tools.PkgTool.FindExtraPackages(self)
132        elif self._deps_checked == 2:
133          # Don't check dependencies for every bundle,
134          # they won't change after the second run
135          return self._extras
136        else:
137          while True:
138            # Get list of packages
139            packages = ''
140            for element in self.getSupportedEntries():
141              if element.get('name') in self.installed:
142                packages += ' \=%s-%s' % (element.get('name'), \
143                             self.installed[element.get('name')])
144
145            if packages == '':
146              # No packages specified. Return an empty list
147              return []
148
149            self.logger.info('Getting package dependencies')
150            out = self.cmd.run('/usr/bin/emerge --emptytree --pretend ' + \
151                      '--quiet ' + packages)
152            if not (out[0] == 0):
153              if fallback_method:
154                # This is really bad. To be safe and not possibly remove
155                # required packages, simply return an empty list
156                self.logger.error('Fallback method failed. ' + \
157                                  'Please run again in debug mode')
158                return []
159              else:
160                self.logger.error('Getting dependent packages failed. ' + \
161                                  'Using fallback method')
162                fallback_method = True
163            else:
164              for line in out[1]:
165                if not self._ebuild_pattern.search(line):
166                  continue
167                # Output is one of the following forms:
168                # [ebuild  R  ] category/package-ver
169                # [ebuild  U  ] category/package-ver [old-ver]
170                # [ebuild  R  ] category/package-ver USE="..."
171                # [ebuild  N  ] category/package-ver
172                #
173                # Want category/package-ver
174                atom     = line.split(']')[1]
175                try:
176                  atom   = atom.split()[0]
177                except:
178                  pass
179                name     = self._pkg_pattern.match(atom).group(1)
180                version  = self._pkg_pattern.match(atom).group(2)
181                if not (name in self.installed):
182                  element = Bcfg2.Client.XML.Element('Package', name=name, \
183                                                           version=version)
184                  self._extras.append(element)
185              self._deps_checked += 1
186              return self._extras
187
188    def RefreshPackages(self):
189        """Refresh memory hashes of packages."""
190        if not self._initialised:
191          return
192        self.logger.info('Getting list of installed packages')
193        cache = self.cmd.run("equery -q list '*'")[1]
194        self.installed = {}
195        for pkg in cache:
196            if self._pkg_pattern.match(pkg):
197                name = self._pkg_pattern.match(pkg).group(1)
198                version = self._pkg_pattern.match(pkg).group(2)
199                self.installed[name] = version
200            else:
201                self.logger.info("Failed to parse pkg name %s" % pkg)
202
203    def VerifyPackage(self, entry, modlist):
204        """Verify package for entry."""
205        if not 'version' in entry.attrib:
206            self.logger.info("Cannot verify unversioned package %s" %
207               (entry.get('name')))
208            return False
209
210        if not (entry.get('name') in self.installed):
211          # Can't verify package that isn't installed
212          entry.set('current_exists', 'false')
213          return False
214
215        # Check the installed version
216        version = self.installed[entry.get('name')]
217        if entry.get('version') in ('any', 'auto'):
218          entry.set('current_version', version)
219
220        if not self.setup['quick']:
221          if (not 'verify' in entry.attrib) or \
222             self._StrToBoolIfBool(entry.get('verify')):
223
224            # Check the package if:
225            # - Not running in quick mode
226            # - No verify option is specified in the literal configuration
227            #    OR
228            # - Verify option is specified and is true
229
230            self.logger.debug('Running equery check on %s' % \
231                                          entry.get('name'))
232            output = self.cmd.run("/usr/bin/equery -N check '=%s-%s' "
233                                  "2>&1 | grep '!!!' | awk '{print $2}'"
234                                  % (entry.get('name'), version))[1]
235            if [filename for filename in output \
236                    if filename not in modlist]:
237              return False
238
239        # By now the package must be in one of the following states:
240        # - Not require checking
241        # - Have no files modified at all
242        # - Have modified files in the modlist only
243        if entry.get('version') in ('auto'):
244          if entry.get('name') in self._can_update:
245            # Package requires updating
246            return False
247          else:
248            # Package has been checked and doesn't require an update
249            return True
250        else:
251          if self.installed[entry.get('name')] == version:
252            # Specified package version is installed
253            # Specified package version may be any in literal configuration
254            return True
255          else:
256            # Specified package version is not installed
257            entry.set('current_version', self.installed[entry.get('name')])
258            return False
259
260        # Something got skipped. Indicates a bug
261        return False
262
263    def RemovePackages(self, packages):
264        """Deal with extra configuration detected."""
265        pkgnames = " ".join([pkg.get('name') for pkg in packages])
266        if len(packages) > 0:
267            self.logger.info('Removing packages:')
268            self.logger.info(pkgnames)
269            self.cmd.run("emerge --unmerge --quiet %s" % \
270                         " ".join(pkgnames.split(' ')))
271            self.RefreshPackages()
272            self.extra = self.FindExtraPackages()