blob: 579fbc13ec35f92dc8ad100340415a9291dddc24 [file] [log] [blame]
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# Copyright 2018 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.
"""Mock Omaha server"""
from __future__ import print_function
# pylint: disable=cros-logging-import
import argparse
import base64
import copy
import datetime
import errno
import json
import logging
import os
import shutil
import signal
import sys
import threading
import traceback
from xml.dom import minidom
from xml.etree import ElementTree
from six.moves import BaseHTTPServer
from six.moves import http_client
from six.moves import urllib
# TODO(crbug/999047): Add these functionalities from to
# nebraska:
# - is_rollback: Passed to Nebraska (default False)
# - failures_per_url: Passed to Nebraska (default 1)
# - disable_backoff: Passed to Nebraska (default False)
# - num_urls: Passed to Nebraska (default 2)
# - eol_date: Passed to Nebraska (default None)
class Error(Exception):
"""The base class for failures raised by Nebraska."""
class InvalidRequestError(Error):
"""Raised for invalid requests."""
class Request(object):
"""Request consisting of a list of apps to update/install."""
APP_TAG = 'app'
APP_APPID_ATTR = 'appid'
APP_DELTA_OKAY_ATTR = 'delta_okay'
# The following app attributes should be the same for all incoming apps if
# they exist. 'version' should be repeated in all apps, but other attributes
# can be omited in non-platform apps. Or at least they should be present in
# one of the apps. For this reason we keep these values in the Request
# object itself and not the AppRequest (except for 'version').
APP_VERSION_ATTR = 'version'
APP_BOARD_ATTR = 'board'
UPDATE_CHECK_TAG = 'updatecheck'
PING_TAG = 'ping'
EVENT_TAG = 'event'
EVENT_TYPE_ATTR = 'eventtype'
EVENT_RESULT_ATTR = 'eventresult'
# Update events and result codes.
class RequestType(object):
"""Simple enumeration for encoding request type."""
INSTALL = 1 # Request installation of a new app.
UPDATE = 2 # Request update for an existing app.
EVENT = 3 # Just an event request.
def __init__(self, request_str):
"""Initializes a request instance.
request_str: XML-formatted request string.
self.request_str = request_str
logging.debug('Received request: %s', self.request_str)
self.version = None
self.track = None
self.board = None
self.request_type = None
self.timestamp ='%Y-%m-%d %H:%M:%S')
self.app_requests = []
def ParseRequest(self):
"""Parse an XML request string into a list of app requests.
An app request can be a no-op, an install request, or an update request, and
may include a ping and/or event tag. We treat app requests with the update
tag omitted as no-ops, since the server is not required to return payload
information. Install requests are signalled by sending app requests along
with a no-op request for the platform app.
A list of AppRequest instances.
InvalidRequestError if the request string is not a valid XML request.
request_root = ElementTree.fromstring(self.request_str)
except ElementTree.ParseError as err:
raise InvalidRequestError(
'Request string is not valid XML: %s' % err)
# TODO( It would be better to specifically check
# the platform app. An install is signalled by omitting the update_check for
# the platform app, so we assume that if we have one appid for which the
# update_check tag is omitted, it is the platform app and this is an install
# request. This assumption should be fine since we never mix updates with
# requests that do not include an update_check tag.
app_elements = request_root.findall(self.APP_TAG)
update_check_count = len(
[x for x in app_elements if x.find(self.UPDATE_CHECK_TAG) is not None])
if update_check_count == 0:
self.request_type = Request.RequestType.EVENT
elif update_check_count == len(app_elements) - 1:
self.request_type = Request.RequestType.INSTALL
elif update_check_count == len(app_elements):
self.request_type = Request.RequestType.UPDATE
raise InvalidRequestError(
'Client request omits update_check tag for more than one, but not all'
' app requests.')
for app in app_elements:
app_request = Request.AppRequest(app, self.request_type)
def _CheckAttributesAndReturnIt(attribute, in_all=False):
"""Checks the attribute integrity among all apps and return its value.
The assumption is that the value of the attribute is the same for all apps
if existed. It can optionally be in one or more apps, but they are all
attribute: An attribute of the app tag.
in_all: If true, the attribute should exist among all apps.
The value of the attribute (which is same among all app tags).
all_attrs = [getattr(x, attribute) for x in self.app_requests]
if in_all and None in all_attrs:
raise InvalidRequestError(
'All apps should have "%s" attribute.' % attribute)
# Filter out the None elements into a set.
unique_attrs = set(x for x in all_attrs if x is not None)
if not unique_attrs:
raise InvalidRequestError('"%s" attribute should appear in at '
'least one app.' % attribute)
if len(unique_attrs) > 1:
raise InvalidRequestError(
'Attribute "%s" is not the same in all app tags.' % attribute)
return unique_attrs.pop()
self.version = _CheckAttributesAndReturnIt(self.APP_VERSION_ATTR,
self.track = _CheckAttributesAndReturnIt(self.APP_CHANNEL_ATTR)
self.board = _CheckAttributesAndReturnIt(self.APP_BOARD_ATTR)
def GetDict(self):
"""Returns a dictionary with some parameters of the request.
This is mostly used by the auto update tests to capture the flow of requests
from update_engine to analyze them (Simply in JSON format).
if not self.app_requests:
return {}
# TODO(ahassani): Extend this to return an object for all App Requests. For
# now only can return the first one to be backward compatible with auto
# update auto tests.
result = self.app_requests[0].__dict__
# Auto tests require an additional timestamp value which can be considered
# as a Request wide varable and not App Request one. So set it here.
result['timestamp'] = self.timestamp
return result
class AppRequest(object):
"""An app request.
Can be an update request, install request, or neither if the update check
tag is omitted (i.e. the platform app when installing a DLC, or when a
request is only an event), in which case we treat the request as a no-op.
An app request can also send pings and event result information.
def __init__(self, app, request_type):
"""Initializes a Request.
app: The request app XML element.
request_type: install, update, or event.
More on event pings:
self.request_type = request_type
self.appid = None
self.version = None
self.track = None
self.board = None = None
self.delta_okay = None
self.event_type = None
self.event_result = None
self.previous_version = None
def __str__(self):
"""Returns a string representation of an AppRequest."""
if self.request_type == Request.RequestType.EVENT:
return str(self.appid)
elif self.request_type == Request.RequestType.INSTALL:
return 'install %s v%s' % (self.appid, self.version)
elif self.request_type == Request.RequestType.UPDATE:
return '%s update %s from v%s' % (
'delta' if self.delta_okay else 'full', self.appid, self.version)
def ParseApp(self, app):
"""Parses the app XML element and populates the self object.
app: The request app XML element.
Raises InvalidRequestError if the input request string is in
invalid format.
self.appid = app.get(Request.APP_APPID_ATTR)
self.version = app.get(Request.APP_VERSION_ATTR)
self.track = app.get(Request.APP_CHANNEL_ATTR)
self.board = app.get(Request.APP_BOARD_ATTR)
self.delta_okay = app.get(Request.APP_DELTA_OKAY_ATTR) == 'true'
event = app.find(Request.EVENT_TAG)
if event is not None:
self.event_type = int(event.get(Request.EVENT_TYPE_ATTR))
self.event_result = int(event.get(Request.EVENT_RESULT_ATTR, 0))
self.previous_version = event.get(Request.EVENT_PREVIOUS_VERSION_ATTR) = app.find(Request.PING_TAG) is not None
if None in (self.request_type, self.appid, self.version):
raise InvalidRequestError('Invalid app request.')
def MatchAppData(self, app_data, partial_match_appid=False):
"""Returns true iff the app matches a given client request.
An app matches a request if the appid matches the requested appid.
Additionally, if the app describes a delta update payload, the request
must be able to accept delta payloads.
app_data: An AppData object describing a valid app data.
partial_match_appid: If true, it will partially check the app_data's
appid. Which means that if app_data's appid is a substring of
request's appid, it will be a match.
True if the request matches the given app, False otherwise.
if self.appid != app_data.appid:
if not partial_match_appid or (app_data.appid is not None and
app_data.appid not in self.appid):
return False
if self.request_type == Request.RequestType.UPDATE:
if app_data.is_delta:
return self.delta_okay
return True
if self.request_type == Request.RequestType.INSTALL:
return not app_data.is_delta
return False
class Response(object):
"""An update/install response.
A response to an update or install request consists of an XML-encoded list
of responses for each appid in the client request. This class takes a list of
responses for update/install requests and compiles them into a single element
constituting an aggregate response that can be returned to the client in XML
format based on the format of an XML response template.
def __init__(self, request, properties):
"""Initialize a reponse from a list of matching apps.
request: Request instance describing client requests.
properties: An instance of NebraskaProperties.
self._request = request
self._properties = properties
curr =
# Jan 1 2007 is the start of Omaha v3 epoch:
self._elapsed_days = (curr - datetime.datetime(2007, 1, 1)).days
self._elapsed_seconds = int((
curr - datetime.datetime.combine(,
def GetXMLString(self):
"""Generates a response to a set of client requests.
Given a client request consisting of one or more app requests, generate a
response to each of these requests and combine them into a single
XML-formatted response.
XML-formatted response string consisting of a response to each app request
in the incoming request from the client.
response_xml = ElementTree.Element(
'response', attrib={'protocol': '3.0', 'server': 'nebraska'})
response_xml, 'daystart',
attrib={'elapsed_days': str(self._elapsed_days),
'elapsed_seconds': str(self._elapsed_seconds)})
for app_request in self._request.app_requests:
self.AppResponse(app_request, self._properties).Compile())
except Exception as err:
raise Error('Failed to compile response: %s' % err)
return ElementTree.tostring(
response_xml, encoding='UTF-8', method='xml')
class AppResponse(object):
"""Response to an app request.
If the request was an update or install request, the response should include
a matching app if one was found. Addionally, the response should include
responses to pings and events as appropriate.
def __init__(self, app_request, properties):
"""Initialize an AppResponse.
app_request: AppRequest representing a client request.
properties: An instance of NebraskaProperties.
self._app_request = app_request
self._app_data = None
self._err_not_found = False
self._payloads_address = None
# Although, for installs the update_engine probably should not care about
# critical updates and should install even if OOBE has not been passed.
self._critical_update = properties.critical_update
# If no update was requested, don't process anything anymore.
if properties.no_update:
if self._app_request.request_type == Request.RequestType.INSTALL:
self._app_data = properties.install_app_index.Find(self._app_request)
self._err_not_found = self._app_data is None
self._payloads_address = properties.install_payloads_address
elif self._app_request.request_type == Request.RequestType.UPDATE:
self._app_data = properties.update_app_index.Find(self._app_request)
self._payloads_address = properties.update_payloads_address
# This differentiates between apps that are not in the index and apps
# that are available, but do not have an update available. Omaha treats
# the former as an error, whereas the latter case should result in a
# response containing a "noupdate" tag.
self._err_not_found = (self._app_data is None and
not properties.update_app_index.Contains(
if self._app_data:
logging.debug('Found matching payload: %s', str(self._app_data))
elif self._err_not_found:
logging.debug('No matches for App ID %s', self._app_request.appid)
elif self._app_request.request_type == Request.RequestType.UPDATE:
logging.debug('No updates available for App ID %s',
def Compile(self):
"""Compiles an app description into XML format.
Compile the app description into an ElementTree Element that can be used
to compile a response to a client request, and ultimately converted into
An ElementTree Element instance describing an update or install payload.
app_response = ElementTree.Element(
'app', attrib={'appid': self._app_request.appid, 'status': 'ok'})
ElementTree.SubElement(app_response, 'ping', attrib={'status': 'ok'})
if self._app_request.event_type is not None:
ElementTree.SubElement(app_response, 'event', attrib={'status': 'ok'})
if self._app_data is not None:
update_check = ElementTree.SubElement(
app_response, 'updatecheck', attrib={'status': 'ok'})
urls = ElementTree.SubElement(update_check, 'urls')
urls, 'url', attrib={'codebase': self._payloads_address})
manifest = ElementTree.SubElement(
update_check, 'manifest',
attrib={'version': self._app_data.target_version})
actions = ElementTree.SubElement(manifest, 'actions')
actions, 'action',
attrib={'event': 'update', 'run':})
action = ElementTree.SubElement(
actions, 'action',
attrib={'ChromeOSVersion': self._app_data.target_version,
'ChromeVersion': '',
'IsDeltaPayload': str(self._app_data.is_delta).lower(),
'MaxDaysToScatter': '14',
'MetadataSignatureRsa': self._app_data.metadata_signature,
'MetadataSize': str(self._app_data.metadata_size),
'sha256': self._app_data.sha256,
'event': 'postinstall'})
if self._critical_update:
action.set('deadline', 'now')
if self._app_data.public_key is not None:
action.set('PublicKeyRsa', self._app_data.public_key)
packages = ElementTree.SubElement(manifest, 'packages')
packages, 'package',
attrib={'fp': '1.%s' % self._app_data.sha256_hex,
'hash_sha256': self._app_data.sha256_hex.decode('utf-8'),
'required': 'true',
'size': str(self._app_data.size)})
elif self._err_not_found:
app_response.set('status', 'error-unknownApplication')
elif self._app_request.request_type == Request.RequestType.UPDATE:
ElementTree.SubElement(app_response, 'updatecheck',
attrib={'status': 'noupdate'})
return app_response
class AppIndex(object):
"""An index of available app payload information.
Index of available apps used to generate responses to Omaha requests. The
index consists of lists of payload information associated with a given appid,
since we can have multiple payloads for a given app (delta/full payloads). The
index is built by scanning a given directory for json files that describe the
available payloads.
_directory: Directory containing metdata and payloads, can be None.
_index: A list of AppData describing payloads.
def __init__(self, directory):
"""Initializes an AppIndex instance."""
self._directory = directory
self._index = []
def Scan(self):
"""Scans the directory and loads all available properties files."""
if self._directory is None:
for f in os.listdir(self._directory):
if f.endswith('.json'):
with open(os.path.join(self._directory, f), 'r') as metafile:
metadata_str =
metadata = json.loads(metadata_str)
# Get the name from file name itself, assuming the metadata file
# ends with '.json'.
metadata[AppIndex.AppData.NAME_KEY] = f[:-len('.json')]
app = AppIndex.AppData(metadata)
except (IOError, KeyError, ValueError) as err:
logging.error('Failed to read app data from %s (%s)', f, str(err))
logging.debug('Found app data: %s', str(app))
def Find(self, request):
"""Search the index for a given appid.
Searches the index for the payloads matching a client request. Matching is
based on appid, and whether the client is searching for an update and can
handle delta payloads.
request: AppRequest describing the client request.
An AppData object describing an available payload matching the client
request, or None if no matches are found. Prefer delta payloads if the
client can accept them and if one is available.
# Find a list of payloads exactly matching the client request.
matches = [app_data for app_data in self._index if
if not matches:
# Look to see if there is any AppData with empty or partial App ID. Then
# return the first one you find. This basically will work as a wild card
# to allow AppDatas that don't have an AppID or their AppID is incomplete
# (e.g. empty platform App ID + _ + DLC App ID) to work just fine.
# The reason we just don't do this in one pass is that we want to find all
# the matches with exact appid and iif there was no match, we do the appid
# partial match.
matches = [app_data for app_data in self._index if
request.MatchAppData(app_data, partial_match_appid=True)]
if not matches:
return None
# If the client can handle a delta, prefer to send a delta.
if request.delta_okay:
match = next((x for x in matches if x.is_delta), None)
match = match if match else next(iter(matches), None)
match = next(iter(matches), None)
return copy.copy(match)
def Contains(self, request):
"""Checks if the AppIndex contains any apps matching a given request appid.
Checks the index for an appid (partially) matching the appid in the given
request. This is necessary because it allows us to differentiate between the
case where we have no new versions of an app and the case where we have no
information about an app at all.
request: Describes the client request.
True if the index contains any appids matching the appid given in the
return any(app_data.appid in request.appid for app_data in self._index)
class AppData(object):
"""Data about an available app.
Data about an available app that can be either installed or upgraded
to. This information is compiled into XML format and returned to the client
in an app tag in the server's response to an update or install request.
appid: appid of the requested app.
name: Filename of requested app on the mock Lorry server.
is_delta: True iff the payload is a delta update.
size: Size of the payload.
metadata_signature: Metadata signature.
metadata_size: Metadata size.
sha256_hex: SHA256 hash of the payload encoded in hexadecimal.
sha256: SHA256 hash of the payload encoded in base64 format.
target_version: ChromeOS version the payload is tied to.
source_version: Source version for delta updates.
public_key: The public key for signature verification. It should be in
base64 format.
APPID_KEY = 'appid'
NAME_KEY = 'name'
IS_DELTA_KEY = 'is_delta'
SIZE_KEY = 'size'
METADATA_SIG_KEY = 'metadata_signature'
METADATA_SIZE_KEY = 'metadata_size'
TARGET_VERSION_KEY = 'target_version'
SOURCE_VERSION_KEY = 'source_version'
SHA256_HEX_KEY = 'sha256_hex'
PUBLIC_KEY_RSA_KEY = 'public_key'
def __init__(self, app_data):
"""Initialize AppData.
app_data: Dictionary containing attributes used to initialize AppData
self.appid = app_data[self.APPID_KEY] = app_data[self.NAME_KEY]
self.target_version = app_data[self.TARGET_VERSION_KEY]
self.is_delta = app_data[self.IS_DELTA_KEY]
self.source_version = (
app_data[self.SOURCE_VERSION_KEY] if self.is_delta else None)
self.size = app_data[self.SIZE_KEY]
# Sometimes the payload is not signed, hence the matadata signature is
# null, but we should pass empty string instead of letting the value be
# null (the XML element tree will break).
self.metadata_signature = app_data[self.METADATA_SIG_KEY] or ''
self.metadata_size = app_data[self.METADATA_SIZE_KEY]
self.public_key = app_data.get(self.PUBLIC_KEY_RSA_KEY)
# Unfortunately the sha256_hex that paygen generates is actually a base64
# sha256 hash of the payload for some unknown historical reason. But the
# Omaha response contains the hex value of that hash. So here convert the
# value from base64 to hex so nebraska can send the correct version to the
# client. See b/131762584.
self.sha256 = app_data[self.SHA256_HEX_KEY]
self.sha256_hex = base64.b16encode(base64.b64decode(self.sha256))
self.url = None # Determined per-request.
def __str__(self):
if self.is_delta:
return '%s v%s: delta update from base v%s' % (
self.appid, self.target_version, self.source_version)
return '%s v%s: full update/install' % (
self.appid, self.target_version)
class NebraskaProperties(object):
"""An instance of this class contains some Nebraska properties."""
def __init__(self, update_payloads_address, install_payloads_address,
update_app_index, install_app_index):
"""Initializes the NebraskaProperties instance.
update_payloads_address: Address serving update payloads.
install_payloads_address: Address serving install payloads.
update_app_index: Index of update payloads.
install_app_index: Index of install payloads.
self.update_payloads_address = update_payloads_address
self.install_payloads_address = install_payloads_address
self.update_app_index = update_app_index
self.install_app_index = install_app_index
self.critical_update = False
self.no_update = False
class Nebraska(object):
"""An instance of this class allows responding to incoming Omaha requests.
This class has the responsibility to manufacture Omaha responses based on
the input update requests. This should be the main point of use of the
Nebraska. If any changes to the behavior of Nebraska is intended, like
creating critical update responses, or messing up with firmware and kernel
versions, new flags should be added here to add that feature.
def __init__(self,
"""Initializes the Nebraska instance.
update_payloads_address: Address of the update payload server.
install_payloads_address: Address of the install payload server. If None
is passed it will default to update_payloads_address.
update_metadata_dir: Update payloads metadata directory.
install_metadata_dir: Install payloads metadata directory.
# Attach '/' at the end of the addresses if they don't have any. The update
# engine just concatenates the base address with the payload file name and
# if there is no '/' the path will be invalid.
upa = os.path.join(update_payloads_address or '', '')
ipa = (os.path.join(install_payloads_address, '')
if install_payloads_address is not None else upa)
uai = AppIndex(update_metadata_dir)
iai = AppIndex(install_metadata_dir)
self._properties = NebraskaProperties(upa, ipa, uai, iai)
self._request_log = []
def GetResponseToRequest(self, request, critical_update=False,
"""Returns the response corresponding to a request.
request: The Request object representation of the incoming request.
critical_update: If true, the response will include 'deadline=now' which
indicates the update is critical.
no_update: If true, it will return a noupdate response regardless.
The string representation of the created response.
properties = copy.copy(self._properties)
properties.critical_update = critical_update
properties.no_update = no_update
response = Response(request, properties).GetXMLString()
# Make the XML response look pretty.
response_str = minidom.parseString(response).toprettyxml(indent=' ',
logging.debug('Sent response: %s', response_str)
return response_str
def GetRequestLog(self):
"""Returns the request logs in JSON format."""
return json.dumps(self._request_log)
class NebraskaServer(object):
"""A simple Omaha server instance.
A simple mock of an Omaha server. Responds to XML-formatted update/install
requests based on the contents of metadata files in update and install
directories, respectively. These metadata files are used to configure
responses to Omaha requests from Update Engine and describe update and install
payloads provided by another server.
def __init__(self, nebraska, runtime_root=None, port=0):
"""Initializes a server instance.
nebraska: The Nebraska instance to process requests and responses.
runtime_root: The root directory in which nebraska will write its PID and
port files.
port: Port the server should run on, 0 if the OS should assign a port.
self.nebraska = nebraska
self._runtime_root = runtime_root
self._port = port
if self._runtime_root:
self._port_file = os.path.join(self._runtime_root, 'port')
self._pid_file = os.path.join(self._runtime_root, 'pid')
self._httpd = None
self._server_thread = None
self._created_runtime_root = False
class NebraskaHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""HTTP request handler for Omaha requests."""
def _SendResponse(self, content_type, response, code=http_client.OK):
"""Sends a given response back to the client.
content_type: The content type of the response data: xml, json, etc.
response: The response content in string format.
code: The HTTP code to send back to the client.
self.send_header('Content-Type', content_type)
def _ParseURL(self, url):
"""Parses a URL into usable components.
url: The input URL to parse.
A tuple of parsed path and parsed query. The parsed query is a
dictionary of keys to list of values. e.g:
- ->
('path', {'key': ['value1', 'value2']})
parsed_result = urllib.parse.urlparse(url)
parsed_path = parsed_result.path.strip('/')
parsed_query = urllib.parse.parse_qs(parsed_result.query)
return parsed_path, parsed_query
def do_POST(self):
"""Responds to XML-formatted Omaha requests.
The URL path can be like:
The keys in query strings can be:
- critical_update: (boolean) For requesting a critical update response.
- no_update: (boolean) For requesting a response that indicates there is
no update (even if there is).
request_len = int(self.headers.get('content-length'))
request =
except Exception as err:
logging.error('Failed to read request in do_POST %s', str(err))
self.send_error(http_client.BAD_REQUEST, 'Invalid request (header).')
parsed_path, parsed_query = self._ParseURL(self.path)
if parsed_path == 'update':
critical_update = parsed_query.get('critical_update', []) == ['True']
no_update = parsed_query.get('no_update', []) == ['True']
request_obj = Request(request)
response = self.server.owner.nebraska.GetResponseToRequest(
request_obj, critical_update=critical_update, no_update=no_update)
self._SendResponse('application/xml', response)
except Exception as err:
logging.error('Failed to handle request (%s)', str(err))
logging.error('The requested path "%s" was not found!', parsed_path)
'The requested path "%s" was not found!' % parsed_path)
def do_GET(self):
"""Responds to Get requests.
The use cases are:
- requestlog: For getting the list of request logs in a JSON format.
The URL path can be like:
parsed_path, _ = self._ParseURL(self.path)
if parsed_path == 'requestlog':
response = self.server.owner.nebraska.GetRequestLog()
self._SendResponse('application/json', response)
except Exception as err:
logging.error('Failed to get request logs (%s)', str(err))
logging.error('The requested path "%s" was not found!', parsed_path)
'The requested path "%s" was not found!' % parsed_path)
def Start(self):
"""Starts the nebraska server."""
self._httpd = BaseHTTPServer.HTTPServer(('', self.GetPort()),
self._port = self._httpd.server_port
if self._runtime_root:
if not os.path.exists(self._runtime_root):
self._created_runtime_root = True
with open(self._port_file, 'w') as port_file:
with open(self._pid_file, 'w') as pid_file:
except IOError as err:
if err.errno == errno.EACCES:
print('Permission error: You need to run the script as root/sudo or '
'change the --runtime-root to point to a non-root accessible '
self._httpd.owner = self
self._server_thread = threading.Thread(target=self._httpd.serve_forever)
self._server_thread.start()'Started nebraska on port %d and pid %d.',
self._port, os.getpid())
def Stop(self):
"""Stops the mock Omaha server."""
if not self._runtime_root:
for f in {self._port_file, self._pid_file}:
except Exception as e:
logging.warning('Failed to remove file %s with error %s', f, e)
if self._created_runtime_root:
except Exception as e:
logging.warning('Failed to remove directory %s with error %s',
self._runtime_root, e)
def GetPort(self):
"""Returns the server's port."""
return self._port
def ParseArguments(argv):
"""Parses command line arguments.
argv: List of commandline arguments.
Namespace object containing parsed arguments.
parser = argparse.ArgumentParser(
parser.add_argument('--update-metadata', metavar='DIR', default=None,
help='Payloads metadata directory for update.')
parser.add_argument('--install-metadata', metavar='DIR', default=None,
help='Payloads metadata directory for install.')
parser.add_argument('--update-payloads-address', metavar='URL',
help='Base payload URI for update payloads',
parser.add_argument('--install-payloads-address', metavar='URL',
help='Base payload URI for install payloads. If not '
'passed it will default to --update-payloads-address')
parser.add_argument('--port', metavar='PORT', type=int, default=0,
help='Port to run the server on.')
parser.add_argument('--runtime-root', metavar='DIR',
help='The root directory in which nebraska will write its'
' pid and port files.')
parser.add_argument('--log-file', metavar='FILE', default='/tmp/nebraska.log',
help='The file to write the logs.'
' pass "stdout" to write to standard output.')
return parser.parse_args(argv[1:])
def main(argv):
"""Main function."""
opts = ParseArguments(argv)
# Reset the log file.
if opts.log_file != 'stdout':
with open(opts.log_file, 'w') as _:
print('Logging to %s' % opts.log_file)
logging.basicConfig(filename=(opts.log_file if opts.log_file != 'stdout'
else None),
level=logging.DEBUG)'Starting nebraska ...')
nebraska = Nebraska(
nebraska_server = NebraskaServer(nebraska, runtime_root=opts.runtime_root,
def handler(signum, _):'Exiting Nebraska with signal %d ...', signum)
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)
return os.EX_OK
if __name__ == '__main__':