blob: 26dfa64efc3f108479724c2c013c8c9e83f36723 [file] [log] [blame]
# Copyright (c) 2013 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.
"""Umpire utility classes."""
import logging
import os
import shutil
import urllib
from twisted.internet import defer
import factory_common # pylint: disable=W0611
from cros.factory.common import AttrDict, Singleton
from cros.factory.umpire import common as umpire_common
from cros.factory.utils import file_utils
from cros.factory.utils import process_utils
class Registry(AttrDict):
"""Registry is a singleton class that inherits from AttrDict.
Example:
config_file = Registry().get('active_config_file', None)
Registry().extend({
'abc': 123,
'def': 456
})
assertEqual(Registry().abc, 123)
"""
__metaclass__ = Singleton
def ConcentrateDeferreds(deferred_list):
"""Collects results from list of deferreds.
CollectDeferreds() returns a deferred object that fires error callback
on first error. And the original failure won't propagate back to original
deferred object's next error callback.
Args:
deferred_list: Iterable of deferred objects.
Returns:
Deferred object that fires error on any deferred_list's errback been
called. Its callback will be trigged when all callback results are
collected. The gathered result is a list of deferred object callback
results.
"""
return defer.gatherResults(deferred_list, consumeErrors=True)
def UnpackFactoryToolkit(env, toolkit_resource, device_toolkit=True,
run_as=None, mode=0750):
"""Unpacks factory toolkit in resources to toolkits/hash directory.
Note that if the destination directory already exists, it doesn't unpack.
Args:
env: UmpireEnv object.
toolkit_resource: Path to factory toolkit resources.
device_toolkit: True to unpack to env.device_toolkits_dir; otherwise,
env.server_toolkits_dir.
run_as: (uid, gid) unpacks using specified user/group. Default None means
no uid, gid changes.
mode: Directory permission (numerical, refer os.chmod) for newly unpacked
toolkit. Default 0750 ('rwxr-x---').
Returns:
Directory to unpack. None if toolkit_resource is invalid.
"""
def RunAs():
"""Demotes user.group to uid.gid specified in run_as param.
It is used to execute a command as another user.
"""
os.umask(022)
if not run_as:
return
uid, gid = run_as
os.setgid(gid)
os.setuid(uid)
if not isinstance(toolkit_resource, str) or not toolkit_resource:
logging.error('Invalid toolkit_resource %r', toolkit_resource)
return None
toolkit_path = env.GetResourcePath(toolkit_resource)
toolkit_hash = umpire_common.GetHashFromResourceName(toolkit_resource)
unpack_dir = os.path.join(
env.device_toolkits_dir if device_toolkit else env.server_toolkits_dir,
toolkit_hash)
if os.path.isdir(unpack_dir):
logging.info('UnpackFactoryToolkit destination dir already exists: %s',
unpack_dir)
return unpack_dir
if run_as:
uid, gid = run_as
else:
uid, gid = -1, -1
# Extract to temp directory first then move the directory to prevent
# keeping a broken toolkit.
with file_utils.TempDirectory() as temp_dir:
if run_as:
os.chown(temp_dir, uid, gid)
process_utils.Spawn([toolkit_path, '--noexec', '--target', temp_dir],
preexec_fn=RunAs, check_call=True, log=True)
# Create toolkit directory's base directory first.
unpack_dir_base = os.path.split(unpack_dir)[0]
if not os.path.isdir(unpack_dir_base):
file_utils.MakeDirsUidGid(unpack_dir_base, uid, gid, mode=mode)
else:
logging.debug('unpack_dir_base %r already exists', unpack_dir_base)
# Use shutil.move() instead of os.rename(). os.rename calls OS
# rename() function. And under Linux-like OSes, this system call
# creates and removes hardlink, that only works when source path and
# destination path are both on same filesystem.
shutil.move(temp_dir, unpack_dir)
os.chmod(unpack_dir, mode)
logging.debug('Factory toolkit extracted to %s', unpack_dir)
# TODO(deanliao): figure out if the permission is too loose.
# Give group/other rX permision on all files/directories under unpack_dir.
process_utils.Spawn(['chmod', '-R', 'og+rX', unpack_dir], check_call=True,
log=True)
# Inject MD5SUM in extracted toolkit as umpire read only.
md5sum_path = os.path.join(unpack_dir, 'usr', 'local', 'factory', 'MD5SUM')
with open(md5sum_path, 'w') as f:
f.write(toolkit_hash)
os.chmod(md5sum_path, 0440)
if run_as:
os.chown(md5sum_path, uid, gid)
logging.debug('%r generated', md5sum_path)
return unpack_dir
def Deprecate(method):
"""Logs error of calling deprecated function.
Args:
method: the deprecated function.
"""
def _Wrapper(*args, **kwargs):
logging.error('%s is deprecated', method.__name__)
return method(*args, **kwargs)
_Wrapper.__name__ = method.__name__
return _Wrapper
def ComposeDownloadConfig(download_files):
"""Composes download config.
Based on given download_files, composes config file for netboot install.
Args:
download_files: list of resource files (full path) to include in the
download config.
Returns:
Download config (multi-line string).
"""
def GetChannel(base_name):
"""Converts file base name to download channel."""
if base_name == 'rootfs-release':
return 'RELEASE'
elif base_name == 'rootfs-test':
return 'FACTORY'
else:
return base_name.upper()
if not download_files:
return
# Content of download config.
result = []
for resource_path in download_files:
resource_name = os.path.basename(resource_path)
# Remove '.gz' suffix.
resource_base_name = umpire_common.ParseResourceName(resource_name)[0][:-3]
channel = GetChannel(resource_base_name)
url_name = urllib.quote(resource_name)
sha1sum = file_utils.B64Sha1(resource_path)
result.append(':'.join([channel, url_name, sha1sum]))
return '\n'.join(result) + '\n'