Package yum :: Module update_md
[hide private]
[frames] | no frames]

Source Code for Module yum.update_md

  1  #!/usr/bin/python -t 
  2  # This program is free software; you can redistribute it and/or modify 
  3  # it under the terms of the GNU General Public License as published by 
  4  # the Free Software Foundation; either version 2 of the License, or 
  5  # (at your option) any later version. 
  6  # 
  7  # This program is distributed in the hope that it will be useful, 
  8  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  9  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 10  # GNU Library General Public License for more details. 
 11  # 
 12  # You should have received a copy of the GNU General Public License 
 13  # along with this program; if not, write to the Free Software 
 14  # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 
 15  # Copyright 2005 Duke University  
 16  # 
 17  # Seth Vidal <skvidal@linux.duke.edu> 
 18  # Luke Macken <lmacken@redhat.com> 
 19   
 20  """ 
 21  Update metadata (updateinfo.xml) parsing. 
 22  """ 
 23   
 24  import sys 
 25  import gzip 
 26   
 27  from yum.i18n import utf8_text_wrap, to_utf8 
 28  from yum.yumRepo import YumRepository 
 29  from yum.packages import FakeRepository 
 30  from yum.misc import to_xml 
 31  import Errors 
 32   
 33  import rpmUtils.miscutils 
 34   
 35  try: 
 36      from xml.etree import cElementTree 
 37  except ImportError: 
 38      import cElementTree 
 39  iterparse = cElementTree.iterparse 
 40   
 41   
42 -class UpdateNoticeException(Exception):
43 """ An exception thrown for bad UpdateNotice data. """ 44 pass
45 46
47 -class UpdateNotice(object):
48 49 """ 50 A single update notice (for instance, a security fix). 51 """ 52
53 - def __init__(self, elem=None):
54 self._md = { 55 'from' : '', 56 'type' : '', 57 'title' : '', 58 'release' : '', 59 'status' : '', 60 'version' : '', 61 'pushcount' : '', 62 'update_id' : '', 63 'issued' : '', 64 'updated' : '', 65 'description' : '', 66 'references' : [], 67 'pkglist' : [], 68 'reboot_suggested' : False 69 } 70 71 if elem: 72 self._parse(elem)
73
74 - def __getitem__(self, item):
75 """ Allows scriptable metadata access (ie: un['update_id']). """ 76 return self._md.has_key(item) and self._md[item] or None
77
78 - def __setitem__(self, item, val):
79 self._md[item] = val
80
81 - def __str__(self):
82 head = """ 83 =============================================================================== 84 %(title)s 85 =============================================================================== 86 Update ID : %(update_id)s 87 Release : %(release)s 88 Type : %(type)s 89 Status : %(status)s 90 Issued : %(issued)s 91 """ % self._md 92 93 if self._md['updated'] and self._md['updated'] != self._md['issued']: 94 head += " Updated : %s" % self._md['updated'] 95 96 # Add our bugzilla references 97 bzs = filter(lambda r: r['type'] == 'bugzilla', self._md['references']) 98 if len(bzs): 99 buglist = " Bugs :" 100 for bz in bzs: 101 buglist += " %s%s\n\t :" % (bz['id'], 'title' in bz 102 and ' - %s' % bz['title'] or '') 103 head += buglist[: - 1].rstrip() + '\n' 104 105 # Add our CVE references 106 cves = filter(lambda r: r['type'] == 'cve', self._md['references']) 107 if len(cves): 108 cvelist = " CVEs :" 109 for cve in cves: 110 cvelist += " %s\n\t :" % cve['id'] 111 head += cvelist[: - 1].rstrip() + '\n' 112 113 if self._md['description'] is not None: 114 desc = utf8_text_wrap(self._md['description'], width=64, 115 subsequent_indent=' ' * 12 + ': ') 116 head += "Description : %s\n" % '\n'.join(desc) 117 118 # Get a list of arches we care about: 119 #XXX ARCH CHANGE - what happens here if we set the arch - we need to 120 # pass this in, perhaps 121 arches = set(rpmUtils.arch.getArchList()) 122 123 filelist = " Files :" 124 for pkg in self._md['pkglist']: 125 for file in pkg['packages']: 126 if file['arch'] not in arches: 127 continue 128 filelist += " %s\n\t :" % file['filename'] 129 head += filelist[: - 1].rstrip() 130 131 return head
132
133 - def get_metadata(self):
134 """ Return the metadata dict. """ 135 return self._md
136
137 - def _parse(self, elem):
138 """ 139 Parse an update element:: 140 141 <!ELEMENT update (id, synopsis?, issued, updated, 142 references, description, pkglist)> 143 <!ATTLIST update type (errata|security) "errata"> 144 <!ATTLIST update status (final|testing) "final"> 145 <!ATTLIST update version CDATA #REQUIRED> 146 <!ATTLIST update from CDATA #REQUIRED> 147 """ 148 if elem.tag == 'update': 149 for attrib in ('from', 'type', 'status', 'version'): 150 self._md[attrib] = elem.attrib.get(attrib) 151 for child in elem: 152 if child.tag == 'id': 153 if not child.text: 154 raise UpdateNoticeException("No id element found") 155 self._md['update_id'] = child.text 156 elif child.tag == 'pushcount': 157 self._md['pushcount'] = child.text 158 elif child.tag == 'issued': 159 self._md['issued'] = child.attrib.get('date') 160 elif child.tag == 'updated': 161 self._md['updated'] = child.attrib.get('date') 162 elif child.tag == 'references': 163 self._parse_references(child) 164 elif child.tag == 'description': 165 self._md['description'] = child.text 166 elif child.tag == 'pkglist': 167 self._parse_pkglist(child) 168 elif child.tag == 'title': 169 self._md['title'] = child.text 170 elif child.tag == 'release': 171 self._md['release'] = child.text 172 else: 173 raise UpdateNoticeException('No update element found')
174
175 - def _parse_references(self, elem):
176 """ 177 Parse the update references:: 178 179 <!ELEMENT references (reference*)> 180 <!ELEMENT reference> 181 <!ATTLIST reference href CDATA #REQUIRED> 182 <!ATTLIST reference type (self|cve|bugzilla) "self"> 183 <!ATTLIST reference id CDATA #IMPLIED> 184 <!ATTLIST reference title CDATA #IMPLIED> 185 """ 186 for reference in elem: 187 if reference.tag == 'reference': 188 data = {} 189 for refattrib in ('id', 'href', 'type', 'title'): 190 data[refattrib] = reference.attrib.get(refattrib) 191 self._md['references'].append(data) 192 else: 193 raise UpdateNoticeException('No reference element found')
194
195 - def _parse_pkglist(self, elem):
196 """ 197 Parse the package list:: 198 199 <!ELEMENT pkglist (collection+)> 200 <!ELEMENT collection (name?, package+)> 201 <!ATTLIST collection short CDATA #IMPLIED> 202 <!ATTLIST collection name CDATA #IMPLIED> 203 <!ELEMENT name (#PCDATA)> 204 """ 205 for collection in elem: 206 data = { 'packages' : [] } 207 if 'short' in collection.attrib: 208 data['short'] = collection.attrib.get('short') 209 for item in collection: 210 if item.tag == 'name': 211 data['name'] = item.text 212 elif item.tag == 'package': 213 data['packages'].append(self._parse_package(item)) 214 self._md['pkglist'].append(data)
215
216 - def _parse_package(self, elem):
217 """ 218 Parse an individual package:: 219 220 <!ELEMENT package (filename, sum, reboot_suggested)> 221 <!ATTLIST package name CDATA #REQUIRED> 222 <!ATTLIST package version CDATA #REQUIRED> 223 <!ATTLIST package release CDATA #REQUIRED> 224 <!ATTLIST package arch CDATA #REQUIRED> 225 <!ATTLIST package epoch CDATA #REQUIRED> 226 <!ATTLIST package src CDATA #REQUIRED> 227 <!ELEMENT reboot_suggested (#PCDATA)> 228 <!ELEMENT filename (#PCDATA)> 229 <!ELEMENT sum (#PCDATA)> 230 <!ATTLIST sum type (md5|sha1) "sha1"> 231 """ 232 package = {} 233 for pkgfield in ('arch', 'epoch', 'name', 'version', 'release', 'src'): 234 package[pkgfield] = elem.attrib.get(pkgfield) 235 for child in elem: 236 if child.tag == 'filename': 237 package['filename'] = child.text 238 elif child.tag == 'sum': 239 package['sum'] = (child.attrib.get('type'), child.text) 240 elif child.tag == 'reboot_suggested': 241 self._md['reboot_suggested'] = True 242 return package
243
244 - def xml(self):
245 """Generate the xml for this update notice object""" 246 msg = """ 247 <update from="%s" status="%s" type="%s" version="%s"> 248 <id>%s</id> 249 <title>%s</title> 250 <release>%s</release> 251 <issued date="%s"/> 252 <description>%s</description>\n""" % (to_xml(self._md['from']), 253 to_xml(self._md['status']), to_xml(self._md['type']), 254 to_xml(self._md['version']), to_xml(self._md['update_id']), 255 to_xml(self._md['title']), to_xml(self._md['release']), 256 to_xml(self._md['issued'], attrib=True), 257 to_xml(self._md['description'])) 258 259 if self._md['references']: 260 msg += """ <references>\n""" 261 for ref in self._md['references']: 262 if ref['title']: 263 msg += """ <reference href="%s" id="%s" title="%s" type="%s"/>\n""" % ( 264 to_xml(ref['href'], attrib=True), to_xml(ref['id'], attrib=True), 265 to_xml(ref['title'], attrib=True), to_xml(ref['type'], attrib=True)) 266 else: 267 msg += """ <reference href="%s" id="%s" type="%s"/>\n""" % ( 268 to_xml(ref['href'], attrib=True), to_xml(ref['id'], attrib=True), 269 to_xml(ref['type'], attrib=True)) 270 271 msg += """ </references>\n""" 272 273 if self._md['pkglist']: 274 msg += """ <pkglist>\n""" 275 for coll in self._md['pkglist']: 276 msg += """ <collection short="%s">\n <name>%s</name>\n""" % ( 277 to_xml(coll['short'], attrib=True), 278 to_xml(coll['name'])) 279 280 for pkg in coll['packages']: 281 msg += """ <package arch="%s" name="%s" release="%s" src="%s" version="%s"> 282 <filename>%s</filename> 283 </package>\n""" % (to_xml(pkg['arch'], attrib=True), 284 to_xml(pkg['name'], attrib=True), 285 to_xml(pkg['release'], attrib=True), 286 to_xml(pkg['src'], attrib=True), 287 to_xml(pkg['version'], attrib=True), 288 to_xml(pkg['filename'])) 289 msg += """ </collection>\n""" 290 msg += """ </pkglist>\n""" 291 msg += """</update>\n""" 292 return msg
293
294 -def _rpm_tup_vercmp(tup1, tup2):
295 """ Compare two "std." tuples, (n, a, e, v, r). """ 296 return rpmUtils.miscutils.compareEVR((tup1[2], tup1[3], tup1[4]), 297 (tup2[2], tup2[3], tup2[4]))
298
299 -class UpdateMetadata(object):
300 301 """ 302 The root update metadata object. 303 """ 304
305 - def __init__(self, repos=[]):
306 self._notices = {} 307 self._cache = {} # a pkg nvr => notice cache for quick lookups 308 self._no_cache = {} # a pkg name only => notice list 309 self._repos = [] # list of repo ids that we've parsed 310 for repo in repos: 311 try: # attempt to grab the updateinfo.xml.gz from the repodata 312 self.add(repo) 313 except Errors.RepoMDError: 314 continue # No metadata found for this repo
315
316 - def get_notices(self, name=None):
317 """ Return all notices. """ 318 if name is None: 319 return self._notices.values() 320 return name in self._no_cache and self._no_cache[name] or []
321 322 notices = property(get_notices) 323
324 - def get_notice(self, nvr):
325 """ 326 Retrieve an update notice for a given (name, version, release) string 327 or tuple. 328 """ 329 if type(nvr) in (type([]), type(())): 330 nvr = '-'.join(nvr) 331 return self._cache.has_key(nvr) and self._cache[nvr] or None
332 333 # The problem with the above "get_notice" is that not everyone updates 334 # daily. So if you are at pkg-1, pkg-2 has a security notice, and pkg-3 335 # has a BZ fix notice. All you can see is the BZ notice for the new "pkg-3" 336 # with the above. 337 # So now instead you lookup based on the _installed_ pkg.pkgtup, and get 338 # two notices, in order: [(pkg-3, notice), (pkg-2, notice)] 339 # the reason for the sorting order is that the first match will give you 340 # the minimum pkg you need to move to.
341 - def get_applicable_notices(self, pkgtup):
342 """ 343 Retrieve any update notices which are newer than a 344 given std. pkgtup (name, arch, epoch, version, release) tuple. 345 """ 346 oldpkgtup = pkgtup 347 name = oldpkgtup[0] 348 arch = oldpkgtup[1] 349 ret = [] 350 for notice in self.get_notices(name): 351 for upkg in notice['pkglist']: 352 for pkg in upkg['packages']: 353 if pkg['name'] != name or pkg['arch'] != arch: 354 continue 355 pkgtup = (pkg['name'], pkg['arch'], pkg['epoch'] or '0', 356 pkg['version'], pkg['release']) 357 if _rpm_tup_vercmp(pkgtup, oldpkgtup) <= 0: 358 continue 359 ret.append((pkgtup, notice)) 360 ret.sort(cmp=_rpm_tup_vercmp, key=lambda x: x[0], reverse=True) 361 return ret
362
363 - def add_notice(self, un):
364 """ Add an UpdateNotice object. This should be fully populated with 365 data, esp. update_id and pkglist/packages. """ 366 if not un or not un["update_id"] or un['update_id'] in self._notices: 367 return 368 369 self._notices[un['update_id']] = un 370 for pkg in un['pkglist']: 371 for filedata in pkg['packages']: 372 self._cache['%s-%s-%s' % (filedata['name'], 373 filedata['version'], 374 filedata['release'])] = un 375 no = self._no_cache.setdefault(filedata['name'], set()) 376 no.add(un)
377
378 - def add(self, obj, mdtype='updateinfo'):
379 """ Parse a metadata from a given YumRepository, file, or filename. """ 380 if not obj: 381 raise UpdateNoticeException 382 if type(obj) in (type(''), type(u'')): 383 infile = obj.endswith('.gz') and gzip.open(obj) or open(obj, 'rt') 384 elif isinstance(obj, YumRepository): 385 if obj.id not in self._repos: 386 self._repos.append(obj.id) 387 md = obj.retrieveMD(mdtype) 388 if not md: 389 raise UpdateNoticeException() 390 infile = gzip.open(md) 391 elif isinstance(obj, FakeRepository): 392 raise Errors.RepoMDError, "No updateinfo for local pkg" 393 else: # obj is a file object 394 infile = obj 395 396 for event, elem in iterparse(infile): 397 if elem.tag == 'update': 398 try: 399 un = UpdateNotice(elem) 400 except UpdateNoticeException, e: 401 print >> sys.stderr, "An update notice is broken, skipping." 402 # what else should we do? 403 continue 404 self.add_notice(un)
405
406 - def __unicode__(self):
407 ret = u'' 408 for notice in self.notices: 409 ret += unicode(notice) 410 return ret
411 - def __str__(self):
412 return to_utf8(self.__unicode__())
413
414 - def xml(self, fileobj=None):
415 msg = """<?xml version="1.0"?>\n<updates>""" 416 if fileobj: 417 fileobj.write(msg) 418 419 for notice in self._notices.values(): 420 if fileobj: 421 fileobj.write(notice.xml()) 422 else: 423 msg += notice.xml() 424 425 end = """</updates>\n""" 426 if fileobj: 427 fileobj.write(end) 428 else: 429 msg += end 430 431 if fileobj: 432 return 433 434 return msg
435 436
437 -def main():
438 """ update_md test function. """ 439 import yum.misc 440 441 yum.misc.setup_locale() 442 def usage(): 443 print >> sys.stderr, "Usage: %s <update metadata> ..." % sys.argv[0] 444 sys.exit(1)
445 446 if len(sys.argv) < 2: 447 usage() 448 449 try: 450 print sys.argv[1] 451 um = UpdateMetadata() 452 for srcfile in sys.argv[1:]: 453 um.add(srcfile) 454 print unicode(um) 455 except IOError: 456 print >> sys.stderr, "%s: No such file:\'%s\'" % (sys.argv[0], 457 sys.argv[1:]) 458 usage() 459 460 if __name__ == '__main__': 461 main() 462