blob: 88def64f3407ed865ddcb6e9079a53756ae22e87 [file] [log] [blame]
Amin Hassani2aa34282020-11-18 01:18:19 +00001# -*- coding: utf-8 -*-
2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Devserver module for handling update client requests."""
7
8from __future__ import print_function
9
10import os
11
12from six.moves import urllib
13
14import cherrypy # pylint: disable=import-error
15
16# 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
24
25import setup_chromite # pylint: disable=unused-import
26from chromite.lib.xbuddy import cherrypy_log_util
27from chromite.lib.xbuddy import devserver_constants as constants
28
29
30# Module-local log function.
31def _Log(message, *args):
32 return cherrypy_log_util.LogWithTag('UPDATE', message, *args)
33
34class AutoupdateError(Exception):
35 """Exception classes used by this module."""
36 pass
37
38
39def _ChangeUrlPort(url, new_port):
40 """Return the URL passed in with a different port"""
41 scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
42 host_port = netloc.split(':')
43
44 if len(host_port) == 1:
45 host_port.append(new_port)
46 else:
47 host_port[1] = new_port
48
49 print(host_port)
50 netloc = '%s:%s' % tuple(host_port)
51
52 # pylint: disable=too-many-function-args
53 return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
54
55def _NonePathJoin(*args):
56 """os.path.join that filters None's from the argument list."""
57 return os.path.join(*[x for x in args if x is not None])
58
59
60class Autoupdate(object):
61 """Class that contains functionality that handles Chrome OS update pings."""
62
63 def __init__(self, xbuddy, static_dir=None):
64 """Initializes the class.
65
66 Args:
67 xbuddy: The xbuddy path.
68 static_dir: The path to the devserver static directory.
69 """
70 self.xbuddy = xbuddy
71 self.static_dir = static_dir
72
73 def GetUpdateForLabel(self, label):
74 """Given a label, get an update from the directory.
75
76 Args:
77 label: the relative directory inside the static dir
78
79 Returns:
80 A relative path to the directory with the update payload.
81 This is the label if an update did not need to be generated, but can
82 be label/cache/hashed_dir_for_update.
83
84 Raises:
85 AutoupdateError: If client version is higher than available update found
86 at the directory given by the label.
87 """
88 _Log('Update label: %s', label)
89 static_update_path = _NonePathJoin(self.static_dir, label,
90 constants.UPDATE_FILE)
91
92 if label and os.path.exists(static_update_path):
93 # An update payload was found for the given label, return it.
94 return label
95
96 # The label didn't resolve.
97 _Log('Did not found any update payload for label %s.', label)
98 return None
99
100 def GetDevserverUrl(self):
101 """Returns the devserver url base."""
102 x_forwarded_host = cherrypy.request.headers.get('X-Forwarded-Host')
103 if x_forwarded_host:
104 # Select the left most <ip>:<port> value so that the request is
105 # forwarded correctly.
106 x_forwarded_host = [x.strip() for x in x_forwarded_host.split(',')][0]
107 hostname = 'http://' + x_forwarded_host
108 else:
109 hostname = cherrypy.request.base
110
111 return hostname
112
113 def GetStaticUrl(self):
114 """Returns the static url base that should prefix all payload responses."""
115 hostname = self.GetDevserverUrl()
116 _Log('Handling update ping as %s', hostname)
117
118 static_urlbase = '%s/static' % hostname
119 _Log('Using static url base %s', static_urlbase)
120 return static_urlbase
121
122 def GetPathToPayload(self, label, board):
123 """Find a payload locally.
124
125 See devserver's update rpc for documentation.
126
127 Args:
128 label: from update request
129 board: from update request
130
131 Returns:
132 The relative path to an update from the static_dir
133
134 Raises:
135 AutoupdateError: If the update could not be found.
136 """
137 label = label or ''
138 label_list = label.split('/')
139 # Suppose that the path follows old protocol of indexing straight
140 # into static_dir with board/version label.
141 # Attempt to get the update in that directory, generating if necc.
142 path_to_payload = self.GetUpdateForLabel(label)
143 if path_to_payload is None:
144 # There was no update found in the directory. Let XBuddy find the
145 # payloads.
146 if label_list[0] == 'xbuddy':
147 # If path explicitly calls xbuddy, pop off the tag.
148 label_list.pop()
149 x_label, _ = self.xbuddy.Translate(label_list, board=board)
150 # Path has been resolved, try to get the payload.
151 path_to_payload = self.GetUpdateForLabel(x_label)
152 if path_to_payload is None:
153 # No update payload found after translation. Try to get an update to
154 # a test image from GS using the label.
155 path_to_payload, _image_name = self.xbuddy.Get(
156 ['remote', label, 'full_payload'])
157
158 # One of the above options should have gotten us a relative path.
159 if path_to_payload is None:
160 raise AutoupdateError('Failed to get an update for: %s' % label)
161
162 return path_to_payload
163
164 def HandleUpdatePing(self, data, label='', **kwargs):
165 """Handles an update ping from an update client.
166
167 Args:
168 data: XML blob from client.
169 label: optional label for the update.
170 kwargs: The map of query strings passed to the /update API.
171
172 Returns:
173 Update payload message for client.
174 """
175 # Get the static url base that will form that base of our update url e.g.
176 # http://hostname:8080/static/update.gz.
177 static_urlbase = self.GetStaticUrl()
178 # Change the URL's string query dictionary provided by cherrypy to a valid
179 # dictionary that has proper values for its keys. e.g. True instead of
180 # 'True'.
181 kwargs = nebraska.QueryDictToDict(kwargs)
182
183 # Process attributes of the update check.
184 request = nebraska.Request(data)
185 if request.request_type == nebraska.Request.RequestType.EVENT:
186 _Log('A non-update event notification received. Returning an ack.')
187 return nebraska.Nebraska().GetResponseToRequest(
188 request, response_props=nebraska.ResponseProperties(**kwargs))
189
190 _Log('Update Check Received.')
191
192 try:
193 path_to_payload = self.GetPathToPayload(label, request.board)
194 base_url = _NonePathJoin(static_urlbase, path_to_payload)
195 local_payload_dir = _NonePathJoin(self.static_dir, path_to_payload)
196 except AutoupdateError as e:
197 # Raised if we fail to generate an update payload.
198 _Log('Failed to process an update request, but we will defer to '
199 'nebraska to respond with no-update. The error was %s', e)
200
201 _Log('Responding to client to use url %s to get image', base_url)
202 nebraska_props = nebraska.NebraskaProperties(
203 update_payloads_address=base_url,
204 update_metadata_dir=local_payload_dir)
205 nebraska_obj = nebraska.Nebraska(nebraska_props=nebraska_props)
206 return nebraska_obj.GetResponseToRequest(
207 request, response_props=nebraska.ResponseProperties(**kwargs))