add magic header
[platform/upstream/swup.git] / tools / updateinfo / updateinfo.py
1 #!/usr/bin/python
2 import yaml
3 from xml.dom import minidom
4 import rpm
5 import glob
6 from optparse import OptionParser
7 import sys, os
8 import zipfile
9 import hashlib
10 import shutil
11 import fileinput
12
13 def get_checksum(fileName, checksum_type="sha256", excludeLine="", includeLine=""):
14     """Compute sha256 hash of the specified file"""
15     m = hashlib.sha256()
16     if checksum_type == "md5":
17         m = hashlib.md5()
18
19     try:
20         fd = open(fileName,"rb")
21     except IOError:
22         print "Unable to open the file in readmode:", filename
23         return
24     content = fd.readlines()
25     fd.close()
26     for eachLine in content:
27         if excludeLine and eachLine.startswith(excludeLine):
28             continue
29         m.update(eachLine)
30     m.update(includeLine)
31     return m.hexdigest()
32
33 class Updates:
34     def __init__(self, patch = None, updates = None, cache = None):
35         self.doc = None
36         self.pids = []
37         self._check_update(cache)
38
39         if patch and updates:
40             self.add_update(patch, updates)
41
42     def _desc_to_html(self, page, desc):
43         in_ul = False
44         for line in desc.splitlines():
45             if line.startswith('- ') or line.startswith('* '):
46                 if not in_ul:
47                     page.ul()
48                     in_ul = True
49                 page.li(line[2:])
50             else:
51                 if in_ul:
52                     page.ul.close()
53                     in_ul = False
54                 page.p(line)
55         if in_ul:
56             page.ul.close()
57
58     def get_title(self, t, is_html = False):
59         if is_html:
60             import markup
61             page = markup.page()
62             page.init( title = t, style = CSS_STRIKE)
63             page.h1(t)
64             return str(page).rstrip('</html>').rstrip().rstrip('</body>')
65         else:
66             return t
67
68     def get_summary_info(self, sum, is_html = False):
69         if is_html:
70             import markup
71             page = markup.page()
72             page.h2(sum['Title'])
73             self._desc_to_html(page, sum['Description'])
74
75             return str(page)
76
77         else:
78             return '%s\n%s' %(sum['Title'], sum['Description'])
79
80     def get_patch_info(self, update, is_html = False):
81         if is_html:
82             import markup
83             page = markup.page()
84             #page.h3(update['Title'])
85             self._desc_to_html(page, update['Description'])
86
87             if update.has_key("Bugs"):
88                 page.p.open()
89                 page.add('Resolved Bugs: ')
90                 firstone = True
91                 for bug in update['Bugs']:
92                     if firstone:
93                         firstone = False
94                     else:
95                         page.add(', ')
96                     page.a(bug, href='http://bugs.tizen.org/show_bug.cgi?id=%s' %bug, class_="strike_link")
97                 page.p.close()
98
99             if update.has_key("CVEs"):
100                 page.p.open()
101                 page.add('Resolved CVE Issues: ')
102                 firstone = True
103                 for cve in update['CVEs']:
104                     if firstone:
105                         firstone = False
106                     else:
107                         page.add(', ')
108                     page.a(cve, href='http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s\n' % cve, class_="strike_link")
109                 page.p.close()
110
111             return str(page)
112         else:
113             INFO = """
114
115     Patch <%s>:
116         Title: %s
117         Type: %s
118         Project: %s
119         Repository: %s
120         Release: %s
121         Status: %s
122         Packages: %s
123         Description:
124             %s
125     """ % (update['ID'],
126            update['Title'],
127            update['Type'],
128            update['Project'],
129            update['Repository'],
130            update['Release'],
131            update['Status'],
132            ", ".join(update['Packages']),
133            '\n        '.join(update['Description'].splitlines()))
134
135             if update.has_key("CVEs"):
136                 INFO += "    CVEs:\n"
137                 cve_info = ''
138                 for cve in update['CVEs']:
139                     cve_info += '       http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s\n' %cve
140                 INFO += cve_info
141
142             if update.has_key("Bugs"):
143                 INFO += "    Bugs:\n"
144                 bug_info = ''
145                 for bug in update['Bugs']:
146                     bug_info += '       http://bugs.tizen.org/show_bug.cgi?id=%s\n' %bug
147                 INFO += bug_info
148
149             if update.has_key('Reboot') and  update['Reboot']:
150                 INFO += '    NOTE: reboot needed\n'
151             if update.has_key('Relogin') and  update['Relogin']:
152                 INFO += '    NOTE: relogin needed\n'
153             if update.has_key('Restart') and update['Restart']:
154                 INFO += '    NOTE: restart needed\n'
155
156         return INFO
157
158     def _new_doc(self):
159         print "Creating new updates.xml file..."
160         doc = minidom.Document()
161         doc.appendChild(doc.createElement('updates'))
162         self.doc = doc
163
164     def _sanity_check(self):
165         if not self.doc:
166             print 'Empty or invalid updates.xml file.'
167             self._new_doc()
168             return
169
170         # check for duplicate patch entries
171
172     def _check_update(self, cache):
173         if cache:
174             try:
175                 self.doc = cache.doc
176             except AttributeError:
177                 if os.path.exists(cache):
178                     self.doc = minidom.parse(cache)
179             self._sanity_check()
180             self.next =  len(self.doc.getElementsByTagName('update'))
181         else:
182             self._new_doc()
183             self.next = 0
184     def _insert(self, parent, name, attrs={}, text=None, data=None):
185         """ Helper function to trivialize inserting an element into the doc """
186         child = self.doc.createElement(name)
187         for item in attrs.items():
188             child.setAttribute(item[0], unicode(item[1]))
189         if text:
190             txtnode = self.doc.createTextNode(unicode(text))
191             child.appendChild(txtnode)
192         if data:
193             txtnode = self.doc.createCDATASection(unicode(data))
194             child.appendChild(txtnode)
195         parent.appendChild(child)
196         return child
197
198     def _get_notice(self, update_id):
199         return update_id in self.pids
200
201     def add_update(self, update,  location, checksum):
202         """
203         Generate the extended metadata for a given update
204         """
205         import time
206
207         self.next = self.next + 1
208         root = self._insert(self.doc.firstChild, 'update', attrs={
209                 'id'      : update['ID']
210         })
211
212         self._insert(root, 'title', text=update['Title'])
213         self._insert(root, 'type', text=update['Type'])
214         self._insert(root, 'location', attrs={'href': location})
215         self._insert(root, 'checksum', text=checksum)
216         self._insert(root, 'version', text=update['Release'])
217         times = str(time.time()).split(".")
218         issued_time = times[0]
219         self._insert(root, 'issued', attrs={ 'date' : issued_time })
220
221         if update.has_key('Reboot') and  update['Reboot']:
222             self._insert(root, 'reboot_required', text='True')
223         if update.has_key('Relogin') and  update['Relogin']:
224             self._insert(root, 'relogin_required', text='True')
225         if update.has_key('Restart') and update['Restart']:
226             self._insert(root, 'restart_required', text='True')
227
228         html = self.get_patch_info(update, True)
229         self._insert(root, 'description', data=html)
230
231 class UpdateInfo:
232     def __init__(self, patch = None, updates = None, cache = None):
233         self.doc = None
234         self.pids = []
235         self._check_update(cache)
236
237         if patch and updates:
238             self.add_patch(patch, updates)
239
240     def _new_doc(self):
241         print "Creating new updateinfo.xml file..."
242         doc = minidom.Document()
243         doc.appendChild(doc.createElement('updates'))
244
245         self.doc = doc
246
247     def _sanity_check(self):
248         if not self.doc:
249             print 'Empty or invalid updateinfo.xml file.'
250             self._new_doc()
251             return
252
253         # check for duplicate patch entries
254         for u in self.doc.getElementsByTagName('update'):
255             pid =u.getElementsByTagName('id')[0].firstChild.data
256             if pid in self.pids:
257                 print 'Found duplicate update entry: %s' % pid
258             else:
259                 self.pids.append(pid)
260
261     def _check_update(self, cache):
262         if cache:
263             try:
264                 self.doc = cache.doc
265             except AttributeError:
266                 if os.path.exists(cache):
267                     self.doc = minidom.parse(cache)
268             self._sanity_check()
269             self.next =  len(self.doc.getElementsByTagName('update'))
270         else:
271             self._new_doc()
272             self.next = 0
273
274     def _insert(self, parent, name, attrs={}, text=None, data=None):
275         """ Helper function to trivialize inserting an element into the doc """
276         child = self.doc.createElement(name)
277         for item in attrs.items():
278             child.setAttribute(item[0], unicode(item[1]))
279         if text:
280             txtnode = self.doc.createTextNode(unicode(text))
281             child.appendChild(txtnode)
282         if data:
283             txtnode = self.doc.createCDATASection(unicode(data))
284             child.appendChild(txtnode)
285         parent.appendChild(child)
286         return child
287
288     def _get_notice(self, update_id):
289         return update_id in self.pids
290
291     def add_patch(self, update, updates):
292         """
293         Generate the extended metadata for a given update
294         """
295
296         import time
297
298         self.next = self.next + 1
299         root = self._insert(self.doc.firstChild, 'update', attrs={
300                 'type'      : update['Type'],
301                 'status'    : update['Status'],
302                 'version'   : "%04d" %self.next,
303                 'from'      : 'updates@tizen.org'
304         })
305
306         self._insert(root, 'id', text=update['ID'])
307         self._insert(root, 'title', text=update['Title'])
308         self._insert(root, 'release', text=update['Release'])
309         times = str(time.time()).split(".")
310         issued_time = times[0]
311         self._insert(root, 'issued', attrs={ 'date' : issued_time })
312
313         ## Build the references
314         refs = self.doc.createElement('references')
315         if update.has_key("CVEs"):
316             for cve in update['CVEs']:
317                 self._insert(refs, 'reference', attrs={
318                         'type' : 'cve',
319                         'href' : 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s' %cve,
320                         'id'   : cve
321                 })
322
323         if update.has_key("Bugs"):
324             for bug in update['Bugs']:
325                 self._insert(refs, 'reference', attrs={
326                         'type' : 'bugzilla',
327                         'href' : 'http://bugs.tizen.org/show_bug.cgi?id=%s' %bug,
328                         'id'   : bug,
329                         'title': 'Bug number %s' %bug
330                 })
331         root.appendChild(refs)
332
333         ## Errata description
334         self._insert(root, 'description', text=update['Description'])
335
336         ## The package list
337         pkglist = self.doc.createElement('pkglist')
338         collection = self.doc.createElement('collection')
339         #collection.setAttribute('short', update.release.name)
340         #self._insert(collection, 'name', text=update.release.long_name)
341
342         for u in updates:
343             filename = "rpms/%s" % (os.path.basename(u['binary']))
344             if u['header'][rpm.RPMTAG_SOURCEPACKAGE] or 'debuginfo' in u['binary']:
345                 continue
346             pkg = self._insert(collection, 'package', attrs={
347                             'name'      : u['header'][rpm.RPMTAG_NAME],
348                             'version'   : u['header'][rpm.RPMTAG_VERSION],
349                             'release'   : u['header'][rpm.RPMTAG_RELEASE],
350                             'arch'      : u['header'][rpm.RPMTAG_ARCH],
351             })
352             self._insert(pkg, 'filename', text=filename)
353
354             if update.has_key('Reboot') and  update['Reboot']:
355                 self._insert(pkg, 'reboot_suggested', text='True')
356             if update.has_key('Relogin') and  update['Relogin']:
357                 self._insert(pkg, 'relogin_suggested', text='True')
358             if update.has_key('Restart') and update['Restart']:
359                 self._insert(pkg, 'restart_suggested', text='True')
360
361             collection.appendChild(pkg)
362
363         pkglist.appendChild(collection)
364         root.appendChild(pkglist)
365
366 def parse_patch( patch_path):
367     print 'Processing patch file:', patch_path
368     try:
369         stream = file("%s" % ( patch_path), 'r')
370     except IOError:
371         print "Cannot read file: %s/%s" % ( patch_path)
372
373     try:
374         patch = yaml.load(stream)
375     except yaml.scanner.ScannerError, e:
376         print 'syntax error found in yaml: %s' % str(e)
377
378     return patch
379
380 def create_updateinfo(root, patch):
381     ui = UpdateInfo()
382     updates = []
383
384     patch_id = patch['ID']
385
386     packages = glob.glob("%s/%s/rpms/*.rpm" % (root, patch_id) ) + glob.glob("%s/%s/new/*.rpm" % (root, patch_id) )
387     for package in packages:
388         u = {}
389         u['binary'] = package
390         ts = rpm.TransactionSet("/", rpm._RPMVSF_NOSIGNATURES)
391         fd = os.open(package, os.O_RDONLY)
392         header = ts.hdrFromFdno(fd)
393         #print header
394         os.close(fd)
395         u['header'] = header
396         updates.append(u) 
397
398     ui.add_patch(patch, updates)           
399
400     # save to file
401     updateinfo_xml = ui.doc.toxml()
402     f = open("%s/updateinfo.xml" % root, "w")
403     f.write(updateinfo_xml)
404     f.close()
405
406 def create_update_file(target_dir, destination, patch_id):
407     # create zip file
408     shutil.copyfile(patch_path, "%s/%s" %(target_dir, patch_id))
409     zip = zipfile.ZipFile("%s/%s.zip" % (destination, patch_id ), 'w', zipfile.ZIP_DEFLATED)
410     rootlen = len(target_dir) + 1
411     for base, dirs, files in os.walk(target_dir):
412         basedir = os.path.basename(base)
413         if basedir == "rpms":
414             continue
415         for file in files:
416             fn = os.path.join(base, file)
417             zip.write(fn, "%s/%s" % (patch_id, fn[rootlen:]))
418     zip.close()
419     zip_checksum = get_checksum("%s/%s.zip" % (destination, patch_id))
420     return zip_checksum
421
422 def update_metadata(destination, root, updates_file, patch, zip_checksum):
423     # creates updates.xml
424     patch_id = patch['ID']
425     up = Updates(cache=opts.updatesfile)
426     up.add_update(patch, "%s.zip" %patch_id, zip_checksum)
427     # save to file
428     updates_xml = up.doc.toxml()
429     f = open("%s/updates.xml" % root, "w")
430     f.write(updates_xml)
431     f.close()
432
433     if not os.path.exists("%s/data/updatemd.xml" %destination):
434         os.mkdir("%s/data" %destination)
435         updatemd = open("%s/data/repomd.xml" %destination, "w")
436         template = """<?xml version="1.0" encoding="UTF-8"?>
437     <repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
438     </repomd>
439     """
440         updatemd.write(template)
441         updatemd.close()
442     else:
443         for line in fileinput.input("%s/data/updatemd.xml" %destination, inplace=1):
444             print line.replace("updatemd", "repomd"),
445         shutil.copyfile("%s/data/updatemd.xml" %destination, "%s/data/repomd.xml" %destination)
446
447     os.system("modifyrepo --mdtype=updates %s/updates.xml %s/data" % (root, destination))
448     shutil.move("%s/data/repomd.xml" %destination, "%s/data/updatemd.xml" %destination)
449     for line in fileinput.input("%s/data/updatemd.xml" %destination, inplace=1):
450         print line.replace("repodata", "data"),
451     for line in fileinput.input("%s/data/updatemd.xml" %destination, inplace=1):
452         print line.replace("repomd", "updatemd"),
453
454
455
456 parser = OptionParser()
457 parser.add_option('-u', '--updateinfo',  metavar='TEXT',
458               help='cached meta updateinfo file')
459 parser.add_option('-U', '--updatesfile',  metavar='UPDATES',
460               help='master updates.xml file')
461 parser.add_option('-O', '--original',  metavar='ORIGINAL',
462               help='Original and Old package directory')
463
464 parser.add_option('-q', '--quiet', action='store_true',
465               help='do not show downloading progress')
466 parser.add_option('-d', '--destdir', default='.', metavar='DIR',
467               help='Directory where to store the updates.')
468 parser.add_option('-p', '--patch',  metavar='TEXT',
469               help='Patch information')
470 parser.add_option('-P', '--patchdir', metavar='DIR',
471               help='directory with patch files')
472 parser.add_option('-t', '--testing', action='store_true',
473               help='test updates')
474
475 (opts, args) = parser.parse_args()
476
477 root = os.getcwd()
478 if not opts.patch:
479     print "missing options --patch. You need to point to a patch file (YAML format)"
480     sys.exit(1)
481
482 if opts.patchdir:
483     root = opts.patchdir
484
485 patch_path = opts.patch
486 destination = ""
487 if not opts.destdir:
488     destination = root
489 else:
490     destination = opts.destdir
491
492 # create deltas (primary, deltainfo)
493 patch = parse_patch ( patch_path)
494 patch_id = patch['ID']
495 target_dir = "%s/%s" % (root, patch_id)
496
497 os.system("createrepo --deltas --oldpackagedirs=%s %s/%s" % (opts.original, root, patch_id))
498
499 # create updateinfo
500 create_updateinfo(root, patch)
501
502 # update repo
503 os.system("modifyrepo %s/updateinfo.xml %s/%s/repodata"  % (root, root, patch_id))
504
505 zip_checksum = create_update_file(target_dir, destination,  patch_id)
506
507 update_metadata(destination, root, opts.updatesfile, patch, zip_checksum)