2019-08-08 21:28:25 -07:00
|
|
|
from __future__ import absolute_import
|
|
|
|
from __future__ import division
|
|
|
|
from __future__ import print_function
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
import argparse
|
2019-09-22 01:36:23 -07:00
|
|
|
import copy
|
2019-08-06 14:36:04 -07:00
|
|
|
import json
|
|
|
|
import jsonschema
|
|
|
|
import os
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
2019-12-14 22:43:06 -08:00
|
|
|
def make_argument_parser(name, params, wildcards):
|
|
|
|
"""Build argument parser dynamically to parse parameter arguments.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
name (str): Name of the command to parse.
|
|
|
|
params (dict): Parameter specification used to construct
|
|
|
|
the argparse parser.
|
|
|
|
wildcards (bool): Whether wildcards are allowed as arguments.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
The argparse parser.
|
|
|
|
A dictionary from argument name to list of valid choices.
|
|
|
|
"""
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog=name)
|
|
|
|
# For argparse arguments that have a 'choices' list associated
|
|
|
|
# with them, save it in the following dictionary.
|
|
|
|
choices = {}
|
|
|
|
for param in params:
|
|
|
|
# Construct arguments to pass into argparse's parser.add_argument.
|
|
|
|
argparse_kwargs = copy.deepcopy(param)
|
|
|
|
name = argparse_kwargs.pop("name")
|
|
|
|
if wildcards and "choices" in param:
|
|
|
|
choices[name] = param["choices"]
|
|
|
|
argparse_kwargs["choices"] = param["choices"] + ["*"]
|
|
|
|
if "type" in param:
|
|
|
|
types = {"int": int, "str": str, "float": float}
|
|
|
|
if param["type"] in types:
|
|
|
|
argparse_kwargs["type"] = types[param["type"]]
|
|
|
|
else:
|
|
|
|
raise ValueError(
|
|
|
|
"Parameter {} has type {} which is not supported. "
|
|
|
|
"Type must be one of {}".format(name, param["type"],
|
|
|
|
list(types.keys())))
|
|
|
|
parser.add_argument("--" + name, dest=name, **argparse_kwargs)
|
|
|
|
|
|
|
|
return parser, choices
|
|
|
|
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
class ProjectDefinition:
|
|
|
|
def __init__(self, current_dir):
|
2019-12-05 16:15:42 -08:00
|
|
|
"""Finds ray-project folder for current project, parse and validates it.
|
2019-09-07 18:30:17 -07:00
|
|
|
|
|
|
|
Args:
|
2019-12-05 16:15:42 -08:00
|
|
|
current_dir (str): Path from which to search for ray-project.
|
2019-09-07 18:30:17 -07:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
jsonschema.exceptions.ValidationError: This exception is raised
|
|
|
|
if the project file is not valid.
|
|
|
|
ValueError: This exception is raised if there are other errors in
|
|
|
|
the project definition (e.g. files not existing).
|
|
|
|
"""
|
|
|
|
root = find_root(current_dir)
|
|
|
|
if root is None:
|
|
|
|
raise ValueError("No project root found")
|
|
|
|
# Add an empty pathname to the end so that rsync will copy the project
|
|
|
|
# directory to the correct target.
|
|
|
|
self.root = os.path.join(root, "")
|
|
|
|
|
|
|
|
# Parse the project YAML.
|
2019-12-05 16:15:42 -08:00
|
|
|
project_file = os.path.join(self.root, "ray-project", "project.yaml")
|
2019-09-07 18:30:17 -07:00
|
|
|
if not os.path.exists(project_file):
|
|
|
|
raise ValueError("Project file {} not found".format(project_file))
|
|
|
|
with open(project_file) as f:
|
|
|
|
self.config = yaml.safe_load(f)
|
|
|
|
|
|
|
|
check_project_config(self.root, self.config)
|
|
|
|
|
|
|
|
def cluster_yaml(self):
|
|
|
|
"""Return the project's cluster configuration filename."""
|
2019-12-14 22:43:06 -08:00
|
|
|
return self.config["cluster"]["config"]
|
2019-09-07 18:30:17 -07:00
|
|
|
|
|
|
|
def working_directory(self):
|
|
|
|
"""Return the project's working directory on a cluster session."""
|
|
|
|
# Add an empty pathname to the end so that rsync will copy the project
|
|
|
|
# directory to the correct target.
|
|
|
|
directory = os.path.join("~", self.config["name"], "")
|
|
|
|
return directory
|
|
|
|
|
2019-09-22 01:36:23 -07:00
|
|
|
def get_command_info(self, command_name, args, shell, wildcards=False):
|
|
|
|
"""Get the shell command, parsed arguments and config for a command.
|
2019-09-07 18:30:17 -07:00
|
|
|
|
|
|
|
Args:
|
2019-09-22 01:36:23 -07:00
|
|
|
command_name (str): Name of the command to run. The command
|
|
|
|
definition should be available in project.yaml.
|
2019-09-07 18:30:17 -07:00
|
|
|
args (tuple): Tuple containing arguments to format the command
|
|
|
|
with.
|
2019-09-22 01:36:23 -07:00
|
|
|
wildcards (bool): If True, enable wildcards as arguments.
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
Returns:
|
2019-09-22 01:36:23 -07:00
|
|
|
The raw shell command to run with placeholders for the arguments.
|
|
|
|
The parsed argument dictonary, parsed with argparse.
|
|
|
|
The config dictionary of the command.
|
2019-09-07 18:30:17 -07:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
ValueError: This exception is raised if the given command is not
|
|
|
|
found in project.yaml.
|
|
|
|
"""
|
2019-09-22 01:36:23 -07:00
|
|
|
if shell or not command_name:
|
|
|
|
return command_name, {}, {}
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
command_to_run = None
|
|
|
|
params = None
|
2019-09-22 01:36:23 -07:00
|
|
|
config = None
|
2019-09-07 18:30:17 -07:00
|
|
|
|
|
|
|
for command_definition in self.config["commands"]:
|
2019-09-22 01:36:23 -07:00
|
|
|
if command_definition["name"] == command_name:
|
2019-09-07 18:30:17 -07:00
|
|
|
command_to_run = command_definition["command"]
|
|
|
|
params = command_definition.get("params", [])
|
2019-09-22 01:36:23 -07:00
|
|
|
config = command_definition.get("config", {})
|
2019-09-07 18:30:17 -07:00
|
|
|
if not command_to_run:
|
|
|
|
raise ValueError(
|
2019-09-22 01:36:23 -07:00
|
|
|
"Cannot find the command named '{}' in commmands section "
|
|
|
|
"of the project file.".format(command_name))
|
2019-09-07 18:30:17 -07:00
|
|
|
|
2019-12-14 22:43:06 -08:00
|
|
|
parser, choices = make_argument_parser(command_name, params, wildcards)
|
2019-11-20 23:52:41 -08:00
|
|
|
parsed_args = vars(parser.parse_args(list(args)))
|
2019-09-07 18:30:17 -07:00
|
|
|
|
2019-09-22 01:36:23 -07:00
|
|
|
if wildcards:
|
|
|
|
for key, val in parsed_args.items():
|
|
|
|
if val == "*":
|
|
|
|
parsed_args[key] = choices[key]
|
2019-09-07 18:30:17 -07:00
|
|
|
|
2019-09-22 01:36:23 -07:00
|
|
|
return command_to_run, parsed_args, config
|
2019-09-07 18:30:17 -07:00
|
|
|
|
2019-09-13 11:34:34 -07:00
|
|
|
def git_repo(self):
|
|
|
|
return self.config.get("repo", None)
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
|
2019-08-06 14:36:04 -07:00
|
|
|
def find_root(directory):
|
|
|
|
"""Find root directory of the ray project.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
directory (str): Directory to start the search in.
|
|
|
|
|
|
|
|
Returns:
|
2019-12-05 16:15:42 -08:00
|
|
|
Path of the parent directory containing the ray-project or
|
2019-08-06 14:36:04 -07:00
|
|
|
None if no such project is found.
|
|
|
|
"""
|
|
|
|
prev, directory = None, os.path.abspath(directory)
|
|
|
|
while prev != directory:
|
2019-12-05 16:15:42 -08:00
|
|
|
if os.path.isdir(os.path.join(directory, "ray-project")):
|
2019-08-06 14:36:04 -07:00
|
|
|
return directory
|
|
|
|
prev, directory = directory, os.path.abspath(
|
|
|
|
os.path.join(directory, os.pardir))
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
def validate_project_schema(project_config):
|
|
|
|
"""Validate a project config against the official ray project schema.
|
2019-08-06 14:36:04 -07:00
|
|
|
|
|
|
|
Args:
|
2019-09-07 18:30:17 -07:00
|
|
|
project_config (dict): Parsed project yaml.
|
2019-08-06 14:36:04 -07:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
jsonschema.exceptions.ValidationError: This exception is raised
|
|
|
|
if the project file is not valid.
|
|
|
|
"""
|
|
|
|
dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
with open(os.path.join(dir, "schema.json")) as f:
|
|
|
|
schema = json.load(f)
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
jsonschema.validate(instance=project_config, schema=schema)
|
2019-08-06 14:36:04 -07:00
|
|
|
|
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
def check_project_config(project_root, project_config):
|
2019-08-06 14:36:04 -07:00
|
|
|
"""Checks if the project definition is valid.
|
|
|
|
|
|
|
|
Args:
|
2019-12-05 16:15:42 -08:00
|
|
|
project_root (str): Path containing the ray-project
|
2019-09-07 18:30:17 -07:00
|
|
|
project_config (dict): Project config definition
|
2019-08-06 14:36:04 -07:00
|
|
|
|
|
|
|
Raises:
|
|
|
|
jsonschema.exceptions.ValidationError: This exception is raised
|
|
|
|
if the project file is not valid.
|
|
|
|
ValueError: This exception is raised if there are other errors in
|
|
|
|
the project definition (e.g. files not existing).
|
|
|
|
"""
|
2019-09-07 18:30:17 -07:00
|
|
|
validate_project_schema(project_config)
|
2019-08-06 14:36:04 -07:00
|
|
|
|
|
|
|
# Make sure the cluster yaml file exists
|
2019-12-14 22:43:06 -08:00
|
|
|
cluster_file = os.path.join(project_root,
|
|
|
|
project_config["cluster"]["config"])
|
|
|
|
if not os.path.exists(cluster_file):
|
|
|
|
raise ValueError("'cluster' file does not exist "
|
|
|
|
"in {}".format(project_root))
|
2019-08-06 14:36:04 -07:00
|
|
|
|
2019-09-07 18:30:17 -07:00
|
|
|
if "environment" in project_config:
|
|
|
|
env = project_config["environment"]
|
2019-08-06 14:36:04 -07:00
|
|
|
|
|
|
|
if sum(["dockerfile" in env, "dockerimage" in env]) > 1:
|
|
|
|
raise ValueError("Cannot specify both 'dockerfile' and "
|
|
|
|
"'dockerimage' in environment.")
|
|
|
|
|
|
|
|
if "requirements" in env:
|
|
|
|
requirements_file = os.path.join(project_root, env["requirements"])
|
|
|
|
if not os.path.exists(requirements_file):
|
|
|
|
raise ValueError("'requirements' file in 'environment' does "
|
|
|
|
"not exist in {}".format(project_root))
|
|
|
|
|
|
|
|
if "dockerfile" in env:
|
|
|
|
docker_file = os.path.join(project_root, env["dockerfile"])
|
|
|
|
if not os.path.exists(docker_file):
|
|
|
|
raise ValueError("'dockerfile' file in 'environment' does "
|
|
|
|
"not exist in {}".format(project_root))
|