Ticket #1086: Portage.2.py

File Portage.2.py, 10.5 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        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        = 'system'
124
125        if not self._initialised:
126          # Must run after checking configuration file
127          return []
128        elif not self._get_deps:
129          # Use parent's method instead
130          return Bcfg2.Client.Tools.PkgTool.FindExtraPackages(self)
131        elif self._deps_checked == 2:
132          # Don't check dependencies for every bundle,
133          # they won't change after the second run
134          return self._extras
135        else:
136          # Get list of packages
137          for element in self.getSupportedEntries():
138            if element.get('name') in self.installed:
139              packages += ' \=%s-%s' % (element.get('name'), \
140                           self.installed[element.get('name')])
141
142          if packages == 'system':
143            # No packages specified. Return an empty list
144            return []
145
146          self.logger.info('Getting package dependencies')
147          out = self.cmd.run('/usr/bin/emerge --emptytree --pretend ' + \
148                    '--quiet ' + packages)
149          if not (out[0] == 0):
150            # This is really bad. To be safe and not possibly remove
151            # required packages, simply return an empty list
152            self.logger.error('Getting dependent packages failed. ' + \
153                              'Please run again in debug mode')
154            return []
155          else:
156            for line in out[1]:
157              if not self._ebuild_pattern.search(line):
158                continue
159              # Output is one of the following forms:
160              # [ebuild  R  ] category/package-ver
161              # [ebuild  U  ] category/package-ver [old-ver]
162              # [ebuild  R  ] category/package-ver USE="..."
163              # [ebuild  N  ] category/package-ver
164              #
165              # Want category/package-ver
166              atom     = line.split(']')[1]
167              try:
168                atom   = atom.split()[0]
169              except:
170                pass
171              name     = self._pkg_pattern.match(atom).group(1)
172              version  = self._pkg_pattern.match(atom).group(2)
173              if not (name in self.installed):
174                element = Bcfg2.Client.XML.Element('Package', name=name, \
175                                                         version=version)
176                self._extras.append(element)
177            self._deps_checked += 1
178            return self._extras
179
180    def RefreshPackages(self):
181        """Refresh memory hashes of packages."""
182        if not self._initialised:
183          return
184        self.logger.info('Getting list of installed packages')
185        cache = self.cmd.run("equery -q list '*'")[1]
186        self.installed = {}
187        for pkg in cache:
188            if self._pkg_pattern.match(pkg):
189                name = self._pkg_pattern.match(pkg).group(1)
190                version = self._pkg_pattern.match(pkg).group(2)
191                self.installed[name] = version
192            else:
193                self.logger.info("Failed to parse pkg name %s" % pkg)
194
195    def VerifyPackage(self, entry, modlist):
196        """Verify package for entry."""
197        if not 'version' in entry.attrib:
198            self.logger.info("Cannot verify unversioned package %s" %
199               (entry.get('name')))
200            return False
201
202        if not (entry.get('name') in self.installed):
203          # Can't verify package that isn't installed
204          entry.set('current_exists', 'false')
205          return False
206
207        # Check the installed version
208        version = self.installed[entry.get('name')]
209        if entry.get('version') in ('any', 'auto'):
210          entry.set('current_version', version)
211
212        if not self.setup['quick']:
213          if (not 'verify' in entry.attrib) or \
214             self._StrToBoolIfBool(entry.get('verify')):
215
216            # Check the package if:
217            # - Not running in quick mode
218            # - No verify option is specified in the literal configuration
219            #    OR
220            # - Verify option is specified and is true
221
222            self.logger.debug('Running equery check on %s' % \
223                                          entry.get('name'))
224            output = self.cmd.run("/usr/bin/equery -N check '=%s-%s' "
225                                  "2>&1 | grep '!!!' | awk '{print $2}'"
226                                  % (entry.get('name'), version))[1]
227            if [filename for filename in output \
228                    if filename not in modlist]:
229              return False
230
231        # By now the package must be in one of the following states:
232        # - Not require checking
233        # - Have no files modified at all
234        # - Have modified files in the modlist only
235        if entry.get('version') in ('auto'):
236          if entry.get('name') in self._can_update:
237            # Package requires updating
238            return False
239          else:
240            # Package has been checked and doesn't require an update
241            return True
242        else:
243          if self.installed[entry.get('name')] == version:
244            # Specified package version is installed
245            # Specified package version may be any in literal configuration
246            return True
247          else:
248            # Specified package version is not installed
249            entry.set('current_version', self.installed[entry.get('name')])
250            return False
251
252        # Something got skipped. Indicates a bug
253        return False
254
255    def RemovePackages(self, packages):
256        """Deal with extra configuration detected."""
257        pkgnames = " ".join([pkg.get('name') for pkg in packages])
258        if len(packages) > 0:
259            self.logger.info('Removing packages:')
260            self.logger.info(pkgnames)
261            self.cmd.run("emerge --unmerge --quiet %s" % \
262                         " ".join(pkgnames.split(' ')))
263            self.RefreshPackages()
264            self.extra = self.FindExtraPackages()