blob: 03a644cfb7fb6f9efee66d8b5a651d4c0d2503ce [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
Gilad Arnolde7819e72014-03-21 12:50:48 -070010import collections
Dale Curtisc9aaf3a2011-08-09 15:47:40 -070011import json
rtc@google.comded22402009-10-26 22:36:21 +000012import os
Chris Sosa05491b12010-11-08 17:14:16 -080013import subprocess
Gilad Arnoldd0c71752013-12-06 11:48:45 -080014import threading
Darin Petkov2b2ff4b2010-07-27 15:02:09 -070015import time
Don Garrett0ad09372010-12-06 16:20:30 -080016import urlparse
Chris Sosa7c931362010-10-11 19:49:01 -070017
Gilad Arnoldabb352e2012-09-23 01:24:27 -070018import cherrypy
19
joychen921e1fb2013-06-28 11:12:20 -070020import build_util
Gilad Arnold55a2a372012-10-02 09:46:32 -070021import common_util
joychen7c2054a2013-07-25 11:14:07 -070022import devserver_constants as constants
Gilad Arnoldc65330c2012-09-20 15:17:48 -070023import log_util
Amin Hassani51780c62017-08-10 13:55:35 -070024
Amin Hassani8d718d12019-06-02 21:28:39 -070025# TODO(crbug.com/872441): We try to import nebraska from different places
26# because when we install the devserver, we copy the nebraska.py into the main
27# directory. Once this bug is resolved, we can always import from nebraska
28# directory.
29try:
30 from nebraska import nebraska
31except ImportError:
32 import nebraska
Chris Sosa05491b12010-11-08 17:14:16 -080033
Gilad Arnoldc65330c2012-09-20 15:17:48 -070034
joychen121fc9b2013-08-02 14:30:30 -070035# If used by client in place of an pre-update version string, forces an update
36# to the client regardless of the relative versions of the payload and client.
37FORCED_UPDATE = 'ForcedUpdate'
38
Gilad Arnoldc65330c2012-09-20 15:17:48 -070039# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080040def _Log(message, *args):
41 return log_util.LogWithTag('UPDATE', message, *args)
Gilad Arnoldc65330c2012-09-20 15:17:48 -070042
rtc@google.comded22402009-10-26 22:36:21 +000043
Gilad Arnold0c9c8602012-10-02 23:58:58 -070044class AutoupdateError(Exception):
45 """Exception classes used by this module."""
46 pass
47
48
Don Garrett0ad09372010-12-06 16:20:30 -080049def _ChangeUrlPort(url, new_port):
50 """Return the URL passed in with a different port"""
51 scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
52 host_port = netloc.split(':')
53
54 if len(host_port) == 1:
55 host_port.append(new_port)
56 else:
57 host_port[1] = new_port
58
Don Garrettfb15e322016-06-21 19:12:08 -070059 print(host_port)
joychen121fc9b2013-08-02 14:30:30 -070060 netloc = '%s:%s' % tuple(host_port)
Don Garrett0ad09372010-12-06 16:20:30 -080061
62 return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
63
Chris Sosa6a3697f2013-01-29 16:44:43 -080064def _NonePathJoin(*args):
65 """os.path.join that filters None's from the argument list."""
66 return os.path.join(*filter(None, args))
Don Garrett0ad09372010-12-06 16:20:30 -080067
Chris Sosa6a3697f2013-01-29 16:44:43 -080068
69class HostInfo(object):
Gilad Arnold286a0062012-01-12 13:47:02 -080070 """Records information about an individual host.
71
72 Members:
73 attrs: Static attributes (legacy)
74 log: Complete log of recorded client entries
75 """
76
77 def __init__(self):
78 # A dictionary of current attributes pertaining to the host.
79 self.attrs = {}
80
81 # A list of pairs consisting of a timestamp and a dictionary of recorded
82 # attributes.
83 self.log = []
84
85 def __repr__(self):
86 return 'attrs=%s, log=%s' % (self.attrs, self.log)
87
88 def AddLogEntry(self, entry):
89 """Append a new log entry."""
90 # Append a timestamp.
91 assert not 'timestamp' in entry, 'Oops, timestamp field already in use'
92 entry['timestamp'] = time.strftime('%Y-%m-%d %H:%M:%S')
93 # Add entry to hosts' message log.
94 self.log.append(entry)
95
Gilad Arnold286a0062012-01-12 13:47:02 -080096
Chris Sosa6a3697f2013-01-29 16:44:43 -080097class HostInfoTable(object):
Gilad Arnold286a0062012-01-12 13:47:02 -080098 """Records information about a set of hosts who engage in update activity.
99
100 Members:
101 table: Table of information on hosts.
102 """
103
104 def __init__(self):
105 # A dictionary of host information. Keys are normally IP addresses.
106 self.table = {}
107
108 def __repr__(self):
109 return '%s' % self.table
110
111 def GetInitHostInfo(self, host_id):
112 """Return a host's info object, or create a new one if none exists."""
113 return self.table.setdefault(host_id, HostInfo())
114
115 def GetHostInfo(self, host_id):
116 """Return an info object for given host, if such exists."""
Chris Sosa1885d032012-11-29 17:07:27 -0800117 return self.table.get(host_id)
Gilad Arnold286a0062012-01-12 13:47:02 -0800118
119
joychen921e1fb2013-06-28 11:12:20 -0700120class Autoupdate(build_util.BuildObject):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700121 """Class that contains functionality that handles Chrome OS update pings.
122
123 Members:
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700124 forced_image: path to an image to use for all updates.
125 payload_path: path to pre-generated payload to serve.
126 src_image: if specified, creates a delta payload from this image.
127 proxy_port: port of local proxy to tell client to connect to you
128 through.
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700129 board: board for the image. Needed for pre-generating of updates.
130 copy_to_static_root: copies images generated from the cache to ~/static.
David Zeuthen52ccd012013-10-31 12:58:26 -0700131 public_key: path to public key in PEM format.
Gilad Arnold8318eac2012-10-04 12:52:23 -0700132 critical_update: whether provisioned payload is critical.
Gilad Arnold8318eac2012-10-04 12:52:23 -0700133 max_updates: maximum number of updates we'll try to provision.
134 host_log: record full history of host update events.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700135 """
rtc@google.comded22402009-10-26 22:36:21 +0000136
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700137 _PAYLOAD_URL_PREFIX = '/static/'
Chris Sosa6a3697f2013-01-29 16:44:43 -0800138
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700139 def __init__(self, xbuddy, forced_image=None, payload_path=None,
Gabe Black70994862014-09-05 00:50:58 -0700140 proxy_port=None, src_image='', board=None,
Amin Hassaniabedfaa2019-06-02 21:30:48 -0700141 copy_to_static_root=True, public_key=None,
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700142 critical_update=False, max_updates=-1, host_log=False,
143 *args, **kwargs):
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700144 super(Autoupdate, self).__init__(*args, **kwargs)
joychen121fc9b2013-08-02 14:30:30 -0700145 self.xbuddy = xbuddy
Chris Sosa0356d3b2010-09-16 15:46:22 -0700146 self.forced_image = forced_image
Gilad Arnold0c9c8602012-10-02 23:58:58 -0700147 self.payload_path = payload_path
Chris Sosa62f720b2010-10-26 21:39:48 -0700148 self.src_image = src_image
Don Garrett0ad09372010-12-06 16:20:30 -0800149 self.proxy_port = proxy_port
joychen562699a2013-08-13 15:22:14 -0700150 self.board = board or self.GetDefaultBoardID()
Chris Sosa08d55a22011-01-19 16:08:02 -0800151 self.copy_to_static_root = copy_to_static_root
David Zeuthen52ccd012013-10-31 12:58:26 -0700152 self.public_key = public_key
Satoru Takabayashid733cbe2011-11-15 09:36:32 -0800153 self.critical_update = critical_update
Jay Srinivasanac69d262012-10-30 19:05:53 -0700154 self.max_updates = max_updates
Gilad Arnold8318eac2012-10-04 12:52:23 -0700155 self.host_log = host_log
Don Garrettfff4c322010-11-19 13:37:12 -0800156
Chris Sosa417e55d2011-01-25 16:40:48 -0800157 self.pregenerated_path = None
Sean O'Connor14b6a0a2010-03-20 23:23:48 -0700158
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700159 # Initialize empty host info cache. Used to keep track of various bits of
Gilad Arnold286a0062012-01-12 13:47:02 -0800160 # information about a given host. A host is identified by its IP address.
161 # The info stored for each host includes a complete log of events for this
162 # host, as well as a dictionary of current attributes derived from events.
163 self.host_infos = HostInfoTable()
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700164
Gilad Arnolde7819e72014-03-21 12:50:48 -0700165 self._update_count_lock = threading.Lock()
Gilad Arnoldd0c71752013-12-06 11:48:45 -0800166
Chris Sosa52148582012-11-15 15:35:58 -0800167 @staticmethod
168 def _GetVersionFromDir(image_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700169 """Returns the version of the image based on the name of the directory."""
170 latest_version = os.path.basename(image_dir)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700171 parts = latest_version.split('-')
joychen121fc9b2013-08-02 14:30:30 -0700172 # If we can't get a version number from the directory, default to a high
173 # number to allow the update to happen
Paul Hobbs5e7b5a72017-10-04 11:02:39 -0700174 # TODO(phobbs) refactor this.
Amin Hassani8d718d12019-06-02 21:28:39 -0700175 return parts[1] if len(parts) == 3 else '999999.0.0'
Chris Sosa0356d3b2010-09-16 15:46:22 -0700176
Chris Sosa52148582012-11-15 15:35:58 -0800177 @staticmethod
178 def _CanUpdate(client_version, latest_version):
Don Garrettfb15e322016-06-21 19:12:08 -0700179 """True if the latest_version is greater than the client_version."""
Chris Sosa6a3697f2013-01-29 16:44:43 -0800180 _Log('client version %s latest version %s', client_version, latest_version)
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700181
182 client_tokens = client_version.replace('_', '').split('.')
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700183 latest_tokens = latest_version.replace('_', '').split('.')
Daniel Erat8a0bc4a2011-09-30 08:52:52 -0700184
Paul Hobbs5e7b5a72017-10-04 11:02:39 -0700185 def _SafeInt(part):
186 try:
187 return int(part)
188 except ValueError:
189 return part
190
joychen121fc9b2013-08-02 14:30:30 -0700191 if len(latest_tokens) == len(client_tokens) == 3:
Paul Hobbs5e7b5a72017-10-04 11:02:39 -0700192 return map(_SafeInt, latest_tokens) > map(_SafeInt, client_tokens)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700193 else:
joychen121fc9b2013-08-02 14:30:30 -0700194 # If the directory name isn't a version number, let it pass.
195 return True
Chris Sosa0356d3b2010-09-16 15:46:22 -0700196
Don Garrettf90edf02010-11-16 17:36:14 -0800197 def GenerateUpdateFile(self, src_image, image_path, output_dir):
Chris Sosa0356d3b2010-09-16 15:46:22 -0700198 """Generates an update gz given a full path to an image.
199
200 Args:
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700201 src_image: Path to a source image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700202 image_path: Full path to image.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700203 output_dir: Path to the generated update file.
204
Chris Sosa6a3697f2013-01-29 16:44:43 -0800205 Raises:
Amin Hassani8d718d12019-06-02 21:28:39 -0700206 subprocess.CalledProcessError if the update generator fails to generate an
207 update payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700208 """
joychen7c2054a2013-07-25 11:14:07 -0700209 update_path = os.path.join(output_dir, constants.UPDATE_FILE)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800210 _Log('Generating update image %s', update_path)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700211
Chris Sosa0f1ec842011-02-14 16:33:22 -0800212 update_command = [
Chris Sosa5b8b5eb2012-03-27 11:15:27 -0700213 'cros_generate_update_payload',
Chris Sosa6a3697f2013-01-29 16:44:43 -0800214 '--image', image_path,
215 '--output', update_path,
Chris Sosa0f1ec842011-02-14 16:33:22 -0800216 ]
Chris Sosa4136e692010-10-28 23:42:37 -0700217
Chris Sosa52148582012-11-15 15:35:58 -0800218 if src_image:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800219 update_command.extend(['--src_image', src_image])
Chris Sosa52148582012-11-15 15:35:58 -0800220
Chris Sosa6a3697f2013-01-29 16:44:43 -0800221 _Log('Running %s', ' '.join(update_command))
222 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700223
Chris Sosa52148582012-11-15 15:35:58 -0800224 @staticmethod
225 def GenerateStatefulFile(image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800226 """Generates a stateful update payload given a full path to an image.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700227
228 Args:
229 image_path: Full path to image.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700230 output_dir: Directory for emitting the stateful update payload.
231
Chris Sosa908fd6f2010-11-10 17:31:18 -0800232 Raises:
Chris Sosa6a3697f2013-01-29 16:44:43 -0800233 subprocess.CalledProcessError if the update generator fails to generate a
Chris Sosa908fd6f2010-11-10 17:31:18 -0800234 stateful payload.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700235 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800236 update_command = [
237 'cros_generate_stateful_update_payload',
238 '--image', image_path,
239 '--output_dir', output_dir,
240 ]
241 _Log('Running %s', ' '.join(update_command))
242 subprocess.check_call(update_command)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700243
Don Garrettf90edf02010-11-16 17:36:14 -0800244 def FindCachedUpdateImageSubDir(self, src_image, dest_image):
245 """Find directory to store a cached update.
246
Gilad Arnold55a2a372012-10-02 09:46:32 -0700247 Given one, or two images for an update, this finds which cache directory
248 should hold the update files, even if they don't exist yet.
Don Garrettf90edf02010-11-16 17:36:14 -0800249
Gilad Arnold55a2a372012-10-02 09:46:32 -0700250 Returns:
251 A directory path for storing a cached update, of the following form:
252 Non-delta updates:
253 CACHE_DIR/<dest_hash>
254 Delta updates:
255 CACHE_DIR/<src_hash>_<dest_hash>
Chris Sosa744e1472011-09-07 19:32:50 -0700256 """
Gilad Arnold55a2a372012-10-02 09:46:32 -0700257 update_dir = ''
Chris Sosa744e1472011-09-07 19:32:50 -0700258 if src_image:
Gilad Arnold55a2a372012-10-02 09:46:32 -0700259 update_dir += common_util.GetFileMd5(src_image) + '_'
Don Garrettf90edf02010-11-16 17:36:14 -0800260
Gilad Arnold55a2a372012-10-02 09:46:32 -0700261 update_dir += common_util.GetFileMd5(dest_image)
Chris Sosa744e1472011-09-07 19:32:50 -0700262
joychen25d25972013-07-30 14:54:16 -0700263 return os.path.join(constants.CACHE_DIR, update_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800264
Don Garrettfff4c322010-11-19 13:37:12 -0800265 def GenerateUpdateImage(self, image_path, output_dir):
Don Garrettf90edf02010-11-16 17:36:14 -0800266 """Force generates an update payload based on the given image_path.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700267
Chris Sosade91f672010-11-16 10:05:44 -0800268 Args:
Don Garrettf90edf02010-11-16 17:36:14 -0800269 image_path: full path to the image.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800270 output_dir: the directory to write the update payloads to
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700271
Chris Sosa6a3697f2013-01-29 16:44:43 -0800272 Raises:
273 AutoupdateError if it failed to generate either update or stateful
274 payload.
Chris Sosade91f672010-11-16 10:05:44 -0800275 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800276 _Log('Generating update for image %s', image_path)
Andrew de los Reyes9a528712010-06-30 10:29:43 -0700277
Chris Sosa6a3697f2013-01-29 16:44:43 -0800278 # Delete any previous state in this directory.
279 os.system('rm -rf "%s"' % output_dir)
280 os.makedirs(output_dir)
rtc@google.comded22402009-10-26 22:36:21 +0000281
Chris Sosa6a3697f2013-01-29 16:44:43 -0800282 try:
283 self.GenerateUpdateFile(self.src_image, image_path, output_dir)
284 self.GenerateStatefulFile(image_path, output_dir)
285 except subprocess.CalledProcessError:
286 os.system('rm -rf "%s"' % output_dir)
287 raise AutoupdateError('Failed to generate update in %s' % output_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800288
Chris Sosa75490802013-09-30 17:21:45 -0700289 def GenerateUpdateImageWithCache(self, image_path):
Don Garrettf90edf02010-11-16 17:36:14 -0800290 """Force generates an update payload based on the given image_path.
rtc@google.comded22402009-10-26 22:36:21 +0000291
Chris Sosa0356d3b2010-09-16 15:46:22 -0700292 Args:
293 image_path: full path to the image.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700294
Chris Sosa0356d3b2010-09-16 15:46:22 -0700295 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700296 update directory relative to static_image_dir.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700297
Chris Sosa6a3697f2013-01-29 16:44:43 -0800298 Raises:
299 AutoupdateError if it we need to generate a payload and fail to do so.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700300 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800301 _Log('Generating update for src %s image %s', self.src_image, image_path)
Chris Sosae67b78f2010-11-04 17:33:16 -0700302
joychen121fc9b2013-08-02 14:30:30 -0700303 # If it was pregenerated, don't regenerate.
Chris Sosa417e55d2011-01-25 16:40:48 -0800304 if self.pregenerated_path:
305 return self.pregenerated_path
Don Garrettfff4c322010-11-19 13:37:12 -0800306
Chris Sosa75490802013-09-30 17:21:45 -0700307 # Which sub_dir should hold our cached update image.
308 cache_sub_dir = self.FindCachedUpdateImageSubDir(self.src_image, image_path)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800309 _Log('Caching in sub_dir "%s"', cache_sub_dir)
Chris Sosa417e55d2011-01-25 16:40:48 -0800310
joychen121fc9b2013-08-02 14:30:30 -0700311 # The cached payloads exist in a cache dir.
Chris Sosa75490802013-09-30 17:21:45 -0700312 cache_dir = os.path.join(self.static_dir, cache_sub_dir)
joychen121fc9b2013-08-02 14:30:30 -0700313
314 cache_update_payload = os.path.join(cache_dir,
joychen7c2054a2013-07-25 11:14:07 -0700315 constants.UPDATE_FILE)
joychen121fc9b2013-08-02 14:30:30 -0700316 cache_stateful_payload = os.path.join(cache_dir,
joychen25d25972013-07-30 14:54:16 -0700317 constants.STATEFUL_FILE)
Chris Sosa417e55d2011-01-25 16:40:48 -0800318 # Check to see if this cache directory is valid.
joychen121fc9b2013-08-02 14:30:30 -0700319 if not (os.path.exists(cache_update_payload) and
320 os.path.exists(cache_stateful_payload)):
321 self.GenerateUpdateImage(image_path, cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -0800322
joychen121fc9b2013-08-02 14:30:30 -0700323 # Don't regenerate the image for this devserver instance.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800324 self.pregenerated_path = cache_sub_dir
Chris Sosa65d339b2013-01-21 18:59:21 -0800325
joychen121fc9b2013-08-02 14:30:30 -0700326 return cache_sub_dir
Chris Sosa0356d3b2010-09-16 15:46:22 -0700327
Chris Sosa75490802013-09-30 17:21:45 -0700328 def _SymlinkUpdateFiles(self, target_dir, link_dir):
329 """Symlinks the update-related files from target_dir to link_dir.
joychen121fc9b2013-08-02 14:30:30 -0700330
331 Every time an update is called, clear existing files/symlinks in the
Chris Sosa75490802013-09-30 17:21:45 -0700332 link_dir, and replace them with symlinks to the target_dir.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700333
334 Args:
Chris Sosa75490802013-09-30 17:21:45 -0700335 target_dir: Location of the target files.
336 link_dir: Directory where the links should exist after.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700337 """
Chris Sosa75490802013-09-30 17:21:45 -0700338 _Log('Linking %s to %s', target_dir, link_dir)
339 if link_dir == target_dir:
340 _Log('Cannot symlink into the same directory.')
joychen121fc9b2013-08-02 14:30:30 -0700341 return
Amin Hassanicf2e4022019-04-30 08:48:38 -0700342 for _, _, files in os.walk(target_dir):
343 for target in files:
344 link = os.path.join(link_dir, target)
345 target = os.path.join(target_dir, target)
346 common_util.SymlinkFile(target, link)
Chris Sosa0356d3b2010-09-16 15:46:22 -0700347
joychen121fc9b2013-08-02 14:30:30 -0700348 def GetUpdateForLabel(self, client_version, label,
349 image_name=constants.TEST_IMAGE_FILE):
350 """Given a label, get an update from the directory.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700351
joychen121fc9b2013-08-02 14:30:30 -0700352 Args:
353 client_version: Current version of the client or FORCED_UPDATE
354 label: the relative directory inside the static dir
355 image_name: If the image type was specified by the update rpc, we try to
356 find an image with this file name first. This is by default
357 "chromiumos_test_image.bin" but can also take any of the values in
358 devserver_constants.ALL_IMAGES
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700359
Chris Sosa6a3697f2013-01-29 16:44:43 -0800360 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700361 A relative path to the directory with the update payload.
362 This is the label if an update did not need to be generated, but can
363 be label/cache/hashed_dir_for_update.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700364
Chris Sosa6a3697f2013-01-29 16:44:43 -0800365 Raises:
joychen121fc9b2013-08-02 14:30:30 -0700366 AutoupdateError: If client version is higher than available update found
367 at the directory given by the label.
Don Garrettf90edf02010-11-16 17:36:14 -0800368 """
joychen121fc9b2013-08-02 14:30:30 -0700369 _Log('Update label/file: %s/%s', label, image_name)
370 static_image_dir = _NonePathJoin(self.static_dir, label)
371 static_update_path = _NonePathJoin(static_image_dir, constants.UPDATE_FILE)
372 static_image_path = _NonePathJoin(static_image_dir, image_name)
joychen7c2054a2013-07-25 11:14:07 -0700373
joychen121fc9b2013-08-02 14:30:30 -0700374 # Update the client only if client version is older than available update.
375 latest_version = self._GetVersionFromDir(static_image_dir)
376 if not (client_version == FORCED_UPDATE or
377 self._CanUpdate(client_version, latest_version)):
378 raise AutoupdateError(
379 'Update check received but no update available for client')
Don Garrettee25e552010-11-23 12:09:35 -0800380
joychen121fc9b2013-08-02 14:30:30 -0700381 if label and os.path.exists(static_update_path):
382 # An update payload was found for the given label, return it.
383 return label
384 elif os.path.exists(static_image_path) and common_util.IsInsideChroot():
385 # Image was found for the given label. Generate update if we can.
Chris Sosa75490802013-09-30 17:21:45 -0700386 rel_path = self.GenerateUpdateImageWithCache(static_image_path)
387 # Add links from the static directory to the update.
388 cache_path = _NonePathJoin(self.static_dir, rel_path)
389 self._SymlinkUpdateFiles(cache_path, static_image_dir)
390 return label
Don Garrett0c880e22010-11-17 18:13:37 -0800391
joychen121fc9b2013-08-02 14:30:30 -0700392 # The label didn't resolve.
393 return None
Chris Sosa2c048f12010-10-27 16:05:27 -0700394
395 def PreGenerateUpdate(self):
Chris Sosa417e55d2011-01-25 16:40:48 -0800396 """Pre-generates an update and prints out the relative path it.
397
Chris Sosa6a3697f2013-01-29 16:44:43 -0800398 Returns relative path of the update.
Chris Sosa65d339b2013-01-21 18:59:21 -0800399
Chris Sosa6a3697f2013-01-29 16:44:43 -0800400 Raises:
401 AutoupdateError if it failed to generate the payload.
402 """
403 _Log('Pre-generating the update payload')
joychen121fc9b2013-08-02 14:30:30 -0700404 # Does not work with labels so just use static dir. (empty label)
405 pregenerated_update = self.GetPathToPayload('', FORCED_UPDATE, self.board)
Don Garrettfb15e322016-06-21 19:12:08 -0700406 print('PREGENERATED_UPDATE=%s' % _NonePathJoin(pregenerated_update,
407 constants.UPDATE_FILE))
Chris Sosa417e55d2011-01-25 16:40:48 -0800408 return pregenerated_update
Chris Sosa2c048f12010-10-27 16:05:27 -0700409
Amin Hassani8d718d12019-06-02 21:28:39 -0700410 def _ProcessUpdateComponents(self, request):
Gilad Arnolde7819e72014-03-21 12:50:48 -0700411 """Processes the components of an update request.
Chris Sosa6a3697f2013-01-29 16:44:43 -0800412
Gilad Arnolde7819e72014-03-21 12:50:48 -0700413 Args:
Amin Hassani8d718d12019-06-02 21:28:39 -0700414 request: A nebraska.Request object representing the update request.
Gilad Arnolde7819e72014-03-21 12:50:48 -0700415
416 Returns:
417 A named tuple containing attributes of the update requests as the
418 following fields: 'forced_update_label', 'client_version', 'board',
419 'event_result' and 'event_type'.
Chris Sosa0356d3b2010-09-16 15:46:22 -0700420 """
Chris Sosa6a3697f2013-01-29 16:44:43 -0800421 # Initialize an empty dictionary for event attributes to log.
422 log_message = {}
Jay Srinivasanac69d262012-10-30 19:05:53 -0700423
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700424 # Determine request IP, strip any IPv6 data for simplicity.
425 client_ip = cherrypy.request.remote.ip.split(':')[-1]
Gilad Arnold286a0062012-01-12 13:47:02 -0800426 # Obtain (or init) info object for this client.
427 curr_host_info = self.host_infos.GetInitHostInfo(client_ip)
428
joychen121fc9b2013-08-02 14:30:30 -0700429 client_version = FORCED_UPDATE
Chris Sosa6a3697f2013-01-29 16:44:43 -0800430 board = None
Amin Hassani8d718d12019-06-02 21:28:39 -0700431 event_result = None
432 event_type = None
433 if request.request_type != nebraska.Request.RequestType.EVENT:
434 client_version = request.version
435 channel = request.track
436 board = request.board or self.GetDefaultBoardID()
Chris Sosa6a3697f2013-01-29 16:44:43 -0800437 # Add attributes to log message
438 log_message['version'] = client_version
439 log_message['track'] = channel
440 log_message['board'] = board
441 curr_host_info.attrs['last_known_version'] = client_version
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700442
Amin Hassani8d718d12019-06-02 21:28:39 -0700443 else:
444 event_result = request.app_requests[0].event_result
445 event_type = request.app_requests[0].event_type
446 client_previous_version = request.app_requests[0].previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800447 # Store attributes to legacy host info structure
448 curr_host_info.attrs['last_event_status'] = event_result
449 curr_host_info.attrs['last_event_type'] = event_type
450 # Add attributes to log message
451 log_message['event_result'] = event_result
452 log_message['event_type'] = event_type
Gilad Arnoldb11a8942012-03-13 15:33:21 -0700453 if client_previous_version is not None:
454 log_message['previous_version'] = client_previous_version
Gilad Arnold286a0062012-01-12 13:47:02 -0800455
Gilad Arnold8318eac2012-10-04 12:52:23 -0700456 # Log host event, if so instructed.
457 if self.host_log:
458 curr_host_info.AddLogEntry(log_message)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700459
Gilad Arnolde7819e72014-03-21 12:50:48 -0700460 UpdateRequestAttrs = collections.namedtuple(
461 'UpdateRequestAttrs',
462 ('forced_update_label', 'client_version', 'board', 'event_result',
463 'event_type'))
464
465 return UpdateRequestAttrs(
466 curr_host_info.attrs.pop('forced_update_label', None),
467 client_version, board, event_result, event_type)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800468
David Rileyee75de22017-11-02 10:48:15 -0700469 def GetDevserverUrl(self):
470 """Returns the devserver url base."""
Chris Sosa6a3697f2013-01-29 16:44:43 -0800471 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
472 if x_forwarded_host:
473 hostname = 'http://' + x_forwarded_host
474 else:
475 hostname = cherrypy.request.base
476
David Rileyee75de22017-11-02 10:48:15 -0700477 return hostname
478
479 def GetStaticUrl(self):
480 """Returns the static url base that should prefix all payload responses."""
481 hostname = self.GetDevserverUrl()
482
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700483 static_urlbase = '%s/static' % hostname
Chris Sosa6a3697f2013-01-29 16:44:43 -0800484 # If we have a proxy port, adjust the URL we instruct the client to
485 # use to go through the proxy.
486 if self.proxy_port:
487 static_urlbase = _ChangeUrlPort(static_urlbase, self.proxy_port)
488
489 _Log('Using static url base %s', static_urlbase)
490 _Log('Handling update ping as %s', hostname)
491 return static_urlbase
492
joychen121fc9b2013-08-02 14:30:30 -0700493 def GetPathToPayload(self, label, client_version, board):
494 """Find a payload locally.
495
496 See devserver's update rpc for documentation.
497
498 Args:
499 label: from update request
500 client_version: from update request
501 board: from update request
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700502
503 Returns:
joychen121fc9b2013-08-02 14:30:30 -0700504 The relative path to an update from the static_dir
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700505
joychen121fc9b2013-08-02 14:30:30 -0700506 Raises:
507 AutoupdateError: If the update could not be found.
508 """
509 path_to_payload = None
510 #TODO(joychen): deprecate --payload flag
511 if self.payload_path:
512 # Copy the image from the path to '/forced_payload'
513 label = 'forced_payload'
514 dest_path = os.path.join(self.static_dir, label, constants.UPDATE_FILE)
515 dest_stateful = os.path.join(self.static_dir, label,
516 constants.STATEFUL_FILE)
Amin Hassani8d718d12019-06-02 21:28:39 -0700517 dest_meta = os.path.join(self.static_dir, label,
518 constants.UPDATE_METADATA_FILE)
joychen121fc9b2013-08-02 14:30:30 -0700519
520 src_path = os.path.abspath(self.payload_path)
521 src_stateful = os.path.join(os.path.dirname(src_path),
522 constants.STATEFUL_FILE)
523 common_util.MkDirP(os.path.join(self.static_dir, label))
Alex Deymo3e2d4952013-09-03 21:49:41 -0700524 common_util.SymlinkFile(src_path, dest_path)
Alex Deymo48e970d2015-09-23 14:34:41 -0700525 # The old metadata file should be regenerated whenever a new payload is
526 # used.
527 try:
528 os.unlink(dest_meta)
529 except OSError:
530 pass
joychen121fc9b2013-08-02 14:30:30 -0700531 if os.path.exists(src_stateful):
532 # The stateful payload is optional.
Alex Deymo3e2d4952013-09-03 21:49:41 -0700533 common_util.SymlinkFile(src_stateful, dest_stateful)
joychen121fc9b2013-08-02 14:30:30 -0700534 else:
535 _Log('WARN: %s not found. Expected for dev and test builds',
536 constants.STATEFUL_FILE)
537 if os.path.exists(dest_stateful):
538 os.remove(dest_stateful)
539 path_to_payload = self.GetUpdateForLabel(client_version, label)
540 #TODO(joychen): deprecate --image flag
541 elif self.forced_image:
joychendbfe6c92013-08-16 20:03:49 -0700542 if self.forced_image.startswith('xbuddy:'):
543 # This is trying to use an xbuddy path in place of a path to an image.
joychendbfe6c92013-08-16 20:03:49 -0700544 xbuddy_label = self.forced_image.split(':')[1]
545 self.forced_image = None
joychen365a5742013-08-21 10:41:18 -0700546 # Make sure the xbuddy path target is in the directory.
547 path_to_payload, _image_name = self.xbuddy.Get(xbuddy_label.split('/'))
548 # Pretend to have called update with this update path to payload.
Chris Sosa54ef81e2013-08-27 16:45:12 -0700549 self.GetPathToPayload(xbuddy_label, client_version, board)
550 else:
551 src_path = os.path.abspath(self.forced_image)
552 if os.path.exists(src_path) and common_util.IsInsideChroot():
553 # Image was found for the given label. Generate update if we can.
Chris Sosa75490802013-09-30 17:21:45 -0700554 path_to_payload = self.GenerateUpdateImageWithCache(src_path)
555 # Add links from the static directory to the update.
556 cache_path = _NonePathJoin(self.static_dir, path_to_payload)
557 self._SymlinkUpdateFiles(cache_path, self.static_dir)
joychen121fc9b2013-08-02 14:30:30 -0700558 else:
559 label = label or ''
560 label_list = label.split('/')
561 # Suppose that the path follows old protocol of indexing straight
562 # into static_dir with board/version label.
563 # Attempt to get the update in that directory, generating if necc.
564 path_to_payload = self.GetUpdateForLabel(client_version, label)
565 if path_to_payload is None:
566 # There was no update or image found in the directory.
567 # Let XBuddy find an image, and then generate an update to it.
568 if label_list[0] == 'xbuddy':
569 # If path explicitly calls xbuddy, pop off the tag.
570 label_list.pop()
Chris Sosa75490802013-09-30 17:21:45 -0700571 x_label, image_name = self.xbuddy.Translate(label_list, board=board)
joychen121fc9b2013-08-02 14:30:30 -0700572 if image_name not in constants.ALL_IMAGES:
573 raise AutoupdateError(
Amin Hassani8d718d12019-06-02 21:28:39 -0700574 'Use an image alias: dev, base, test, or recovery.')
joychen121fc9b2013-08-02 14:30:30 -0700575 # Path has been resolved, try to get the image.
576 path_to_payload = self.GetUpdateForLabel(client_version, x_label,
577 image_name)
578 if path_to_payload is None:
579 # Neither image nor update payload found after translation.
580 # Try to get an update to a test image from GS using the label.
581 path_to_payload, _image_name = self.xbuddy.Get(
582 ['remote', label, 'full_payload'])
583
584 # One of the above options should have gotten us a relative path.
585 if path_to_payload is None:
586 raise AutoupdateError('Failed to get an update for: %s' % label)
Amin Hassani8d718d12019-06-02 21:28:39 -0700587
588 return path_to_payload
joychen121fc9b2013-08-02 14:30:30 -0700589
590 def HandleUpdatePing(self, data, label=''):
Chris Sosa6a3697f2013-01-29 16:44:43 -0800591 """Handles an update ping from an update client.
592
593 Args:
594 data: XML blob from client.
595 label: optional label for the update.
Gilad Arnoldd8d595c2014-03-21 13:00:41 -0700596
Chris Sosa6a3697f2013-01-29 16:44:43 -0800597 Returns:
598 Update payload message for client.
599 """
600 # Get the static url base that will form that base of our update url e.g.
601 # http://hostname:8080/static/update.gz.
David Rileyee75de22017-11-02 10:48:15 -0700602 static_urlbase = self.GetStaticUrl()
Chris Sosa6a3697f2013-01-29 16:44:43 -0800603
Chris Sosab26b1202013-08-16 16:40:55 -0700604 # Process attributes of the update check.
Amin Hassani8d718d12019-06-02 21:28:39 -0700605 request = nebraska.Request(data)
606 request_attrs = self._ProcessUpdateComponents(request)
Chris Sosab26b1202013-08-16 16:40:55 -0700607
Amin Hassani8d718d12019-06-02 21:28:39 -0700608 if request.request_type == nebraska.Request.RequestType.EVENT:
Gilad Arnolde7819e72014-03-21 12:50:48 -0700609 if ((request_attrs.event_type ==
Amin Hassani8d718d12019-06-02 21:28:39 -0700610 nebraska.Request.EVENT_TYPE_UPDATE_DOWNLOAD_STARTED) and
611 request_attrs.event_result == nebraska.Request.EVENT_RESULT_SUCCESS):
Gilad Arnolde7819e72014-03-21 12:50:48 -0700612 with self._update_count_lock:
613 if self.max_updates == 0:
614 _Log('Received too many download_started notifications. This '
615 'probably means a bug in the test environment, such as too '
616 'many clients running concurrently. Alternatively, it could '
617 'be a bug in the update client.')
618 elif self.max_updates > 0:
619 self.max_updates -= 1
joychen121fc9b2013-08-02 14:30:30 -0700620
Gilad Arnolde7819e72014-03-21 12:50:48 -0700621 _Log('A non-update event notification received. Returning an ack.')
Amin Hassani8d718d12019-06-02 21:28:39 -0700622 nebraska_obj = nebraska.Nebraska()
623 return nebraska_obj.GetResponseToRequest(request)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800624
Gilad Arnolde7819e72014-03-21 12:50:48 -0700625 # Make sure that we did not already exceed the max number of allowed update
626 # responses. Note that the counter is only decremented when the client
627 # reports an actual download, to avoid race conditions between concurrent
628 # update requests from the same client due to a timeout.
joychen121fc9b2013-08-02 14:30:30 -0700629 if self.max_updates == 0:
Gilad Arnolde7819e72014-03-21 12:50:48 -0700630 _Log('Request received but max number of updates already served.')
Amin Hassani8d718d12019-06-02 21:28:39 -0700631 nebraska_obj = nebraska.Nebraska()
632 return nebraska_obj.GetResponseToRequest(request, no_update=True)
joychen121fc9b2013-08-02 14:30:30 -0700633
Amin Hassani8d718d12019-06-02 21:28:39 -0700634 if request_attrs.forced_update_label:
635 if label:
636 _Log('Label: %s set but being overwritten to %s by request', label,
637 request_attrs.forced_update_label)
638 label = request_attrs.forced_update_label
joychen121fc9b2013-08-02 14:30:30 -0700639
Amin Hassani8d718d12019-06-02 21:28:39 -0700640 _Log('Update Check Received.')
Chris Sosa6a3697f2013-01-29 16:44:43 -0800641
642 try:
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700643 path_to_payload = self.GetPathToPayload(
644 label, request_attrs.client_version, request_attrs.board)
Amin Hassani8d718d12019-06-02 21:28:39 -0700645 base_url = _NonePathJoin(static_urlbase, path_to_payload)
Amin Hassanic9dd11e2019-07-11 15:33:55 -0700646 local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800647 except AutoupdateError as e:
648 # Raised if we fail to generate an update payload.
Amin Hassani8d718d12019-06-02 21:28:39 -0700649 _Log('Failed to process an update request, but we will defer to '
650 'nebraska to respond with no-update. The error was %s', e)
Chris Sosa6a3697f2013-01-29 16:44:43 -0800651
Amin Hassani8d718d12019-06-02 21:28:39 -0700652 _Log('Responding to client to use url %s to get image', base_url)
653 nebraska_obj = nebraska.Nebraska(update_payloads_address=base_url,
654 update_metadata_dir=local_payload_dir)
655 return nebraska_obj.GetResponseToRequest(
656 request, critical_update=self.critical_update)
Gilad Arnoldd0c71752013-12-06 11:48:45 -0800657
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700658 def HandleHostInfoPing(self, ip):
659 """Returns host info dictionary for the given IP in JSON format."""
660 assert ip, 'No ip provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800661 if ip in self.host_infos.table:
662 return json.dumps(self.host_infos.GetHostInfo(ip).attrs)
663
664 def HandleHostLogPing(self, ip):
665 """Returns a complete log of events for host in JSON format."""
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700666 # If all events requested, return a dictionary of logs keyed by IP address.
Gilad Arnold286a0062012-01-12 13:47:02 -0800667 if ip == 'all':
668 return json.dumps(
669 dict([(key, self.host_infos.table[key].log)
670 for key in self.host_infos.table]))
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700671
672 # Otherwise we're looking for a specific IP address, so find its log.
Gilad Arnold286a0062012-01-12 13:47:02 -0800673 if ip in self.host_infos.table:
674 return json.dumps(self.host_infos.GetHostInfo(ip).log)
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700675
Gilad Arnold4ba437d2012-10-05 15:28:27 -0700676 # If no events were logged for this IP, return an empty log.
677 return json.dumps([])
678
Dale Curtisc9aaf3a2011-08-09 15:47:40 -0700679 def HandleSetUpdatePing(self, ip, label):
680 """Sets forced_update_label for a given host."""
681 assert ip, 'No ip provided.'
682 assert label, 'No label provided.'
Gilad Arnold286a0062012-01-12 13:47:02 -0800683 self.host_infos.GetInitHostInfo(ip).attrs['forced_update_label'] = label