blob: a87a099099eca9ed1e8e7b767fe739477c4cca3a [file] [log] [blame]
Amin Hassani8d718d12019-06-02 21:28:39 -07001# -*- coding: utf-8 -*-
Darin Petkovc3fd90c2011-05-11 14:23:00 -07002# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
rtc@google.comded22402009-10-26 22:36:21 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Gilad Arnoldd8d595c2014-03-21 13:00:41 -07006"""Devserver module for handling update client requests."""
7
Don Garrettfb15e322016-06-21 19:12:08 -07008from __future__ import print_function
9
rtc@google.comded22402009-10-26 22:36:21 +000010import os
Chris Sosa7c931362010-10-11 19:49:01 -070011
Amin Hassani4f1e4622019-10-03 10:40:50 -070012from six.moves import urllib
13
14import cherrypy # pylint: disable=import-error
Gilad Arnoldabb352e2012-09-23 01:24:27 -070015
Amin Hassani8d718d12019-06-02 21:28:39 -070016# TODO(crbug.com/872441): We try to import nebraska from different places
17# because when we install the devserver, we copy the nebraska.py into the main
18# directory. Once this bug is resolved, we can always import from nebraska
19# directory.
20try:
21 from nebraska import nebraska
22except ImportError:
23 import nebraska
Chris Sosa05491b12010-11-08 17:14:16 -080024
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070025import setup_chromite # pylint: disable=unused-import
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070026from chromite.lib.xbuddy import cherrypy_log_util
27from chromite.lib.xbuddy import common_util
28from chromite.lib.xbuddy import devserver_constants as constants
29
Gilad Arnoldc65330c2012-09-20 15:17:48 -070030
31# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080032def _Log(message, *args):
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070033 return cherrypy_log_util.LogWithTag('UPDATE', message, *args)
rtc@google.comded22402009-10-26 22:36:21 +000034
Gilad Arnold0c9c8602012-10-02 23:58:58 -070035class AutoupdateError(Exception):
36 """Exception classes used by this module."""
37 pass
38
39
Don Garrett0ad09372010-12-06 16:20:30 -080040def _ChangeUrlPort(url, new_port):
41 """Return the URL passed in with a different port"""
Amin Hassani4f1e4622019-10-03 10:40:50 -070042 scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
Don Garrett0ad09372010-12-06 16:20:30 -080043 host_port = netloc.split(':')
44
45 if len(host_port) == 1:
46 host_port.append(new_port)
47 else:
48 host_port[1] = new_port
49
Don Garrettfb15e322016-06-21 19:12:08 -070050 print(host_port)
joychen121fc9b2013-08-02 14:30:30 -070051 netloc = '%s:%s' % tuple(host_port)
Don Garrett0ad09372010-12-06 16:20:30 -080052
Amin Hassani4f1e4622019-10-03 10:40:50 -070053 # pylint: disable=too-many-function-args
54 return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
Don Garrett0ad09372010-12-06 16:20:30 -080055
Chris Sosa6a3697f2013-01-29 16:44:43 -080056def _NonePathJoin(*args):
57 """os.path.join that filters None's from the argument list."""
Amin Hassani4f1e4622019-10-03 10:40:50 -070058 return os.path.join(*[x for x in args if x is not None])
Don Garrett0ad09372010-12-06 16:20:30 -080059
Chris Sosa6a3697f2013-01-29 16:44:43 -080060
Amin Hassani4b5d5ab2020-03-22 19:57:46 -070061class Autoupdate(object):
Amin Hassanie9ffb862019-09-25 17:10:40 -070062 """Class that contains functionality that handles Chrome OS update pings."""
rtc@google.comded22402009-10-26 22:36:21 +000063
Gilad Arnold0c9c8602012-10-02 23:58:58 -070064 _PAYLOAD_URL_PREFIX = '/static/'
Chris Sosa6a3697f2013-01-29 16:44:43 -080065
Amin Hassani4b5d5ab2020-03-22 19:57:46 -070066 def __init__(self, xbuddy, static_dir=None, payload_path=None,
Amin Hassani4539a6b2020-03-25 13:57:08 -070067 proxy_port=None, critical_update=False):
Amin Hassanie9ffb862019-09-25 17:10:40 -070068 """Initializes the class.
69
70 Args:
71 xbuddy: The xbuddy path.
Amin Hassani4b5d5ab2020-03-22 19:57:46 -070072 static_dir: The path to the devserver static directory.
Amin Hassanie9ffb862019-09-25 17:10:40 -070073 payload_path: The path to pre-generated payload to serve.
74 proxy_port: The port of local proxy to tell client to connect to you
75 through.
76 critical_update: Whether provisioned payload is critical.
Amin Hassanie9ffb862019-09-25 17:10:40 -070077 """
joychen121fc9b2013-08-02 14:30:30 -070078 self.xbuddy = xbuddy
Amin Hassani4b5d5ab2020-03-22 19:57:46 -070079 self.static_dir = static_dir
Gilad Arnold0c9c8602012-10-02 23:58:58 -070080 self.payload_path = payload_path
Don Garrett0ad09372010-12-06 16:20:30 -080081 self.proxy_port = proxy_port
Satoru Takabayashid733cbe2011-11-15 09:36:32 -080082 self.critical_update = critical_update
Gilad Arnoldd0c71752013-12-06 11:48:45 -080083
Amin Hassanie9ffb862019-09-25 17:10:40 -070084 def GetUpdateForLabel(self, label):
joychen121fc9b2013-08-02 14:30:30 -070085 """Given a label, get an update from the directory.
Chris Sosa0356d3b2010-09-16 15:46:22 -070086
joychen121fc9b2013-08-02 14:30:30 -070087 Args:
joychen121fc9b2013-08-02 14:30:30 -070088 label: the relative directory inside the static dir
Gilad Arnoldd8d595c2014-03-21 13:00:41 -070089
Chris Sosa6a3697f2013-01-29 16:44:43 -080090 Returns:
joychen121fc9b2013-08-02 14:30:30 -070091 A relative path to the directory with the update payload.
92 This is the label if an update did not need to be generated, but can
93 be label/cache/hashed_dir_for_update.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -070094
Chris Sosa6a3697f2013-01-29 16:44:43 -080095 Raises:
joychen121fc9b2013-08-02 14:30:30 -070096 AutoupdateError: If client version is higher than available update found
97 at the directory given by the label.
Don Garrettf90edf02010-11-16 17:36:14 -080098 """
Amin Hassanie9ffb862019-09-25 17:10:40 -070099 _Log('Update label: %s', label)
100 static_update_path = _NonePathJoin(self.static_dir, label,
101 constants.UPDATE_FILE)
Don Garrettee25e552010-11-23 12:09:35 -0800102
joychen121fc9b2013-08-02 14:30:30 -0700103 if label and os.path.exists(static_update_path):
104 # An update payload was found for the given label, return it.
105 return label
Don Garrett0c880e22010-11-17 18:13:37 -0800106
joychen121fc9b2013-08-02 14:30:30 -0700107 # The label didn't resolve.
Amin Hassanie9ffb862019-09-25 17:10:40 -0700108 _Log('Did not found any update payload for label %s.', label)
joychen121fc9b2013-08-02 14:30:30 -0700109 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700110
David Rileyee75de22017-11-02 10:48:15 -0700111 def GetDevserverUrl(self):
112 """Returns the devserver url base."""
Chris Sosa6a3697f2013-01-29 16:44:43 -0800113 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
114 if x_forwarded_host:
115 hostname = 'http://' + x_forwarded_host
116 else:
117 hostname = cherrypy.request.base
118
David Rileyee75de22017-11-02 10:48:15 -0700119 return hostname
120
121 def GetStaticUrl(self):
122 """Returns the static url base that should prefix all payload responses."""
123 hostname = self.GetDevserverUrl()
124
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700125 static_urlbase = '%s/static' % hostname
Chris Sosa6a3697f2013-01-29 16:44:43 -0800126 # If we have a proxy port, adjust the URL we instruct the client to
127 # use to go through the proxy.
128 if self.proxy_port:
129 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
130
131 _Log('Using static url base %s', static_urlbase)
132 _Log('Handling update ping as %s', hostname)
133 return static_urlbase
134
Amin Hassanie9ffb862019-09-25 17:10:40 -0700135 def GetPathToPayload(self, label, board):
joychen121fc9b2013-08-02 14:30:30 -0700136 """Find a payload locally.
137
138 See devserver's update rpc for documentation.
139
140 Args:
141 label: from update request
joychen121fc9b2013-08-02 14:30:30 -0700142 board: from update request
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700143
144 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700145 The relative path to an update from the static_dir
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700146
joychen121fc9b2013-08-02 14:30:30 -0700147 Raises:
148 AutoupdateError: If the update could not be found.
149 """
150 path_to_payload = None
Amin Hassanie9ffb862019-09-25 17:10:40 -0700151 # TODO(crbug.com/1006305): deprecate --payload flag
joychen121fc9b2013-08-02 14:30:30 -0700152 if self.payload_path:
153 # Copy the image from the path to '/forced_payload'
154 label = 'forced_payload'
155 dest_path = os.path.join(self.static_dir, label, constants.UPDATE_FILE)
156 dest_stateful = os.path.join(self.static_dir, label,
157 constants.STATEFUL_FILE)
Amin Hassani8d718d12019-06-02 21:28:39 -0700158 dest_meta = os.path.join(self.static_dir, label,
159 constants.UPDATE_METADATA_FILE)
joychen121fc9b2013-08-02 14:30:30 -0700160
161 src_path = os.path.abspath(self.payload_path)
Amin Hassanie9ffb862019-09-25 17:10:40 -0700162 src_meta = os.path.abspath(self.payload_path + '.json')
joychen121fc9b2013-08-02 14:30:30 -0700163 src_stateful = os.path.join(os.path.dirname(src_path),
164 constants.STATEFUL_FILE)
165 common_util.MkDirP(os.path.join(self.static_dir, label))
Alex Deymo3e2d4952013-09-03 21:49:41 -0700166 common_util.SymlinkFile(src_path, dest_path)
Amin Hassanie9ffb862019-09-25 17:10:40 -0700167 common_util.SymlinkFile(src_meta, dest_meta)
joychen121fc9b2013-08-02 14:30:30 -0700168 if os.path.exists(src_stateful):
169 # The stateful payload is optional.
Alex Deymo3e2d4952013-09-03 21:49:41 -0700170 common_util.SymlinkFile(src_stateful, dest_stateful)
joychen121fc9b2013-08-02 14:30:30 -0700171 else:
172 _Log('WARN: %s not found. Expected for dev and test builds',
173 constants.STATEFUL_FILE)
174 if os.path.exists(dest_stateful):
175 os.remove(dest_stateful)
Amin Hassanie9ffb862019-09-25 17:10:40 -0700176 path_to_payload = self.GetUpdateForLabel(label)
joychen121fc9b2013-08-02 14:30:30 -0700177 else:
178 label = label or ''
179 label_list = label.split('/')
180 # Suppose that the path follows old protocol of indexing straight
181 # into static_dir with board/version label.
182 # Attempt to get the update in that directory, generating if necc.
Amin Hassanie9ffb862019-09-25 17:10:40 -0700183 path_to_payload = self.GetUpdateForLabel(label)
joychen121fc9b2013-08-02 14:30:30 -0700184 if path_to_payload is None:
Amin Hassanie9ffb862019-09-25 17:10:40 -0700185 # There was no update found in the directory. Let XBuddy find the
186 # payloads.
joychen121fc9b2013-08-02 14:30:30 -0700187 if label_list[0] == 'xbuddy':
188 # If path explicitly calls xbuddy, pop off the tag.
189 label_list.pop()
Amin Hassanie9ffb862019-09-25 17:10:40 -0700190 x_label, _ = self.xbuddy.Translate(label_list, board=board)
191 # Path has been resolved, try to get the payload.
192 path_to_payload = self.GetUpdateForLabel(x_label)
joychen121fc9b2013-08-02 14:30:30 -0700193 if path_to_payload is None:
Amin Hassanie9ffb862019-09-25 17:10:40 -0700194 # No update payload found after translation. Try to get an update to
195 # a test image from GS using the label.
joychen121fc9b2013-08-02 14:30:30 -0700196 path_to_payload, _image_name = self.xbuddy.Get(
197 ['remote', label, 'full_payload'])
198
199 # One of the above options should have gotten us a relative path.
200 if path_to_payload is None:
201 raise AutoupdateError('Failed to get an update for: %s' % label)
Amin Hassani8d718d12019-06-02 21:28:39 -0700202
203 return path_to_payload
joychen121fc9b2013-08-02 14:30:30 -0700204
Amin Hassani6eec8792020-01-09 14:06:48 -0800205 def HandleUpdatePing(self, data, label='', **kwargs):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800206 """Handles an update ping from an update client.
207
208 Args:
209 data: XML blob from client.
210 label: optional label for the update.
Amin Hassani6eec8792020-01-09 14:06:48 -0800211 kwargs: The map of query strings passed to the /update API.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700212
Chris Sosa6a3697f2013-01-29 16:44:43 -0800213 Returns:
214 Update payload message for client.
215 """
216 # Get the static url base that will form that base of our update url e.g.
217 # http://hostname:8080/static/update.gz.
David Rileyee75de22017-11-02 10:48:15 -0700218 static_urlbase = self.GetStaticUrl()
Amin Hassani86c6fb52020-02-28 11:03:52 -0800219 # Change the URL's string query dictionary provided by cherrypy to a valid
220 # dictionary that has proper values for its keys. e.g. True instead of
221 # 'True'.
222 kwargs = nebraska.QueryDictToDict(kwargs)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800223
Chris Sosab26b1202013-08-16 16:40:55 -0700224 # Process attributes of the update check.
Amin Hassani8d718d12019-06-02 21:28:39 -0700225 request = nebraska.Request(data)
Amin Hassani8d718d12019-06-02 21:28:39 -0700226 if request.request_type == nebraska.Request.RequestType.EVENT:
Gilad Arnolde7819e72014-03-21 12:50:48 -0700227 _Log('A non-update event notification received. Returning an ack.')
Amin Hassani083e3fe2020-02-13 11:39:18 -0800228 return nebraska.Nebraska().GetResponseToRequest(
229 request, response_props=nebraska.ResponseProperties(**kwargs))
Chris Sosa6a3697f2013-01-29 16:44:43 -0800230
Amin Hassani8d718d12019-06-02 21:28:39 -0700231 _Log('Update Check Received.')
Chris Sosa6a3697f2013-01-29 16:44:43 -0800232
233 try:
Amin Hassanie7ead902019-10-11 16:42:43 -0700234 path_to_payload = self.GetPathToPayload(label, request.board)
Amin Hassani8d718d12019-06-02 21:28:39 -0700235 base_url = _NonePathJoin(static_urlbase, path_to_payload)
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700236 local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800237 except AutoupdateError as e:
238 # Raised if we fail to generate an update payload.
Amin Hassani8d718d12019-06-02 21:28:39 -0700239 _Log('Failed to process an update request, but we will defer to '
240 'nebraska to respond with no-update. The error was %s', e)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800241
Amin Hassani6eec8792020-01-09 14:06:48 -0800242 if self.critical_update:
243 kwargs['critical_update'] = True
244
Amin Hassani8d718d12019-06-02 21:28:39 -0700245 _Log('Responding to client to use url %s to get image', base_url)
Amin Hassanic91fc0d2019-12-04 11:07:16 -0800246 nebraska_props = nebraska.NebraskaProperties(
247 update_payloads_address=base_url,
248 update_metadata_dir=local_payload_dir)
Amin Hassanic91fc0d2019-12-04 11:07:16 -0800249 nebraska_obj = nebraska.Nebraska(nebraska_props=nebraska_props)
Amin Hassani083e3fe2020-02-13 11:39:18 -0800250 return nebraska_obj.GetResponseToRequest(
251 request, response_props=nebraska.ResponseProperties(**kwargs))