Ticket #1086: Portage.3.py

File Portage.3.py, 10.7 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', \
18                                     ['cpv']))
19    pkgtool = ('emerge %s', ('%s',['cpv']))
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 name base on version tags and
106        calls parent Install method"""
107
108        for pkg in packages:
109          # If version is any and not installed
110          #    OR
111          # Version is auto and package can be updated
112          if (pkg.get('version') == 'any' and \
113                (not pkg.get('name') in self.installed)) \
114              or (pkg.get('version') == 'auto' and \
115                (pkg.get('name') in self._can_update)):
116            # Set package CPV to be just the package name
117            pkg.set('cpv', pkg.get('name'))
118          else:
119            # Set package CPV to be a full atom with version included
120            cpv = '=' + pkg.get('name') + '-' + pkg.get('version')
121            pkg.set('cpv', cpv)
122        Bcfg2.Client.Tools.PkgTool.Install(self, packages, states)
123
124        for pkg in packages:
125          # Revert changes
126          if 'cpv' in pkg.keys():
127            del pkg.attrib['cpv']
128
129    def FindExtraPackages(self):
130        """Conditionally finds extra packages that are not dependencies"""
131        packages = 'system'
132
133        if not self._initialised:
134          # Must run after checking configuration file
135          return []
136        elif not self._get_deps:
137          # Use parent's method instead
138          return Bcfg2.Client.Tools.PkgTool.FindExtraPackages(self)
139        elif self._deps_checked == 2:
140          # Don't check dependencies for every bundle,
141          # they won't change after the second run
142          return self._extras
143        else:
144          # Get list of packages
145          for element in self.getSupportedEntries():
146            if element.get('name') in self.installed:
147              packages += ' \=%s-%s' % (element.get('name'), \
148                           self.installed[element.get('name')])
149
150          if packages == 'system':
151            # No packages specified. Return an empty list
152            return []
153
154          self.logger.info('Getting package dependencies')
155          out = self.cmd.run('/usr/bin/emerge --emptytree --pretend ' + \
156                    '--quiet ' + packages)
157          if not (out[0] == 0):
158            # This is really bad. To be safe and not possibly remove
159            # required packages, simply return an empty list
160            self.logger.error('Getting dependent packages failed. ' + \
161                              'Please run again in debug mode')
162            return []
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()