Refactor the devserver to make autoupdate protocol logic shareable.

This CL refactors the devservers AU logic to allow it to be shareable
with cros_image_to_target (which originally forked this code). This allows
the latter to be both compatible with both 2.0 and 3.0 protocols.

I also fixed the unittests that were broken by
I2b96047a95a66aa920dc5fd1f54807f0541af554

BUG=chromium-os:36418
TEST=Pylint + Unittests + image_to_live

Change-Id: I73cf6343c4fce7289c660bc1cda7fe63cd16c881
Reviewed-on: https://gerrit.chromium.org/gerrit/38159
Commit-Ready: Chris Sosa <[email protected]>
Reviewed-by: Chris Sosa <[email protected]>
Tested-by: Chris Sosa <[email protected]>
diff --git a/autoupdate_lib.py b/autoupdate_lib.py
new file mode 100644
index 0000000..c727c3c
--- /dev/null
+++ b/autoupdate_lib.py
@@ -0,0 +1,196 @@
+# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Module containing common autoupdate utilities and protocol dictionaries."""
+
+import datetime
+import os
+import time
+from xml.dom import minidom
+
+
+APP_ID = '87efface-864d-49a5-9bb3-4b050a7c227a'
+
+# Responses for the various Omaha protocols indexed by the protocol version.
+UPDATE_RESPONSE = {}
+UPDATE_RESPONSE['2.0'] = """<?xml version="1.0" encoding="UTF-8"?>
+  <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
+    <daystart elapsed_seconds="%(time_elapsed)s"/>
+    <app appid="{%(appid)s}" status="ok">
+      <ping status="ok"/>
+      <updatecheck
+        ChromeOSVersion="9999.0.0"
+        codebase="%(url)s"
+        hash="%(sha1)s"
+        sha256="%(sha256)s"
+        needsadmin="false"
+        size="%(size)s"
+        IsDelta="%(is_delta_format)s"
+        status="ok"
+        %(extra_attr)s/>
+    </app>
+  </gupdate>
+  """
+
+
+UPDATE_RESPONSE['3.0'] = """<?xml version="1.0" encoding="UTF-8"?>
+  <response protocol="3.0">
+    <daystart elapsed_seconds="%(time_elapsed)s"/>
+    <app appid="{%(appid)s}" status="ok">
+      <ping status="ok"/>
+      <updatecheck status="ok">
+        <urls>
+          <url codebase="%(codebase)s/"/>
+        </urls>
+        <manifest version="9999.0.0">
+          <packages>
+            <package hash="%(sha1)s" name="%(filename)s" size="%(size)s"
+                     required="true"/>
+          </packages>
+          <actions>
+            <action event="postinstall"
+              ChromeOSVersion="9999.0.0"
+              sha256="%(sha256)s"
+              needsadmin="false"
+              IsDelta="%(is_delta_format)s"
+              %(extra_attr)s />
+          </actions>
+        </manifest>
+      </updatecheck>
+    </app>
+  </response>
+  """
+
+
+# Responses for the various Omaha protocols indexed by the protocol version
+# when there's no update to be served.
+NO_UPDATE_RESPONSE = {}
+NO_UPDATE_RESPONSE['2.0'] = """<?xml version="1.0" encoding="UTF-8"?>
+  <gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
+    <daystart elapsed_seconds="%(time_elapsed)s"/>
+    <app appid="{%(appid)s}" status="ok">
+      <ping status="ok"/>
+      <updatecheck status="noupdate"/>
+    </app>
+  </gupdate>
+  """
+
+
+NO_UPDATE_RESPONSE['3.0'] = """<?xml version="1.0" encoding="UTF-8"?>
+  <response" protocol="3.0">
+    <daystart elapsed_seconds="%(time_elapsed)s"/>
+    <app appid="{%(appid)s}" status="ok">
+      <ping status="ok"/>
+      <updatecheck status="noupdate"/>
+    </app>
+  </response>
+  """
+
+
+class UnknownProtocolRequestedException(Exception):
+  """Raised when an supported protocol is specified."""
+
+
+def GetSecondsSinceMidnight():
+  """Returns the seconds since midnight as a decimal value."""
+  now = time.localtime()
+  return now[3] * 3600 + now[4] * 60 + now[5]
+
+
+def GetCommonResponseValues():
+  """Returns a dictionary of default values for the response."""
+  response_values = {}
+  response_values['appid'] = APP_ID
+  response_values['time_elapsed'] = GetSecondsSinceMidnight()
+  return response_values
+
+
+def GetSubstitutedResponse(response_dict, protocol, response_values):
+  """Substitutes the protocol-specific response with response_values.
+
+  Args:
+    response_dict: Canned response messages indexed by protocol.
+    protocol: client's protocol version from the request Xml.
+    response_values: Values to be substituted in the canned response.
+  Returns:
+    Xml string to be passed back to client.
+  """
+  response_xml = response_dict[protocol] % response_values
+  return response_xml
+
+
+def GetUpdateResponse(sha1, sha256, size, url, is_delta_format, protocol,
+                      critical_update=False):
+  """Returns a protocol-specific response to the client for a new update.
+
+  Args:
+    sha1: SHA1 hash of update blob
+    sha256: SHA256 hash of update blob
+    size: size of update blob
+    url: where to find update blob
+    is_delta_format: true if url refers to a delta payload
+    protocol: client's protocol version from the request Xml.
+    critical_update: whether this is a critical update.
+  Returns:
+    Xml string to be passed back to client.
+  """
+  response_values = GetCommonResponseValues()
+  response_values['sha1'] = sha1
+  response_values['sha256'] = sha256
+  response_values['size'] = size
+  response_values['url'] = url
+  (codebase, filename) = os.path.split(url)
+  response_values['codebase'] = codebase
+  response_values['filename'] = filename
+  response_values['is_delta_format'] = is_delta_format
+  extra_attributes = []
+  if critical_update:
+    # The date string looks like '20111115' (2011-11-15). As of writing,
+    # there's no particular format for the deadline value that the
+    # client expects -- it's just empty vs. non-empty.
+    date_str = datetime.date.today().strftime('%Y%m%d')
+    extra_attributes.append('deadline="%s"' % date_str)
+
+  response_values['extra_attr'] = ' '.join(extra_attributes)
+  return GetSubstitutedResponse(UPDATE_RESPONSE, protocol, response_values)
+
+
+def GetNoUpdateResponse(protocol):
+  """Returns a protocol-specific response to the client for no update.
+
+  Args:
+    protocol: client's protocol version from the request Xml.
+  Returns:
+    Xml string to be passed back to client.
+  """
+  response_values = GetCommonResponseValues()
+  return GetSubstitutedResponse(NO_UPDATE_RESPONSE, protocol, response_values)
+
+
+def ParseUpdateRequest(request_string):
+  """Returns a tuple containing information parsed from an update request.
+
+  Args:
+    request_dom: an xml string containing the update request.
+  Returns tuple consisting of protocol string, app element, event element and
+    update_check element.
+  Raises UnknownProtocolRequestedException if we do not understand the
+    protocol.
+  """
+  request_dom = minidom.parseString(request_string)
+  protocol = request_dom.firstChild.getAttribute('protocol')
+  supported_protocols = '2.0', '3.0'
+  if protocol not in supported_protocols:
+    raise UnknownProtocolRequestedException('Supported protocols are %s' %
+                                            supported_protocols)
+
+  element_dict = {}
+  for name in ['event', 'app', 'updatecheck']:
+    element_dict[name] = 'o:' + name if protocol == '2.0' else name
+
+  app = request_dom.firstChild.getElementsByTagName(element_dict['app'])[0]
+  event = request_dom.getElementsByTagName(element_dict['event'])
+  update_check = request_dom.getElementsByTagName(element_dict['updatecheck'])
+
+  return protocol, app, event, update_check