1 | """This is the Bcfg2 tool for the Gentoo Portage system.""" |
---|
2 | __revision__ = '$Revision$' |
---|
3 | |
---|
4 | import re |
---|
5 | import Bcfg2.Client.Tools |
---|
6 | from Bcfg2.Bcfg2Py3k import ConfigParser |
---|
7 | |
---|
8 | class 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() |
---|