Initial addition of the systemd update mechanism. accepted/trunk/20130310.193633 submit/trunk/20130310.193459
authorWilliam Douglas <william.douglas@intel.com>
Wed, 27 Feb 2013 21:39:49 +0000 (13:39 -0800)
committerAnas Nashif <anas.nashif@intel.com>
Sun, 10 Mar 2013 19:34:47 +0000 (12:34 -0700)
Create target and service for system-update. Also add update script
that just removes the /system-update symlink and reboots (for now).

Signed-off-by: William Douglas <william.douglas@intel.com>
Makefile
packaging/swup.changes
packaging/swup.spec
swup.py
system-update.service [new file with mode: 0644]
system-update.sh [new file with mode: 0755]
system-update.target [new file with mode: 0644]
tools/getpacs/getpacs
tools/updateinfo/markup.py [new file with mode: 0644]
tools/updateinfo/updateinfo.py

index 2353027..9655ebb 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,8 @@
+all:
 
 install:
        install -D -m 755 swup.py ${DESTDIR}/usr/bin/swup
+       install -D -m 755 system-update.sh ${DESTDIR}/usr/bin/system-update
        install -D -m 755 tools/updateinfo/updateinfo.py ${DESTDIR}/usr/bin/updateinfo
+       install -D -m 644 system-update.target ${DESTDIR}/usr/lib/systemd/system/system-update.target
+       install -D -m 644 system-update.service ${DESTDIR}/usr/lib/systemd/system/system-update.service
index a8494f8..1c2cfc0 100644 (file)
@@ -1,3 +1,12 @@
+* Sun Mar 10 2013 Anas Nashif <anas.nashif@intel.com> accepted/trunk/20130305.082416@6c0cc3e
+- Bump version: 0.2
+
+* Tue Mar 05 2013 Anas Nashif <anas.nashif@intel.com> submit/trunk/20130305.063957@1b22b82
+- do not install system-update.target
+
+* Mon Mar 04 2013 Anas Nashif <anas.nashif@intel.com> submit/trunk/20130227.214201@9a3dcbd
+- Add systemd service
+
 * Mon Feb 25 2013 Anas Nashif <anas.nashif@intel.com> submit/trunk/20130225.214101@faf331d
 - Do not be verbose
 
index d2bec7b..e1e43b3 100644 (file)
@@ -1,11 +1,12 @@
 Name:           swup
-Version:        0.1
+Version:        0.2
 Release:        0
 License:        GPL-2.0+
 Summary:        Software Update Tool
 Url:            http://www.tizen.org
 Group:          System/Management
 Source:         %{name}-%{version}.tar.bz2
+BuildRequires:  systemd
 Requires:       deltarpm
 Requires:       python-lxml
 Requires:       python-yaml
@@ -21,11 +22,17 @@ Software Update Tool.
 
 %install
 %make_install
+rm -f %{buildroot}%{_unitdir}/system-update.target
+%install_service system-update.target.wants system-update.service
 
 %files
 %defattr(-,root,root)
 %{_bindir}/swup
+%{_bindir}/system-update
 %{_bindir}/updateinfo
+%{_unitdir}/system-update.service
+#%{_unitdir}/system-update.target
+%{_unitdir}/system-update.target.wants/system-update.service
 
 %changelog
 
diff --git a/swup.py b/swup.py
index 9742720..4da1d10 100755 (executable)
--- a/swup.py
+++ b/swup.py
@@ -15,9 +15,8 @@ import rpm
 import subprocess as sub
 import distutils
 
-update_repo="http://anashif-desktop.jf.intel.com/~anashif/tizen-pc/updates"
-update_cache="/var/cache/updates"
-
+update_repo="http://10.0.0.140/~anashif/updates"
+update_cache="/var/cache/updatemanager"
 
 
 class FakeSecHead(object):
@@ -61,14 +60,14 @@ def probe_updates():
     print "Checking for new updates..."
     response = urllib2.urlopen("%s/data/updatemd.xml" % update_repo )
     updatemd = response.read()
-    if not os.path.exists("%s/data" %update_cache):
-        os.mkdir("%s/data" %update_cache)
+    if not os.path.exists("%s/download/metadata" %update_cache):
+        os.mkdir("%s/download/metadata" %update_cache)
 
-    fp = open("%s/data/updatemd.xml" % update_cache , "w")
+    fp = open("%s/download/metadata/updatemd.xml" % update_cache , "w")
     fp.write(updatemd)
     fp.close()
 
-    updatemd_local = open("%s/data/updatemd.xml" % update_cache )
+    updatemd_local = open("%s/download/metadata/updatemd.xml" % update_cache )
     root = etree.XML(updatemd_local.read())
     data = root.xpath("//data[@type='updates']")[0]
     loc = data.xpath("location")[0]
@@ -76,8 +75,8 @@ def probe_updates():
     chksum = data.xpath("checksum")[0]
     chksum_type = chksum.attrib['type']
     
-    if os.path.exists("%s/data/updates.xml" % update_cache):
-        cur_sum = checksum("%s/data/updates.xml" % update_cache, checksum_type=chksum_type) 
+    if os.path.exists("%s/download/metadata/updates.xml" % update_cache):
+        cur_sum = checksum("%s/download/metadata/updates.xml" % update_cache, checksum_type=chksum_type) 
         if cur_sum ==  chksum.text:
             print "Using file from cache, no new updates on server."
         else:
@@ -90,8 +89,7 @@ def probe_updates():
 def parse_updates():
 
     updates = {}
-
-    fp = open("%s/data/updates.xml" % update_cache , "r")
+    fp = open("%s/download/metadata/updates.xml" % update_cache , "r")
     updates_root = etree.XML(fp.read())
     updates_el = updates_root.xpath("//update")
     for update in updates_el:
@@ -110,22 +108,22 @@ def parse_updates():
 def download_update(update_data):
     u = update_data
     location = u['location']
-    if not os.path.exists("%s/downloads" % (update_cache)):
-        os.mkdir("%s/downloads" % (update_cache))
-    if not os.path.exists("%s/downloads/%s" % (update_cache,location)):
+    if not os.path.exists("%s/download" % (update_cache)):
+        os.mkdir("%s/download" % (update_cache))
+    if not os.path.exists("%s/download/%s/download_done" % (update_cache, u['id'] )):
         print "Downloading %s/%s" % (update_repo, location)
         update_file = urllib2.urlopen("%s/%s" % (update_repo, location) )
         location = os.path.basename(location)
         announced_csum = u['checksum']
         update_raw = update_file.read()
-        fp = open("%s/downloads/%s" % (update_cache,location) , "w")
+        fp = open("%s/download/%s" % (update_cache,location) , "w")
         fp.write(update_raw)
         fp.close()
-        downloaded_csum = checksum("%s/downloads/%s" % (update_cache,location), "sha256")
+        downloaded_csum = checksum("%s/download/%s" % (update_cache,location), "sha256")
         # Verify Checksum
         if downloaded_csum != announced_csum:
             print "Error: Checksum mismatch"
-            os.remove("%s/downloads/%s" % (update_cache,location))
+            os.remove("%s/download/%s" % (update_cache,location))
     else:
         print "%s already downloaded" % location    
 
@@ -145,13 +143,13 @@ def get_new_update_list(location):
     up = urllib2.urlopen("%s/%s" % (update_repo, location) )
     import gzip
     update_raw = up.read()
-    fp = open("%s/data/updates.xml.gz" % update_cache , "w")
+    fp = open("%s/download/metadata/updates.xml.gz" % update_cache , "w")
     fp.write(update_raw)
     fp.close()
-    f = gzip.open("%s/data/updates.xml.gz" % update_cache, 'rb')
+    f = gzip.open("%s/download/metadata/updates.xml.gz" % update_cache, 'rb')
     file_content = f.read()
     f.close()
-    fp = open("%s/data/updates.xml" % update_cache , "w")
+    fp = open("%s/download/metadata/updates.xml" % update_cache , "w")
     fp.write(file_content)
     fp.close()
 
@@ -186,32 +184,34 @@ def pack(target):
     print_info('%s.zip' %target)
 
 def unpack(location, update_id):
-    os.mkdir("%s/downloads/%s" %(update_cache, update_id))
-    zfile = zipfile.ZipFile("%s/downloads/%s" % (update_cache,location))
+    #os.mkdir("%s/download/%s" %(update_cache, update_id))
+    zfile = zipfile.ZipFile("%s/download/%s/%s" % (update_cache,update_id, location))
     for name in zfile.namelist():            
         (dirname, filename) = os.path.split(name)
-        #print "Decompressing " + filename + " on " + dirname
-        if not os.path.exists("%s/downloads/%s" % (update_cache, dirname)):
-            os.mkdir("%s/downloads/%s" % (update_cache, dirname))            
+        print "Decompressing " + filename + " on " + dirname
+        if not os.path.exists("%s/download/%s/%s" % (update_cache, update_id, dirname)):
+            os.mkdir("%s/download/%s/%s" % (update_cache, update_id, dirname))            
         if filename != "":
-            fd = open("%s/downloads/%s" % (update_cache, name),"w")
+            fd = open("%s/download/%s/%s" % (update_cache, update_id, name),"w")
             fd.write(zfile.read(name))
             fd.close()
 
+    os.rename("%s/download/%s/%s" % (update_cache,update_id, update_id), "%s/download/%s/content" % (update_cache,update_id))
+
 
 def prepare_update(update_data, download):
     u = update_data
     location = u['location']
     update_id = u['id']
     # unzip
-    if os.path.exists("%s/downloads/%s" % (update_cache,location)) and not os.path.exists("%s/downloads/%s" % (update_cache,update_id)):
+    if os.path.exists("%s/download/%s" % (update_cache,update_id)) and not os.path.exists("%s/download/%s/content" % (update_cache,update_id)):
         print "Unpacking %s" %location
         unpack(location, update_id)
     else:
         print "Update %s already unpacked" % update_id
 
     repodir = "%s/repos.d" %update_cache
-    repourl = "file://%s/downloads/%s" % (update_cache, update_id)
+    repourl = "file://%s/download/%s/content" % (update_cache, update_id)
     if not os.path.exists("%s/%s.repo" % (repourl, update_id)):
         os.system("zypper --quiet --reposd-dir %s ar --no-gpgcheck --no-keep-packages %s %s" %(repodir, repourl, update_id))
     if not download:
@@ -222,18 +222,24 @@ def install_update(update_data):
     location = u['location']
     update_id = u['id']
     # unzip
-    if not os.path.exists("%s/downloads/%s" % (update_cache,update_id)):
+    if not os.path.exists("%s/download/%s/content" % (update_cache,update_id)):
         prepare_update(update_data, False)
 
     repodir = "%s/repos.d" %update_cache
-    repourl = "file://%s/downloads/%s" % (update_cache, update_id)
-    if os.path.exists("%s/%s.repo" % (repourl, update_id)):
+    repourl = "file://%s/download/%s/content" % (update_cache, update_id)
+    if not os.path.exists("%s/%s.repo" % (repodir, update_id)):
         os.system("zypper --quiet --reposd-dir %s ar --no-gpgcheck --no-keep-packages %s %s" %(repodir, repourl, update_id))
-    os.system("zypper --quiet  --non-interactive --reposd-dir %s patch --repo %s " % (repodir, update_id) )
+    print "zypper -n  --reposd-dir %s patch --with-interactive  --repo %s " % (repodir, update_id) 
+    os.system("zypper -n  --reposd-dir %s patch --with-interactive --repo %s " % (repodir, update_id) )
     if not os.path.exists("%s/installed" % (update_cache)):
         os.mkdir("%s/installed" % (update_cache))
-    shutil.copyfile("%s/downloads/%s/%s" %(update_cache, update_id, update_id), "%s/installed/%s" % (update_cache, update_id))
+    shutil.copyfile("%s/download/%s/content/%s" %(update_cache, update_id, update_id), "%s/installed/%s" % (update_cache, update_id))
 
+    print "Finished installing %s." % update_id 
+    for line in fileinput.input("/etc/os-release", inplace=1):
+        print line.replace(current_version, u["version"]),
+    for line in fileinput.input("/etc/tizen-release", inplace=1):
+        print line.replace(current_version, u["version"]),
 
 def apply_update(update_data):
     pass
@@ -281,7 +287,11 @@ parser.add_option("-q", "--quiet",
 
 if not os.path.exists(update_cache):
     os.mkdir("%s" % update_cache)
-    os.mkdir("%s/downloads" % update_cache)
+if not os.path.exists("%s/download" % update_cache):
+    os.mkdir("%s/download" % update_cache)
+    os.mkdir("%s/download/metadata" % update_cache)
+
+if not os.path.exists("%s/repos.d" % update_cache):
     os.mkdir("%s/repos.d" % update_cache)
 
 if options.osver:
@@ -299,15 +309,15 @@ if options.downloadonly:
 if options.prepare is not None:
     probe_updates()
     updates = parse_updates()
-    if not updates.has_key(options.install):
-        print "%s is not available for installation. Abort." %options.install
+    if not updates.has_key(options.prepare):
+        print "%s is not available for installation. Abort." %options.prepare
         sys.exit()
-    u = updates[options.install]
+    u = updates[options.prepare]
     download_update(u)
     prepare_update(u, False)
 
 if options.install is not None:
-    probe_updates()
+    #probe_updates()
     updates = parse_updates()
     if not updates.has_key(options.install):
         print "%s is not available for installation. Abort." %options.install
diff --git a/system-update.service b/system-update.service
new file mode 100644 (file)
index 0000000..9ba9d73
--- /dev/null
@@ -0,0 +1,13 @@
+[Unit]
+Description=Tizen System Updater
+DefaultDependencies=no
+OnFailure=reboot.target
+Requires=systemd-remount-fs.service connman.service
+After=systemd-remount-fs.service connman.service
+
+[Service]
+Type=oneshot
+#StandardInput=tty-force
+#StandardOutput=tty-force
+RemainAfterExit=no
+ExecStart=/usr/bin/system-update
diff --git a/system-update.sh b/system-update.sh
new file mode 100755 (executable)
index 0000000..7c6bff6
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+. /etc/os-release
+rm /system-update
+
+# take btrfs snapshot
+
+# call updater
+
+for i in `ls /var/cache/updatemanager/install`; do
+       UPDATE=$(echo $i | sed -e 's/^[0-9]*-//')
+       mkdir -p /var/cache/zypp/packages/$UPDATE/rpms
+       /usr/bin/swup -i  $UPDATE | tee /var/log/system-update.log
+       rm /var/cache/updatemanager/install/$i
+done
+
+# check update status
+# update failed revert to snapshot
+
+# reboot
+/sbin/reboot
diff --git a/system-update.target b/system-update.target
new file mode 100644 (file)
index 0000000..7f337ad
--- /dev/null
@@ -0,0 +1,5 @@
+[Unit]
+Description=System Update
+Documentation=man:systemd.special(7)
+Requires=sysinit.target
+After=sysinit.target
index 03a75cd..e1448f3 100755 (executable)
@@ -2,14 +2,18 @@
 
 BUILD_OLD=$1
 BUILD_NEW=$2
+if [ -z "$BUILD_OLD" -o -z "$BUILD_NEW" ]; then
+    echo "You need to provide old and new snapshot IDs"
+    exit 1
+fi
 USER="nashif"
 PASS=""
 WGET="/usr/bin/wget -q --timestamping "
 DAILY="/pc/releases/daily/trunk"
 WEEKLY="/pc/releases/weekly/trunk"
-SNAPSHOTS="/${RELEASE_TYPE}"
+SNAPSHOTS="/snapshots/trunk/pc/"
 
-RELEASE_TYPE=$DAILY
+RELEASE_TYPE=$SNAPSHOTS
 
 BASE_DIR=$PWD
 wget  https://$USER:$PASS@download.tz.otcshare.org/${RELEASE_TYPE}/${BUILD_OLD}/images/gnome/gnome-${BUILD_OLD}.packages -O old
@@ -49,4 +53,4 @@ done
 IFS=$OLD_IFS
 
 
-rm $BASE_DIR/new $BASE_DIR/old $BASE_DIR/p0 $BASE_DIR/{p1,new_packages}
+#rm $BASE_DIR/new $BASE_DIR/old $BASE_DIR/p0 $BASE_DIR/{p1,new_packages}
diff --git a/tools/updateinfo/markup.py b/tools/updateinfo/markup.py
new file mode 100644 (file)
index 0000000..d5e9a6a
--- /dev/null
@@ -0,0 +1,527 @@
+# This code is in the public domain, it comes
+# with absolutely no warranty and you can do
+# absolutely whatever you want with it.
+
+__date__ = '1 October 2012'
+__version__ = '1.9'
+__doc__= """
+This is markup.py - a Python module that attempts to
+make it easier to generate HTML/XML from a Python program
+in an intuitive, lightweight, customizable and pythonic way.
+
+The code is in the public domain.
+
+Version: %s as of %s.
+
+Documentation and further info is at http://markup.sourceforge.net/
+
+Please send bug reports, feature requests, enhancement
+ideas or questions to nogradi at gmail dot com.
+
+Installation: drop markup.py somewhere into your Python path.
+""" % ( __version__, __date__ )
+
+try:
+    basestring
+    import string
+except:
+    # python 3
+    basestring = str
+    string = str
+
+# tags which are reserved python keywords will be referred 
+# to by a leading underscore otherwise we end up with a syntax error
+import keyword
+
+class element:
+    """This class handles the addition of a new element."""
+
+    def __init__( self, tag, case='lower', parent=None ):
+        self.parent = parent
+
+        if case == 'upper':
+            self.tag = tag.upper( )
+        elif case == 'lower':
+            self.tag = tag.lower( )
+        elif case =='given':
+            self.tag = tag
+        else:
+            self.tag = tag
+    
+    def __call__( self, *args, **kwargs ):
+        if len( args ) > 1:
+            raise ArgumentError( self.tag )
+
+        # if class_ was defined in parent it should be added to every element
+        if self.parent is not None and self.parent.class_ is not None:
+            if 'class_' not in kwargs:
+                kwargs['class_'] = self.parent.class_
+            
+        if self.parent is None and len( args ) == 1:
+            x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
+            return '\n'.join( x )
+        elif self.parent is None and len( args ) == 0:
+            x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
+            return '\n'.join( x )
+            
+        if self.tag in self.parent.twotags:
+            for myarg, mydict in _argsdicts( args, kwargs ):
+                self.render( self.tag, False, myarg, mydict )
+        elif self.tag in self.parent.onetags:
+            if len( args ) == 0:
+                for myarg, mydict in _argsdicts( args, kwargs ):
+                    self.render( self.tag, True, myarg, mydict )    # here myarg is always None, because len( args ) = 0
+            else:
+                raise ClosingError( self.tag )
+        elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
+            raise DeprecationError( self.tag )
+        else:
+            raise InvalidElementError( self.tag, self.parent.mode )
+    
+    def render( self, tag, single, between, kwargs ):
+        """Append the actual tags to content."""
+
+        out = "<%s" % tag
+        for key, value in list( kwargs.items( ) ):
+            if value is not None:               # when value is None that means stuff like <... checked>
+                key = key.strip('_')            # strip this so class_ will mean class, etc.
+                if key == 'http_equiv':         # special cases, maybe change _ to - overall?
+                    key = 'http-equiv'
+                elif key == 'accept_charset':
+                    key = 'accept-charset'
+                out = "%s %s=\"%s\"" % ( out, key, escape( value ) )
+            else:
+                out = "%s %s" % ( out, key )
+        if between is not None:
+            out = "%s>%s</%s>" % ( out, between, tag )
+        else:
+            if single:
+                out = "%s />" % out
+            else:
+                out = "%s>" % out
+        if self.parent is not None:
+            self.parent.content.append( out )
+        else:
+            return out
+    
+    def close( self ):
+        """Append a closing tag unless element has only opening tag."""
+
+        if self.tag in self.parent.twotags:
+            self.parent.content.append( "</%s>" % self.tag )
+        elif self.tag in self.parent.onetags:
+            raise ClosingError( self.tag )
+        elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
+            raise DeprecationError( self.tag )
+
+    def open( self, **kwargs ):
+        """Append an opening tag."""
+
+        if self.tag in self.parent.twotags or self.tag in self.parent.onetags:
+            self.render( self.tag, False, None, kwargs )
+        elif self.mode == 'strict_html' and self.tag in self.parent.deptags:
+            raise DeprecationError( self.tag )
+
+class page:
+    """This is our main class representing a document. Elements are added
+    as attributes of an instance of this class."""
+
+    def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ):
+        """Stuff that effects the whole document.
+
+        mode -- 'strict_html'   for HTML 4.01 (default)
+                'html'          alias for 'strict_html'
+                'loose_html'    to allow some deprecated elements
+                'xml'           to allow arbitrary elements
+
+        case -- 'lower'         element names will be printed in lower case (default)
+                'upper'         they will be printed in upper case
+                'given'         element names will be printed as they are given
+
+        onetags --              list or tuple of valid elements with opening tags only
+        twotags --              list or tuple of valid elements with both opening and closing tags
+                                these two keyword arguments may be used to select
+                                the set of valid elements in 'xml' mode
+                                invalid elements will raise appropriate exceptions
+        
+        separator --            string to place between added elements, defaults to newline
+        
+        class_ --               a class that will be added to every element if defined"""
+        
+        valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ]
+        valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON",
+                "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET",
+                "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS",
+                "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP",
+                "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE",
+                "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR",
+                "TT", "UL", "VAR" ]
+        deprecated_onetags = [ "BASEFONT", "ISINDEX" ]
+        deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ]
+
+        self.header = [ ]
+        self.content = [ ]
+        self.footer = [ ]
+        self.case = case
+        self.separator = separator
+
+        # init( ) sets it to True so we know that </body></html> has to be printed at the end
+        self._full = False
+        self.class_= class_
+
+        if mode == 'strict_html' or mode == 'html':
+            self.onetags = valid_onetags
+            self.onetags += list( map( string.lower, self.onetags ) )
+            self.twotags = valid_twotags
+            self.twotags += list( map( string.lower, self.twotags ) )
+            self.deptags = deprecated_onetags + deprecated_twotags
+            self.deptags += list( map( string.lower, self.deptags ) )
+            self.mode = 'strict_html'
+        elif mode == 'loose_html':
+            self.onetags = valid_onetags + deprecated_onetags 
+            self.onetags += list( map( string.lower, self.onetags ) )
+            self.twotags = valid_twotags + deprecated_twotags
+            self.twotags += list( map( string.lower, self.twotags ) )
+            self.mode = mode
+        elif mode == 'xml':
+            if onetags and twotags:
+                self.onetags = onetags
+                self.twotags = twotags
+            elif ( onetags and not twotags ) or ( twotags and not onetags ):
+                raise CustomizationError( )
+            else:
+                self.onetags = russell( )
+                self.twotags = russell( )
+            self.mode = mode
+        else:
+            raise ModeError( mode )
+
+    def __getattr__( self, attr ):
+
+        # tags should start with double underscore
+        if attr.startswith("__") and attr.endswith("__"):
+            raise AttributeError( attr )
+        # tag with single underscore should be a reserved keyword
+        if attr.startswith( '_' ):
+            attr = attr.lstrip( '_' ) 
+            if attr not in keyword.kwlist:
+                raise AttributeError( attr )
+
+        return element( attr, case=self.case, parent=self )
+
+    def __str__( self ):
+        
+        if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ):
+            end = [ '</body>', '</html>' ]
+        else:
+            end = [ ]
+
+        return self.separator.join( self.header + self.content + self.footer + end )
+
+    def __call__( self, escape=False ):
+        """Return the document as a string.
+
+        escape --   False   print normally
+                    True    replace < and > by &lt; and &gt;
+                            the default escape sequences in most browsers"""
+
+        if escape:
+            return _escape( self.__str__( ) )
+        else:
+            return self.__str__( )
+
+    def add( self, text ):
+        """This is an alias to addcontent."""
+        self.addcontent( text )
+
+    def addfooter( self, text ):
+        """Add some text to the bottom of the document"""
+        self.footer.append( text )
+
+    def addheader( self, text ):
+        """Add some text to the top of the document"""
+        self.header.append( text )
+
+    def addcontent( self, text ):
+        """Add some text to the main part of the document"""
+        self.content.append( text )
+
+
+    def init( self, lang='en', css=None, metainfo=None, title=None, header=None,
+              footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ):
+        """This method is used for complete documents with appropriate
+        doctype, encoding, title, etc information. For an HTML/XML snippet
+        omit this method.
+
+        lang --     language, usually a two character string, will appear
+                    as <html lang='en'> in html mode (ignored in xml mode)
+        
+        css --      Cascading Style Sheet filename as a string or a list of
+                    strings for multiple css files (ignored in xml mode)
+
+        metainfo -- a dictionary in the form { 'name':'content' } to be inserted
+                    into meta element(s) as <meta name='name' content='content'>
+                    (ignored in xml mode)
+        
+        base     -- set the <base href="..."> tag in <head>
+        
+        bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added
+                    as attributes of the <body> element as <body key='value' ... >
+                    (ignored in xml mode)
+
+        script --   dictionary containing src:type pairs, <script type='text/type' src=src></script>
+                    or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all
+
+        title --    the title of the document as a string to be inserted into
+                    a title element as <title>my title</title> (ignored in xml mode)
+
+        header --   some text to be inserted right after the <body> element
+                    (ignored in xml mode)
+
+        footer --   some text to be inserted right before the </body> element
+                    (ignored in xml mode)
+
+        charset --  a string defining the character set, will be inserted into a
+                    <meta http-equiv='Content-Type' content='text/html; charset=myset'>
+                    element (ignored in xml mode)
+
+        encoding -- a string defining the encoding, will be put into to first line of
+                    the document as <?xml version='1.0' encoding='myencoding' ?> in
+                    xml mode (ignored in html mode)
+
+        doctype --  the document type string, defaults to
+                    <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>
+                    in html mode (ignored in xml mode)"""
+
+        self._full = True
+
+        if self.mode == 'strict_html' or self.mode == 'loose_html':
+            if doctype is None:
+                doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>"
+            self.header.append( doctype )
+            self.html( lang=lang )
+            self.head( )
+            if charset is not None:
+                self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset )
+            if metainfo is not None:
+                self.metainfo( metainfo )
+            if css is not None:
+                self.css( css )
+            if title is not None:
+                self.title( title )
+            if script is not None:
+                self.scripts( script )
+            if base is not None:
+                self.base( href='%s' % base )
+            self.head.close()
+            if bodyattrs is not None:
+                self.body( **bodyattrs )
+            else:
+                self.body( )
+            if header is not None:
+                self.content.append( header )
+            if footer is not None:
+                self.footer.append( footer )
+
+        elif self.mode == 'xml':
+            if doctype is None:
+                if encoding is not None:
+                    doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding
+                else:
+                    doctype = "<?xml version='1.0' ?>"
+            self.header.append( doctype )
+
+    def css( self, filelist ):
+        """This convenience function is only useful for html.
+        It adds css stylesheet(s) to the document via the <link> element."""
+      
+        if isinstance( filelist, basestring ):
+            self.link( href=filelist, rel='stylesheet', type='text/css', media='all' )
+        else:
+            for file in filelist:
+                self.link( href=file, rel='stylesheet', type='text/css', media='all' )
+
+    def metainfo( self, mydict ):
+        """This convenience function is only useful for html.
+        It adds meta information via the <meta> element, the argument is
+        a dictionary of the form { 'name':'content' }."""
+
+        if isinstance( mydict, dict ):
+            for name, content in list( mydict.items( ) ):
+                self.meta( name=name, content=content )
+        else:
+            raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." )
+
+    def scripts( self, mydict ):
+        """Only useful in html, mydict is dictionary of src:type pairs or a list
+        of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type.
+        Will be rendered as <script type='text/type' src=src></script>"""
+
+        if isinstance( mydict, dict ):
+            for src, type in list( mydict.items( ) ):
+                self.script( '', src=src, type='text/%s' % type )
+        else:
+            try:
+                for src in mydict:
+                    self.script( '', src=src, type='text/javascript' )
+            except:
+                raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." )
+
+
+class _oneliner:
+    """An instance of oneliner returns a string corresponding to one element.
+    This class can be used to write 'oneliners' that return a string
+    immediately so there is no need to instantiate the page class."""
+    
+    def __init__( self, case='lower' ):
+        self.case = case
+    
+    def __getattr__( self, attr ):
+        
+        # tags should start with double underscore
+        if attr.startswith("__") and attr.endswith("__"):
+            raise AttributeError( attr )
+        # tag with single underscore should be a reserved keyword
+        if attr.startswith( '_' ):
+            attr = attr.lstrip( '_' ) 
+            if attr not in keyword.kwlist:
+                raise AttributeError( attr )
+        
+        return element( attr, case=self.case, parent=None )
+
+oneliner = _oneliner( case='lower' )
+upper_oneliner = _oneliner( case='upper' )
+given_oneliner = _oneliner( case='given' )
+
+def _argsdicts( args, mydict ):
+    """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1."""
+    
+    if len( args ) == 0:
+        args = None, 
+    elif len( args ) == 1:
+        args = _totuple( args[0] )
+    else:
+        raise Exception( "We should have never gotten here." )
+
+    mykeys = list( mydict.keys( ) )
+    myvalues = list( map( _totuple, list( mydict.values( ) ) ) )
+
+    maxlength = max( list( map( len, [ args ] + myvalues ) ) )
+
+    for i in range( maxlength ):
+        thisdict = { }
+        for key, value in zip( mykeys, myvalues ):
+            try:
+                thisdict[ key ] = value[i]
+            except IndexError:
+                thisdict[ key ] = value[-1]
+        try:
+            thisarg = args[i]
+        except IndexError:
+            thisarg = args[-1]
+
+        yield thisarg, thisdict
+
+def _totuple( x ):
+    """Utility stuff to convert string, int, long, float, None or anything to a usable tuple."""
+
+    if isinstance( x, basestring ):
+        out = x,
+    elif isinstance( x, ( int, long, float ) ):
+        out = str( x ),
+    elif x is None:
+        out = None,
+    else:
+        out = tuple( x )
+
+    return out
+
+def escape( text, newline=False ):
+    """Escape special html characters."""
+
+    if isinstance( text, basestring ):
+        if '&' in text:
+            text = text.replace( '&', '&amp;' )
+        if '>' in text:
+            text = text.replace( '>', '&gt;' )
+        if '<' in text:
+            text = text.replace( '<', '&lt;' )
+        if '\"' in text:
+            text = text.replace( '\"', '&quot;' )
+        if '\'' in text:
+            text = text.replace( '\'', '&quot;' )
+        if newline:
+            if '\n' in text:
+                text = text.replace( '\n', '<br>' )
+
+    return text
+
+_escape = escape
+
+def unescape( text ):
+    """Inverse of escape."""
+    
+    if isinstance( text, basestring ):
+        if '&amp;' in text:
+            text = text.replace( '&amp;', '&' )
+        if '&gt;' in text:
+            text = text.replace( '&gt;', '>' )
+        if '&lt;' in text:
+            text = text.replace( '&lt;', '<' )
+        if '&quot;' in text:
+            text = text.replace( '&quot;', '\"' )
+
+    return text
+
+class dummy:
+    """A dummy class for attaching attributes."""
+    pass
+
+doctype = dummy( )
+doctype.frameset = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">"""
+doctype.strict = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"""
+doctype.loose = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">"""
+
+class russell:
+    """A dummy class that contains anything."""
+
+    def __contains__( self, item ):
+        return True
+
+
+class MarkupError( Exception ):
+    """All our exceptions subclass this."""
+    def __str__( self ):
+        return self.message
+
+class ClosingError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag
+
+class OpeningError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' can not be opened." % tag
+
+class ArgumentError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' was called with more than one non-keyword argument." % tag
+
+class InvalidElementError( MarkupError ):
+    def __init__( self, tag, mode ):
+        self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode )
+
+class DeprecationError( MarkupError ):
+    def __init__( self, tag ):
+        self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag
+
+class ModeError( MarkupError ):
+    def __init__( self, mode ):
+        self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode
+
+class CustomizationError( MarkupError ):
+    def __init__( self ):
+        self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'."
+
+if __name__ == '__main__':
+    import sys
+    sys.stdout.write( __doc__ )
index 5a83e53..efc073e 100755 (executable)
@@ -38,6 +38,122 @@ class Updates:
         if patch and updates:
             self.add_update(patch, updates)
 
+    def _desc_to_html(self, page, desc):
+        in_ul = False
+        for line in desc.splitlines():
+            if line.startswith('- ') or line.startswith('* '):
+                if not in_ul:
+                    page.ul()
+                    in_ul = True
+                page.li(line[2:])
+            else:
+                if in_ul:
+                    page.ul.close()
+                    in_ul = False
+                page.p(line)
+        if in_ul:
+            page.ul.close()
+
+    def get_title(self, t, is_html = False):
+        if is_html:
+            import markup
+            page = markup.page()
+            page.init( title = t, style = CSS_STRIKE)
+            page.h1(t)
+            return str(page).rstrip('</html>').rstrip().rstrip('</body>')
+        else:
+            return t
+
+    def get_summary_info(self, sum, is_html = False):
+        if is_html:
+            import markup
+            page = markup.page()
+            page.h2(sum['Title'])
+            self._desc_to_html(page, sum['Description'])
+
+            return str(page)
+
+        else:
+            return '%s\n%s' %(sum['Title'], sum['Description'])
+
+    def get_patch_info(self, update, is_html = False):
+        if is_html:
+            import markup
+            page = markup.page()
+            #page.h3(update['Title'])
+            self._desc_to_html(page, update['Description'])
+
+            if update.has_key("Bugs"):
+                page.p.open()
+                page.add('Resolved Bugs: ')
+                firstone = True
+                for bug in update['Bugs']:
+                    if firstone:
+                        firstone = False
+                    else:
+                        page.add(', ')
+                    page.a(bug, href='http://bugs.tizen.org/show_bug.cgi?id=%s' %bug, class_="strike_link")
+                page.p.close()
+
+            if update.has_key("CVEs"):
+                page.p.open()
+                page.add('Resolved CVE Issues: ')
+                firstone = True
+                for cve in update['CVEs']:
+                    if firstone:
+                        firstone = False
+                    else:
+                        page.add(', ')
+                    page.a(cve, href='http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s\n' % cve, class_="strike_link")
+                page.p.close()
+
+            return str(page)
+        else:
+            INFO = """
+
+    Patch <%s>:
+        Title: %s
+        Type: %s
+        Project: %s
+        Repository: %s
+        Release: %s
+        Status: %s
+        Packages: %s
+        Description:
+            %s
+    """ % (update['ID'],
+           update['Title'],
+           update['Type'],
+           update['Project'],
+           update['Repository'],
+           update['Release'],
+           update['Status'],
+           ", ".join(update['Packages']),
+           '\n        '.join(update['Description'].splitlines()))
+
+            if update.has_key("CVEs"):
+                INFO += "    CVEs:\n"
+                cve_info = ''
+                for cve in update['CVEs']:
+                    cve_info += '       http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s\n' %cve
+                INFO += cve_info
+
+            if update.has_key("Bugs"):
+                INFO += "    Bugs:\n"
+                bug_info = ''
+                for bug in update['Bugs']:
+                    bug_info += '       http://bugs.tizen.org/show_bug.cgi?id=%s\n' %bug
+                INFO += bug_info
+
+            if update.has_key('Reboot') and  update['Reboot']:
+                INFO += '    NOTE: reboot needed\n'
+            if update.has_key('Relogin') and  update['Relogin']:
+                INFO += '    NOTE: relogin needed\n'
+            if update.has_key('Restart') and update['Restart']:
+                INFO += '    NOTE: restart needed\n'
+
+        return INFO
+
     def _new_doc(self):
         print "Creating new updates.xml file..."
         doc = minidom.Document()
@@ -64,8 +180,7 @@ class Updates:
         else:
             self._new_doc()
             self.next = 0
-
-    def _insert(self, parent, name, attrs={}, text=None):
+    def _insert(self, parent, name, attrs={}, text=None, data=None):
         """ Helper function to trivialize inserting an element into the doc """
         child = self.doc.createElement(name)
         for item in attrs.items():
@@ -73,6 +188,9 @@ class Updates:
         if text:
             txtnode = self.doc.createTextNode(unicode(text))
             child.appendChild(txtnode)
+        if data:
+            txtnode = self.doc.createCDATASection(unicode(data))
+            child.appendChild(txtnode)
         parent.appendChild(child)
         return child
 
@@ -99,7 +217,15 @@ class Updates:
         issued_time = times[0]
         self._insert(root, 'issued', attrs={ 'date' : issued_time })
 
-        self._insert(root, 'description', text=update['Description'])
+        if update.has_key('Reboot') and  update['Reboot']:
+            self._insert(root, 'reboot_required', text='True')
+        if update.has_key('Relogin') and  update['Relogin']:
+            self._insert(root, 'relogin_required', text='True')
+        if update.has_key('Restart') and update['Restart']:
+            self._insert(root, 'restart_required', text='True')
+
+        html = self.get_patch_info(update, True)
+        self._insert(root, 'description', data=html)
 
 class UpdateInfo:
     def __init__(self, patch = None, updates = None, cache = None):
@@ -144,7 +270,7 @@ class UpdateInfo:
             self._new_doc()
             self.next = 0
 
-    def _insert(self, parent, name, attrs={}, text=None):
+    def _insert(self, parent, name, attrs={}, text=None, data=None):
         """ Helper function to trivialize inserting an element into the doc """
         child = self.doc.createElement(name)
         for item in attrs.items():
@@ -152,6 +278,9 @@ class UpdateInfo:
         if text:
             txtnode = self.doc.createTextNode(unicode(text))
             child.appendChild(txtnode)
+        if data:
+            txtnode = self.doc.createCDATASection(unicode(data))
+            child.appendChild(txtnode)
         parent.appendChild(child)
         return child
 
@@ -190,13 +319,14 @@ class UpdateInfo:
                         'id'   : cve
                 })
 
-        for bug in update['Bugs']:
-            self._insert(refs, 'reference', attrs={
-                    'type' : 'bugzilla',
-                    'href' : 'http://bugs.tizen.org/show_bug.cgi?id=%s' %bug,
-                    'id'   : bug,
-                    'title': 'Bug number %s' %bug
-            })
+        if update.has_key("Bugs"):
+            for bug in update['Bugs']:
+                self._insert(refs, 'reference', attrs={
+                        'type' : 'bugzilla',
+                        'href' : 'http://bugs.tizen.org/show_bug.cgi?id=%s' %bug,
+                        'id'   : bug,
+                        'title': 'Bug number %s' %bug
+                })
         root.appendChild(refs)
 
         ## Errata description
@@ -209,7 +339,7 @@ class UpdateInfo:
         #self._insert(collection, 'name', text=update.release.long_name)
 
         for u in updates:
-            filename = u['binary']
+            filename = "rpms/%s" % (os.path.basename(u['binary']))
             if u['header'][rpm.RPMTAG_SOURCEPACKAGE] or 'debuginfo' in u['binary']:
                 continue
             pkg = self._insert(collection, 'package', attrs={