blob: db97ae37ebabef9fdec892ece28996b8283902ff [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2020 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.
"""A Nebraska Wrapper to handle update client requests."""
from __future__ import print_function
import os
import re
import shutil
import tempfile
import requests
from six.moves import urllib
import cherrypy # pylint: disable=import-error
# Nebraska.py has been added to PYTHONPATH, so gs_archive_server should be able
# to import nebraska.py directly. But if gs_archive_server is triggered from
# ~/chromiumos/src/platform/dev, it will import nebraska, the python package,
# instead of nebraska.py, thus throwing an AttributeError when the module is
# eventually used. To mitigate this, catch the exception and import nebraska.py
# from the nebraska package directly.
try:
import nebraska
nebraska.QueryDictToDict({})
except AttributeError as e:
from nebraska import nebraska
from chromite.lib import cros_logging as logging
# Define module logger.
_logger = logging.getLogger(__file__)
# Define all GS Cache related constants.
GS_CACHE_PORT = '8888'
GS_ARCHIVE_BUCKET = 'chromeos-image-archive'
GS_CACHE_DWLD_RPC = 'download'
GS_CACHE_LIST_DIR_RPC = 'list_dir'
def _log(*args, **kwargs):
"""A wrapper function of logging.debug/info, etc."""
level = kwargs.pop('level', logging.DEBUG)
_logger.log(level, extra=cherrypy.request.headers, *args, **kwargs)
class NebraskaWrapperError(Exception):
"""Exception class used by this module."""
class NebraskaWrapper(object):
"""Class that contains functionality that handles Chrome OS update pings."""
# Define regexes for properties file. These patterns are the same as the ones
# defined in chromite/lib/xbuddy/build_artifact.py. Only the '.*' in the
# beginning and '$' at the end is different as in this class, we need to
# compare the full gs URL of the file without a newline at the end to this
# regex pattern.
_FULL_PAYLOAD_PROPS_PATTERN = r'.*chromeos_.*_full_dev.*bin(\.json)$'
_DELTA_PAYLOAD_PROPS_PATTERN = r'.*chromeos_.*_delta_dev.*bin(\.json)$'
def __init__(self, label, server_addr, full_update):
"""Initializes the class.
Args:
label: Label (string) for the update, typically in the format
<board>-<XXXX>/Rxx-xxxxx.x.x-<unique string>.
server_addr: IP address (string) for the server on which gs cache is
running.
full_update: Indicates whether the requested update is full or delta. The
string values for this argument can be 'True', 'False', or
'unspecified'.
"""
self._label = self._GetLabel(label)
self._gs_cache_base_url = 'http://%s:%s' % (server_addr, GS_CACHE_PORT)
# When full_update parameter is not specified in the request, the update
# type is 'delta'.
self._is_full_update = full_update.lower().strip() == 'true'
self._props_dir = tempfile.mkdtemp(prefix='gsc-update')
self._payload_props_file = None
def __enter__(self):
"""Called while entering context manager; does nothing."""
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Called while exiting context manager; cleans up temp dirs."""
try:
shutil.rmtree(self._props_dir)
except Exception as e:
_log('Something went wrong. Could not delete %s due to exception: %s',
self._props_dir, e, level=logging.WARNING)
@property
def _PayloadPropsFilename(self):
"""Get the name of the payload properties file.
The name of the properties file is obtained from the list of files returned
by the list_dir RPC by matching the name of the file with the update_type
and file extension.
Returns:
Name of the payload properties file.
Raises:
NebraskaWrapperError if the list_dir calls returns 4xx/5xx or if the
correct file could not be determined.
"""
if self._payload_props_file:
return self._payload_props_file
urlbase = self._GetListDirURL()
url = urllib.parse.urljoin(urlbase, self._label)
resp = requests.get(url)
try:
resp.raise_for_status()
except Exception as e:
raise NebraskaWrapperError('An error occurred while trying to complete '
'the request: %s' % e)
if self._is_full_update:
pattern = re.compile(self._FULL_PAYLOAD_PROPS_PATTERN)
else:
pattern = re.compile(self._DELTA_PAYLOAD_PROPS_PATTERN)
# Iterate through all listed files to determine the correct payload
# properties file. Since the listed files will be in the format
# gs://<gs_bucket>/<board>/<version>/<filename>, return the filename only
# once a match is determined.
for fname in [x.strip() for x in resp.content.strip().split('\n')]:
if pattern.match(fname):
self._payload_props_file = fname.rsplit('/', 1)[-1]
return self._payload_props_file
raise NebraskaWrapperError(
'Request to %s returned a %s but gs_archive_server was unable to '
'determine the name of the properties file.' %
(url, resp.status_code))
def _GetLabel(self, label):
"""Gets the label for the request.
Removes a trailing /au_nton from the label argument.
Args:
label: A string obtained from the request.
Returns:
A string in the format <board>-<XXXX>/Rxx-xxxxx.x.x-<unique string>.
"""
# TODO(crbug.com/1102552): Remove this logic once all clients stopped
# sending au_nton in the request.
return label[:-len('/au_nton')] if label.endswith('/au_nton') else label
def _GetDownloadURL(self):
"""Returns the static url base that should prefix all payload responses."""
_log('Handling update ping as %s', self._gs_cache_base_url)
return self._GetURL(GS_CACHE_DWLD_RPC)
def _GetListDirURL(self):
"""Returns the static url base that should prefix all list_dir requests."""
_log('Using base URL to list contents: %s', self._gs_cache_base_url)
return self._GetURL(GS_CACHE_LIST_DIR_RPC)
def _GetURL(self, rpc_name):
"""Construct gs_cache URL for the given RPC.
Args:
rpc_name: Name of the RPC for which the URL needs to be built.
Returns:
Base URL to be used.
"""
urlbase = urllib.parse.urljoin(self._gs_cache_base_url,
'%s/%s/' % (rpc_name, GS_ARCHIVE_BUCKET))
_log('Using static url base %s', urlbase)
return urlbase
def _GetPayloadPropertiesDir(self, urlbase):
"""Download payload properties file from GS Archive
Args:
urlbase: Base url that should be used to form the download request.
Returns:
The path to the /tmp directory which stores the payload properties file
that nebraska will use.
Raises:
NebraskaWrapperError is raised if the method is unable to
download the file for some reason.
"""
local_payload_dir = self._props_dir
partial_url = urllib.parse.urljoin(urlbase, '%s/' % self._label)
_log('Downloading %s from bucket %s.', self._PayloadPropsFilename,
partial_url, level=logging.INFO)
try:
resp = requests.get(urllib.parse.urljoin(partial_url,
self._PayloadPropsFilename))
resp.raise_for_status()
file_path = os.path.join(local_payload_dir, self._PayloadPropsFilename)
# We are not worried about multiple threads writing to the same file as
# we are creating a different directory for each initialization of this
# class anyway.
with open(file_path, 'wb') as f:
f.write(resp.content)
except Exception as e:
raise NebraskaWrapperError('An error occurred while trying to complete '
'the request: %s' % e)
_log('Path to downloaded payload properties file: %s' % file_path)
return local_payload_dir
def HandleUpdatePing(self, data, **kwargs):
"""Handles an update ping from an update client.
Args:
data: XML blob from client.
kwargs: The map of query strings passed to the /update API.
Returns:
Update payload message for client.
"""
# Get the static url base that will form that base of our update url e.g.
# http://<GS_CACHE_IP>:<GS_CACHE_PORT>/download/chromeos-image-archive/.
urlbase = self._GetDownloadURL()
# Change the URL's string query dictionary provided by cherrypy to a
# valid dictionary that has proper values for its keys. e.g. True
# instead of 'True'.
kwargs = nebraska.QueryDictToDict(kwargs)
try:
# Process attributes of the update check.
request = nebraska.Request(data)
if request.request_type == nebraska.Request.RequestType.EVENT:
_log('A non-update event notification received. Returning an ack.',
level=logging.INFO)
n = nebraska.Nebraska()
n.UpdateConfig(**kwargs)
return n.GetResponseToRequest(request)
_log('Update Check Received.')
base_url = urllib.parse.urljoin(urlbase, '%s/' % self._label)
_log('Responding to client to use url %s to get image', base_url,
level=logging.INFO)
local_payload_dir = self._GetPayloadPropertiesDir(urlbase=urlbase)
_log('Using %s as the update_metadata_dir for NebraskaProperties.',
local_payload_dir)
n = nebraska.Nebraska()
n.UpdateConfig(update_payloads_address=base_url,
update_app_index=nebraska.AppIndex(local_payload_dir))
return n.GetResponseToRequest(request)
except Exception as e:
raise NebraskaWrapperError('An error occurred while processing the '
'update request: %s' % e)