blob: f2b8e92db067536296661c82bba7bc9ad1c0de1d [file] [log] [blame]
# Copyright (c) 2012 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.
"""Helper class for interacting with the Dev Server.
DevServer is the controlling class for all Dev Server interactions. All Dev
Server centric methods are collected here.
Using the locking methods provided in this class, multiple instances of various
tools and methods can be run concurrently with guaranteed safety.
"""
__author__ = 'dalecurtis@google.com (Dale Curtis)'
import logging
import os
import posixpath
# Autotest imports
import common
import common_util
class DevServer(object):
"""Helper class for interacting with the Dev Server.
All methods assume password-less login for the Dev Server has been set up or
methods are being run interactively.
"""
AU_BASE = 'au'
LATEST = 'LATEST'
ROOT_UPDATE = 'update.gz'
STATEFUL_UPDATE = 'stateful.tgz'
TEST_IMAGE = 'chromiumos_test_image.bin'
def __init__(self, dev_host, path, user, private_key=None, remote_host=None):
"""Initializes class variables and fixes private key permissions.
Args:
@param host: Address of the Dev Server.
@param path: Images directory root on Dev Server.
@param user: Dev Server SSH user name.
@param private_key: Optional private key file for password-less login.
If the key file has any group or world permissions
they will be removed.
@param remote_host: If a different hostname/ip should be needed for
uploading images to the dev server.
"""
self._dev_host = dev_host
if remote_host:
self._remote_host = remote_host
else:
self._remote_host = dev_host
self._user = user
self._images = path
self._private_key = private_key
if self._private_key:
# Check that there are no group or world permissions.
perms = os.stat(self._private_key).st_mode
if perms & 0o77 != 0:
# Remove group and world permissions, keep higher order bits.
os.chmod(self._private_key, (perms >> 6) << 6)
logging.warning(
'Removing group and world permissions from private key %s to make'
' SSH happy.', self._private_key)
def UploadAutotestPackages(self, remote_dir, staging_dir):
"""Uploads Autotest packages from staging directory to Dev Server.
Specifically, the autotest-pkgs directory is uploaded from the staging
directory to the specified Dev Server.
Args:
@param remote_dir: Directory to upload build components into.
@param staging_dir: Directory containing update.gz and stateful.tgz
Raises:
common_util.ChromeOSTestError: If any steps in the process fail to
complete.
"""
if os.path.isdir(os.path.join(staging_dir, 'autotest-pkgs')):
# Upload autotest-pkgs to Dev Server.
remote_pkgs_dir = posixpath.join(remote_dir, 'autotest')
msg = 'Failed to upload autotest packages to Dev Server!'
self.RemoteCopy(src='autotest-pkgs/*', dest=remote_pkgs_dir,
cwd=staging_dir, error_msg=msg)
def UploadBuildComponents(self, remote_dir, staging_dir, upload_image=False):
"""Uploads various build components from staging directory to Dev Server.
Specifically, the following components are uploaded:
- update.gz
- stateful.tgz
- chromiumos_test_image.bin
- The entire contents of the au directory. Symlinks are generated for each
au payload as well.
- Contents of autotest-pkgs directory.
- Control files from autotest/server/{tests, site_tests}
Args:
@param remote_dir: Directory to upload build components into.
@param staging_dir: Directory containing update.gz and stateful.tgz
@param upload_image: Should the chromiumos_test_image.bin be uploaded?
Raises:
common_util.ChromeOSTestError: If any steps in the process fail to
complete.
"""
upload_list = [self.ROOT_UPDATE, self.STATEFUL_UPDATE]
if upload_image:
upload_list.append(self.TEST_IMAGE)
else:
# Create blank chromiumos_test_image.bin. Otherwise the Dev Server will
# try to rebuild it unnecessarily.
cmd = 'touch ' + posixpath.join(remote_dir, self.TEST_IMAGE)
msg = 'Failed to create %s on Dev Server!' % self.TEST_IMAGE
self.RemoteCommand(cmd, error_msg=msg)
# Upload AU payloads.
au_path = os.path.join(staging_dir, self.AU_BASE)
if os.path.isdir(au_path):
upload_list.append(self.AU_BASE)
# For each AU payload, setup symlinks to the main payloads.
cwd = os.getcwd()
for au in os.listdir(au_path):
os.chdir(os.path.join(staging_dir, au_path, au))
os.symlink('../../%s' % self.TEST_IMAGE, self.TEST_IMAGE)
os.symlink('../../%s' % self.STATEFUL_UPDATE, self.STATEFUL_UPDATE)
os.chdir(cwd)
msg = 'Failed to upload build components to the Dev Server!'
self.RemoteCopy(
' '.join(upload_list), dest=remote_dir, cwd=staging_dir, error_msg=msg)
self.UploadAutotestPackages(remote_dir, staging_dir)
if os.path.isdir(os.path.join(staging_dir, 'autotest')):
remote_server_dir = posixpath.join(remote_dir, 'server')
cmd = 'mkdir -p ' + remote_server_dir
msg = 'Failed to create autotest server dir on Dev Server!'
self.RemoteCommand(cmd, error_msg=msg)
# Upload autotest/server/{tests,site_tests} onto Dev Server.
msg = 'Failed to upload autotest/server/{tests,site_tests} to Dev Server!'
self.RemoteCopy(src='autotest/server/{tests,site_tests}',
dest=remote_server_dir, cwd=staging_dir, error_msg=msg)
def AcquireLock(self, tag):
"""Acquires a Dev Server lock for a given tag.
Creates a directory for the specified tag on the Dev Server, telling other
components the resource/task represented by the tag is unavailable.
Args:
@param tag: Unique resource/task identifier. Use '/' for nested tags.
Returns:
Path to the created directory on Dev Server or None if creation failed.
Raises:
common_util.ChromeOSTestError: If Dev Server lock can't be acquired.
"""
remote_dir = posixpath.join(self._images, tag)
# Attempt to make the directory '<image dir>/<tag>' on the Dev Server. Doing
# so tells other components that this build is being/was processed by
# another instance. Directory creation is atomic and will return a non-zero
# exit code if the directory already exists.
cmd = 'mkdir ' + remote_dir
self.RemoteCommand(cmd)
return remote_dir
def ReleaseLock(self, tag):
"""Releases Dev Server lock for a given tag. Removes lock directory content.
Used to release an acquired Dev Server lock. If lock directory is not empty
the lock will fail to release.
Args:
@param tag: Unique resource/task identifier. Use '/' for nested tags.
Raises:
common_util.ChromeOSTestError: If processing lock can't be released.
"""
remote_dir = posixpath.join(self._images, tag)
cmd = 'rmdir ' + remote_dir
self.RemoteCommand(cmd)
def UpdateLatestBuild(self, board, build):
"""Create and upload LATEST file to the Dev Server for the given build.
If a LATEST file already exists, it's renamed to LATEST.n-1
Args:
@param board: Board name for this build; e.g., x86-generic-rel
@param build: Full build string to look for; e.g., 0.8.61.0-r1cf43296-b269
"""
try:
latest_path = posixpath.join(self._images, board, self.LATEST)
# Update the LATEST file and move any existing LATEST to LATEST.n-1. Use
# cp instead of mv to prevent any race conditions elsewhere.
cmd = '[ -f "%s" ] && cp "%s" "%s.n-1"; echo %s>"%s"' % (
latest_path, latest_path, latest_path, build, latest_path)
self.RemoteCommand(cmd=cmd)
except common_util.ChromeOSTestError:
# Log an error, but don't raise an exception. We don't want to blow away
# all the work we did just because we can't update the LATEST file.
logging.error('Could not update %s file for board %s, build %s.',
self.LATEST, board, build)
def GetUpdateUrl(self, board, build):
"""Returns Dev Server update URL for use with memento updater.
Args:
@param board: Board name for this build; e.g., x86-generic-rel
@param build: Full build string to look for; e.g., 0.8.61.0-r1cf43296-b269
Returns:
Properly formatted Dev Server update URL.
"""
return 'http://%s:8080/update/%s/%s' % (self._dev_host, board, build)
def FindMatchingBoard(self, board):
"""Returns a list of boards given a partial board name.
Args:
@param board: Partial board name for this build; e.g., x86-generic
Returns:
Returns a list of boards given a partial board and build.
"""
cmd = 'cd %s; ls -d %s*' % (self._images, board)
output = self.RemoteCommand(cmd, ignore_errors=True, output=True)
if output:
return output.splitlines()
else:
return []
def FindMatchingBuild(self, board, build):
"""Returns a list of matching builds given a board and partial build.
Args:
@param board: Partial board name for this build; e.g., x86-generic-rel
@param build: Partial build string to look for; e.g., 0.8.61.0
Returns:
Returns a list of (board, build) tuples given a partial board and build.
"""
cmd = 'cd %s; find \$(ls -d %s*) -maxdepth 1 -type d -name "%s*"' % (
self._images, board, build)
results = self.RemoteCommand(cmd, output=True)
if results:
return [tuple(line.split('/')) for line in results.splitlines()]
else:
return []
def RemoteCommand(self, cmd, **kwargs):
"""Wrapper function for executing commands on the Dev Server.
Args:
@param cmd: Command to execute on Dev Server.
@param kwargs: Dicionary of optional args.
Returns:
Results from common_util.RunCommand()
"""
return common_util.RemoteCommand(self._remote_host, self._user, cmd,
private_key=self._private_key, **kwargs)
def RemoteCopy(self, src, dest, **kwargs):
"""Wrapper function for copying a file to the Dev Server.
Copies from a local source to a remote destination (on the Dev Server). See
definition for common_util.RunCommand for complete argument definitions.
Args:
@param src: Local path/file.
@param dest: Remote destination on Dev Server.
@param kwargs: Dictionary of optional args.
Returns:
Results from common_util.RemoteCopy()
"""
return common_util.RemoteCopy(self._remote_host, self._user, src, dest,
private_key=self._private_key, **kwargs)
def PrepareDevServer(self, tag, force=False):
"""Prepare Dev Server file system to recieve build components.
Checks if the component directory for the given build is available and if
not creates it.
Args:
@param tag: Unique resource/task identifier. Use '/' for nested tags.
@param force: Force re-creation of remote_build_dir even if it already
exists.
Returns:
Tuple of (remote_build_dir, exists).
remote_build_dir: The path on Dev Server to the remote build.
exists: Indicates whether the directory was already present.
"""
# Check Dev Server for the build before we begin processing.
remote_build_dir = posixpath.join(self._images, tag)
# If force is request, delete the existing remote build directory.
if force:
self.RemoteCommand('rm -rf ' + remote_build_dir)
# Create remote directory. Will fail if it already exists.
exists = self.RemoteCommand('[ -d %s ] && echo exists || mkdir -p %s' % (
remote_build_dir, remote_build_dir), output=True) == 'exists'
return remote_build_dir, exists
def FindDevServerBuild(self, board, build):
"""Given partial build and board ids, figure out the appropriate build.
Args:
@param board: Partial board name for this build; e.g., x86-generic
@param build: Partial build string to look for; e.g., 0.8.61.0
Returns:
Tuple of (board, build):
board: Fully qualified board name; e.g., x86-generic-rel
build: Fully qualified build string; e.g., 0.8.61.0-r1cf43296-b269
Raises:
common_util.ChromeOSTestError: If no boards, no builds, or too many builds
are matched.
"""
# Find matching updates on Dev Server.
if build.lower().strip() == 'latest':
raise NotImplementedException('FindDevServerBuild no longer supports '
'the value "latest". You must pass a '
'build string to look for.')
builds = self.FindMatchingBuild(board, build)
if not builds:
raise common_util.ChromeOSTestError(
'No builds matching %s could be found for board %s.' % (
build, board))
if len(builds) > 1:
raise common_util.ChromeOSTestError(
'The given build id is ambiguous. Disambiguate by using one of'
' these instead: %s' % ', '.join([b[1] for b in builds]))
board, build = builds[0]
return board, build
def CloneDevServerBuild(self, board, build, tag, force=False):
"""Clone existing Dev Server build. Returns path to cloned build.
Args:
@param board: Fully qualified board name; e.g., x86-generic-rel
@param build: Fully qualified build string; e.g., 0.8.61.0-r1cf43296-b269
@param tag: Unique resource/task identifier. Use '/' for nested tags.
@param force: Force re-creation of remote_build_dir even if it already
exists.
Returns:
The path on Dev Server to the remote build.
"""
# Prepare the Dev Server for this build.
remote_build_dir, exists = self.PrepareDevServer(tag, force=force)
if not exists:
# Make a copy of the official build, only take necessary files.
self.RemoteCommand('cp %s %s %s %s' % (
os.path.join(self._images, board, build, self.TEST_IMAGE),
os.path.join(self._images, board, build, self.ROOT_UPDATE),
os.path.join(self._images, board, build, self.STATEFUL_UPDATE),
remote_build_dir))
return remote_build_dir
def GetControlFile(self, board, build, control):
"""Attempts to pull the requested control file from the Dev Server.
Args:
@param board: Fully qualified board name; e.g., x86-generic-rel
@param build: Fully qualified build string; e.g., 0.8.61.0-r1cf43296-b269
@param control: Path to control file on remote host relative to Autotest
root.
Returns:
Contents of the control file.
Raises:
common_util.ChromeOSTestError: If control file can't be retrieved
"""
# Create temporary file to target via scp; close.
return self.RemoteCommand(
'cat %s' % posixpath.join(self._images, board, build, control),
output=True)
def ListAutoupdateTargets(self, board, build):
"""Returns a list of autoupdate test targets for the given board, build.
Args:
@param board: Fully qualified board name; e.g., x86-generic-rel
@param build: Fully qualified build string; e.g., 0.8.61.0-r1cf43296-b269
Returns:
List of autoupdate test targets; e.g., ['0.14.747.0-r2bf8859c-b2927_nton']
Raises:
common_util.ChromeOSTestError: If control file can't be retrieved
"""
msg = 'Unable to retrieve list of autoupdate targets!'
return [os.path.basename(t) for t in self.RemoteCommand(
'ls -d %s/*' % posixpath.join(self._images, board, build, self.AU_BASE),
output=True, error_msg=msg).split()]
def GetImage(self, board, build, staging_dir):
"""Retrieve the TEST_IMAGE for the specified board and build.
Downloads the image using wget via the Dev Server HTTP interface. The image
is given a new random file name in the staging directory.
Args:
@param board: Fully qualified board name; e.g., x86-generic-rel
@param build: Fully qualified build string; e.g., 0.8.61.0-r1cf43296-b269
@param staging_dir: Directory to store downloaded image in.
Returns:
File name of the image in the staging directory.
"""
image_url = '%s/%s' % (
self.GetUpdateUrl(board, build).replace('update', 'static/archive'),
self.TEST_IMAGE)
image_file = self.TEST_IMAGE + '.n-1'
msg = 'Failed to retrieve the specified image from the Dev Server!'
common_util.RunCommand(
'wget -O %s --timeout=30 --tries=1 --no-proxy %s' % (
image_file, image_url), cwd=staging_dir, error_msg=msg)
return image_file