blob: e7c36f122101ca15c2f8923f24184b2633d800dd [file] [log] [blame]
#!/usr/bin/env python
# Copyright (c) 2014 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 CLI (command line interface).
It parses command line arguments, packs them and makes JSON RPC call to
Umpire daemon (umpired). "init" command is an exception as umpired is not
running at that time.
"""
import errno
import logging
import os
import xmlrpclib
import factory_common # pylint: disable=W0611
from cros.factory.common import SetupLogging
from cros.factory.hacked_argparse import (CmdArg, Command, ParseCmdline,
verbosity_cmd_arg)
from cros.factory.umpire.commands import init
from cros.factory.umpire.commands import edit
from cros.factory.umpire import common
from cros.factory.umpire.config import ShowDiff
from cros.factory.umpire.umpire_env import UmpireEnv
from cros.factory.utils import file_utils
def UmpireCLI(env):
"""Gets connection to Umpire CLI XMLRPC server.
Args:
env: UmpireEnv object to get umpire_cli_port.
Returns:
A logical connection to an XML-RPC server
"""
uri = 'http://127.0.0.1:%d' % env.umpire_cli_port
logging.debug('UmpireCLI uri: %s', uri)
return xmlrpclib.Server(uri)
@Command('init',
CmdArg('--base-dir',
help=('the Umpire base directory. If not specified, use '
'%s/<board>' % common.DEFAULT_BASE_DIR)),
CmdArg('--board',
help=('board name the Umpire to serve. If not specified, use '
'board in bundle\'s MANIFEST.yaml')),
CmdArg('--default', action='store_true',
help='make umpire-<board> as default'),
CmdArg('--local', action='store_true',
help='do not set up /usr/local/bin and umpired'),
CmdArg('--user', default='umpire',
help='the user to run Umpire daemon'),
CmdArg('--group', default='umpire',
help='the group to run Umpire daemon'),
CmdArg('bundle_path', default='.',
help='Bundle path. If not specified, use local path.'))
def Init(args, env, root_dir='/'):
"""Initializes or updates an Umpire working environment.
It creates base directory, installs Umpire executables and sets up daemon
running environment. Base directory is specified by --base-dir or use
common.DEFAULT_BASE_DIR/<board>, where board is specified by --board or
derived from bundle's MANIFEST.
If an Umpire environment is already set, running it again will only update
Umpire executables.
Args:
env: UmpireEnv object.
root_dir: Root directory. Used for testing purpose.
"""
def GetBoard():
"""Gets board name.
It derives board name from bundle's MANIFEST.yaml.
"""
manifest_path = os.path.join(args.bundle_path, common.BUNDLE_MANIFEST)
manifest = common.LoadBundleManifest(manifest_path)
try:
return manifest['board']
except:
raise common.UmpireError(
'Unable to resolve board name from bundle manifest: ' +
manifest_path)
board = args.board if args.board else GetBoard()
# Sanity check: make sure factory toolkit exists.
factory_toolkit_path = os.path.join(args.bundle_path,
common.BUNDLE_FACTORY_TOOLKIT_PATH)
file_utils.CheckPath(factory_toolkit_path, description='factory toolkit')
base_dir = (args.base_dir if args.base_dir else
os.path.join(root_dir, common.DEFAULT_BASE_DIR, board))
env.base_dir = base_dir
init.Init(env, args.bundle_path, board, args.default, args.local, args.user,
args.group)
@Command('import-bundle',
CmdArg('--id',
help=('the target bundle id. If not specified, use '
'bundle_name in bundle\'s MANIFEST.yaml')),
CmdArg('bundle_path', default='.',
help='Bundle path. If not specified, use local path.'))
def ImportBundle(args, env):
"""Imports a factory bundle to Umpire.
It does the following: 1) sanity check for Umpire Config; 2) copy bundle
resources; 3) add a bundle item in bundles section in Umpire Config;
4) prepend a ruleset for the new bundle; 5) mark the updated config as
staging and prompt user to edit it.
"""
UmpireCLI(env).ImportBundle(os.path.realpath(args.bundle_path), args.id,
args.note)
@Command('update',
CmdArg('--from', dest='source_id',
help=('the bundle id to update. If not specified, update the '
'last one in rulesets')),
CmdArg('--to', dest='dest_id',
help=('bundle id for the new updated bundle. If omitted, the '
'bundle is updated in place')),
CmdArg('resources', nargs='+',
help=('resource(s) to update. Format: '
'<resource_type>=/path/to/resource where resource_type '
'is one of ' + ', '.join(common.UPDATEABLE_RESOURCES))))
def Update(args, env):
"""Updates a specific resource of a bundle.
It imports the specified resource(s) and updates the bundle's resource
section. It can update the bundle in place, or copy the target bundle to a
new one to update the resource.
"""
resources_to_update = []
for resource in args.resources:
resource_type, resource_path = resource.split('=', 1)
if resource_type not in common.UPDATEABLE_RESOURCES:
raise common.UmpireError('Unsupported resource type: ' + resource_type)
if not os.path.isfile(resource_path):
raise IOError(errno.ENOENT, 'Resource file not found', resource_path)
resources_to_update.append((resource_type,
os.path.realpath(resource_path)))
logging.debug('Invoke CLI Update(%r, source_id=%r, dest_id=%r)',
resources_to_update, args.source_id, args.dest_id)
UmpireCLI(env).Update(resources_to_update, args.source_id, args.dest_id)
@Command('edit')
def Edit(args, env):
"""Edits the Umpire Config file.
It calls user's default EDITOR to edit the config file and verifies the
modified result afterward.
"""
editor = edit.ConfigEditor(env, umpire_cli=UmpireCLI(env))
editor.Edit(config_file=args.config)
@Command('deploy')
def Deploy(unused_args, env):
"""Deploys an Umpire service.
It runs an Umpire service based on the staging Umpire Config (unless
specified by --config).
"""
# The config to deploy is already determined in _LoadConfig(). However,
# we need to ask Umpire damnon to validate resources.
config_path_to_deploy = os.path.realpath(env.config_path)
if os.path.dirname(config_path_to_deploy) != env.resources_dir:
raise common.UmpireError('Config to deploy %r must be in resources' %
env.config_path)
# First, ask Umpire daemon to validate config.
cli = UmpireCLI(env)
cli.ValidateConfig(config_path_to_deploy)
# Then, double confirm the user to deploy the config.
ok_to_deploy = True
if env.active_config_file:
print 'Changes for this deploy: '
print ''.join(ShowDiff(env.active_config_file, config_path_to_deploy))
if raw_input('Ok to deploy [y/n]? ') not in ['y', 'Y']:
ok_to_deploy = False
# Deploying, finally.
if ok_to_deploy:
config_res = os.path.basename(config_path_to_deploy)
cli.Deploy(config_res)
@Command('status')
def Status(unused_args, unused_env):
"""Shows the pstree of Umpire services."""
raise NotImplementedError
@Command('list')
def List(unused_args, unused_env):
"""Lists all Umpire Config files."""
raise NotImplementedError
@Command('start')
def Start(unused_args, unused_env):
"""Starts Umpire service."""
raise NotImplementedError
@Command('stop')
def Stop(unused_args, env):
"""Stops Umpire service."""
UmpireCLI(env).StopUmpired()
@Command('stage')
def Stage(unused_args, env):
"""Stages an Umpire Config file for edit."""
UmpireCLI(env).StageConfigFile(env.config_path)
@Command('unstage')
def Unstage(unused_args, env):
"""Unstages staging Umpire Config file."""
UmpireCLI(env).UnstageConfigFile()
@Command('import-resource',
CmdArg('resources', nargs='+',
help='Path to resource file(s).'))
def ImportResource(args, env):
"""Imports file(s) to resources folder."""
umpire_cli = UmpireCLI(env)
# Find out absolute path of resources and perform simple sanity check.
for path in args.resources:
resource_path = os.path.abspath(path)
if not os.path.isfile(resource_path):
raise IOError(errno.ENOENT, 'Resource file not found', resource_path)
umpire_cli.AddResource(resource_path)
def _LoadConfig(args, env):
"""Loads Umpire config file.
It loads Umpire config file and stores in UmpireEnv object.
Args:
args: command line arguments
env: UmpireEnv object
Raises:
UmpireError if config fails to load.
"""
if args.command_name in ['import-bundle', 'update', 'stage']:
# For import-bundle, update and stage command, it writes modified
# config file and makes it staging. So no staging config should exist
# when running the commands.
if env.HasStagingConfigFile():
raise common.UmpireError(
'A staging config file exists. Please unstage it before '
'import-bundle, update or stage.')
env.LoadConfig(custom_path=args.config)
elif args.command_name in ['start', 'stop']:
env.LoadConfig(custom_path=args.config)
elif args.command_name in ['edit', 'deploy']:
env.LoadConfig(staging=True, custom_path=args.config)
def main():
args = ParseCmdline(
'Umpire CLI (command line interface)',
CmdArg('--note', help='a note for this command'),
CmdArg('--config', help='path to Umpire Config file'),
verbosity_cmd_arg)
SetupLogging(level=args.verbosity)
# TODO(deanliao): Except init and deploy, env is used only to retrieve
# Umpire daemon port for XML-RPC for CLI. We shall just retrieve CLI port
# here and don't create UmpireEnv here to make sure that the only trustful
# UmpireEnv is the one in Umpire daemon.
env = UmpireEnv()
_LoadConfig(args, env)
args.command(args, env)
if __name__ == '__main__':
main()