diff --git a/README.rst b/README.rst index 7386884..146efa2 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ DCOS Command Line Interface =========================== -The DCOS Command Line Interface (CLI) is a cross-platform command line utility +The DCOS Command Line Interface (CLI) is a cross-platform command line utility that provides a user-friendly yet powerful way to manage DCOS installations. Installation and Usage @@ -30,7 +30,7 @@ The example below installs every package available in the DCOS repository:: Using the CLI without DCOS -------------------------- -You may optionally configure the DCOS CLI to work with open source Mesos and +You may optionally configure the DCOS CLI to work with open source Mesos and Marathon_ by setting the following properties:: dcos config set core.mesos_master_url http://:5050 dcos config set marathon.url http://:8080 @@ -112,12 +112,9 @@ Running Tox will run unit and integration tests in both Python environments using a temporarily created virtualenv. -You should ensure :code:`DCOS_CONFIG` is set and that the config file points -to the Marathon instance you want to use for integration tests. If you're -happy to use the default test configuration which assumes there is a Marathon -instance running on localhost, set :code:`DCOS_CONFIG` as follows:: - - export DCOS_CONFIG=$(pwd)/tests/data/dcos.toml +You can set :code:`DCOS_CONFIG` to a config file that points to a DCOS +cluster you want to use for integration tests. This defaults to +:code:`~/.dcos/dcos.toml` If you are testing against the DCOS Image you can configure the URL to the Exhibitor:: diff --git a/cli/bin/env-setup b/cli/bin/env-setup index 226ff75..60e7d13 100644 --- a/cli/bin/env-setup +++ b/cli/bin/env-setup @@ -6,12 +6,8 @@ else BIN_DIR=$PWD/bin fi +# real, absolute path to BIN_DIR FULL_BIN_PATH=$(python -c "import os; print(os.path.realpath('$BIN_DIR'))") +# ensure BIN_DIR is prepended to PATH expr "$PATH" : "${FULL_BIN_PATH}.*" > /dev/null || export PATH=$FULL_BIN_PATH:$PATH -export DCOS_CONFIG=~/.dcos/dcos.toml - -if [ ! -f "$DCOS_CONFIG" ]; then - mkdir -p $(dirname "$DCOS_CONFIG") - touch "$DCOS_CONFIG" -fi diff --git a/cli/dcoscli/config/main.py b/cli/dcoscli/config/main.py index 7fe96bf..fe8e9c2 100644 --- a/cli/dcoscli/config/main.py +++ b/cli/dcoscli/config/main.py @@ -24,15 +24,12 @@ Positional Arguments: import collections import copy import json -import os import dcoscli import docopt import pkg_resources import six -import toml -from dcos import (cmds, config, constants, emitting, http, jsonitem, - subcommand, util) +from dcos import cmds, config, emitting, http, jsonitem, subcommand, util from dcos.errors import DCOSException from dcoscli import analytics @@ -153,7 +150,7 @@ def _set(name, value): :rtype: int """ - config_path, toml_config = _load_config() + toml_config = util.get_config(True) section, subkey = _split_key(name) @@ -172,7 +169,7 @@ def _set(name, value): _check_config(toml_config_pre, toml_config) - _save_config_file(config_path, toml_config) + config.save(toml_config) return 0 @@ -182,7 +179,7 @@ def _append(name, value): :rtype: int """ - config_path, toml_config = _load_config() + toml_config = util.get_config(True) python_value = _parse_array_item(name, value) toml_config_pre = copy.deepcopy(toml_config) @@ -194,7 +191,7 @@ def _append(name, value): _check_config(toml_config_pre, toml_config) - _save_config_file(config_path, toml_config) + config.save(toml_config) return 0 @@ -204,7 +201,7 @@ def _prepend(name, value): :rtype: int """ - config_path, toml_config = _load_config() + toml_config = util.get_config(True) python_value = _parse_array_item(name, value) @@ -215,7 +212,7 @@ def _prepend(name, value): toml_config[name] = python_value + toml_config.get(name, []) _check_config(toml_config_pre, toml_config) - _save_config_file(config_path, toml_config) + config.save(toml_config) return 0 @@ -225,7 +222,7 @@ def _unset(name, index): :rtype: int """ - config_path, toml_config = _load_config() + toml_config = util.get_config(True) toml_config_pre = copy.deepcopy(toml_config) section = name.split(".", 1)[0] if section not in toml_config_pre._dictionary: @@ -251,7 +248,7 @@ def _unset(name, index): raise DCOSException( 'Unsetting based on an index is only supported for lists') - _save_config_file(config_path, toml_config) + config.save(toml_config) return 0 @@ -261,7 +258,7 @@ def _show(name): :rtype: int """ - _, toml_config = _load_config() + toml_config = util.get_config(True) if name is not None: value = toml_config.get(name) @@ -285,7 +282,7 @@ def _validate(): :rtype: int """ - _, toml_config = _load_config() + toml_config = util.get_config(True) errs = util.validate_json(toml_config._dictionary, _generate_root_schema(toml_config)) @@ -338,29 +335,6 @@ def _generate_choice_msg(name, value): return message -def _load_config(): - """ - :returns: process status - :rtype: int - """ - - config_path = os.environ[constants.DCOS_CONFIG_ENV] - return (config_path, config.mutable_load_from_path(config_path)) - - -def _save_config_file(config_path, toml_config): - """ - :param config_path: path to configuration file. - :type config_path: str - :param toml_config: TOML configuration object - :type toml_config: MutableToml or Toml - """ - - serial = toml.dumps(toml_config._dictionary) - with util.open_file(config_path, 'w') as config_file: - config_file.write(serial) - - def _get_config_schema(command): """ :param command: the subcommand name diff --git a/cli/dcoscli/main.py b/cli/dcoscli/main.py index 4e15cbf..d062409 100644 --- a/cli/dcoscli/main.py +++ b/cli/dcoscli/main.py @@ -29,6 +29,7 @@ Environment Variables: DCOS_CONFIG This environment variable points to the location of the DCOS configuration file. + [default: ~/.dcos/dcos.toml] DCOS_DEBUG If set then enable further debug messages which are sent to stdout. @@ -59,9 +60,6 @@ def main(): def _main(): signal.signal(signal.SIGINT, signal_handler) - if not _is_valid_configuration(): - return 1 - args = docopt.docopt( __doc__, version='dcos version {}'.format(dcoscli.version), @@ -128,27 +126,6 @@ def _config_debug_environ(is_debug): os.environ.pop(constants.DCOS_DEBUG_ENV, None) -def _is_valid_configuration(): - """Validates running environment - - :returns: True if the environment is configure correctly; False otherwise. - :rtype: bool - """ - - dcos_config = os.environ.get(constants.DCOS_CONFIG_ENV) - if dcos_config is None: - msg = 'Environment variable {!r} must be set to the DCOS config file.' - emitter.publish(msg.format(constants.DCOS_CONFIG_ENV)) - return False - - if not os.path.isfile(dcos_config): - msg = 'Environment variable {!r} maps to {!r} and it is not a file.' - emitter.publish(msg.format(constants.DCOS_CONFIG_ENV, dcos_config)) - return False - - return True - - def signal_handler(signal, frame): emitter.publish( errors.DefaultError("User interrupted command with Ctrl-C")) diff --git a/cli/tests/integrations/test_dcos.py b/cli/tests/integrations/test_dcos.py index 9a4daa5..471da7f 100644 --- a/cli/tests/integrations/test_dcos.py +++ b/cli/tests/integrations/test_dcos.py @@ -1,7 +1,3 @@ -import os - -from dcos import constants - from .common import assert_command, exec_command @@ -62,6 +58,7 @@ Environment Variables: DCOS_CONFIG This environment variable points to the location of the DCOS configuration file. + [default: ~/.dcos/dcos.toml] DCOS_DEBUG If set then enable further debug messages which are sent to stdout. @@ -76,38 +73,6 @@ def test_version(): stdout=b'dcos version SNAPSHOT\n') -def test_missing_dcos_config(): - env = os.environ.copy() - del env['DCOS_CONFIG'] - env.update({ - constants.PATH_ENV: os.environ[constants.PATH_ENV], - }) - - stdout = (b"Environment variable 'DCOS_CONFIG' must be set " - b"to the DCOS config file.\n") - - assert_command(['dcos'], - stdout=stdout, - returncode=1, - env=env) - - -def test_dcos_config_not_a_file(): - env = os.environ.copy() - env.update({ - constants.PATH_ENV: os.environ[constants.PATH_ENV], - 'DCOS_CONFIG': 'missing/file', - }) - - stdout = (b"Environment variable 'DCOS_CONFIG' maps to " - b"'missing/file' and it is not a file.\n") - - assert_command(['dcos'], - returncode=1, - stdout=stdout, - env=env) - - def test_log_level_flag(): returncode, stdout, stderr = exec_command( ['dcos', '--log-level=info', 'config', '--info']) diff --git a/dcos/auth.py b/dcos/auth.py index fafe569..91e38d6 100644 --- a/dcos/auth.py +++ b/dcos/auth.py @@ -1,11 +1,9 @@ import json -import os import sys import uuid import pkg_resources -import toml -from dcos import config, constants, emitting, errors, http, jsonitem, util +from dcos import config, emitting, errors, http, jsonitem, util from dcos.errors import DCOSException from six import iteritems @@ -124,8 +122,7 @@ def _save_auth_keys(key_dict): :rtype: None """ - config_path = os.environ[constants.DCOS_CONFIG_ENV] - toml_config = config.mutable_load_from_path(config_path) + toml_config = util.get_config(True) section = 'core' config_schema = json.loads( @@ -137,8 +134,5 @@ def _save_auth_keys(key_dict): name = '{}.{}'.format(section, k) toml_config[name] = python_value - serial = toml.dumps(toml_config._dictionary) - with util.open_file(config_path, 'w') as config_file: - config_file.write(serial) - + config.save(toml_config) return None diff --git a/dcos/config.py b/dcos/config.py index 3d0f72f..d93b9d0 100644 --- a/dcos/config.py +++ b/dcos/config.py @@ -4,30 +4,33 @@ import toml from dcos import util -def mutable_load_from_path(path): - """Loads a TOML file from the path - - :param path: Path to the TOML file - :type path: str - :returns: Mutable map for the configuration file - :rtype: MutableToml - """ - - with util.open_file(path) as config_file: - return MutableToml(toml.loads(config_file.read())) - - -def load_from_path(path): +def load_from_path(path, mutable=False): """Loads a TOML file from the path :param path: Path to the TOML file :type path: str + :param mutable: True if the returned Toml object should be mutable + :type mutable: boolean :returns: Map for the configuration file - :rtype: Toml + :rtype: Toml | MutableToml """ - with util.open_file(path) as config_file: - return Toml(toml.loads(config_file.read())) + util.ensure_file_exists(path) + with util.open_file(path, 'r') as config_file: + toml_obj = toml.loads(config_file.read()) + return (MutableToml if mutable else Toml)(toml_obj) + + +def save(toml_config): + """ + :param toml_config: TOML configuration object + :type toml_config: MutableToml or Toml + """ + + serial = toml.dumps(toml_config._dictionary) + path = util.get_config_path() + with util.open_file(path, 'w') as config_file: + config_file.write(serial) def _get_path(config, path): diff --git a/dcos/subcommand.py b/dcos/subcommand.py index 28996ed..ce47c9d 100644 --- a/dcos/subcommand.py +++ b/dcos/subcommand.py @@ -275,7 +275,7 @@ def install(pkg, revision, options): """ pkg_dir = package_dir(pkg.name()) - util.ensure_dir(pkg_dir) + util.ensure_dir_exists(pkg_dir) _write_package_json(pkg, revision) _write_package_revision(pkg, revision) diff --git a/dcos/util.py b/dcos/util.py index 89b5273..ea58b32 100644 --- a/dcos/util.py +++ b/dcos/util.py @@ -74,7 +74,7 @@ def temptext(): shutil.rmtree(path, ignore_errors=True) -def ensure_dir(directory): +def ensure_dir_exists(directory): """If `directory` does not exist, create it. :param directory: path to the directory @@ -92,6 +92,22 @@ def ensure_dir(directory): 'Cannot create directory [{}]: {}'.format(directory, e)) +def ensure_file_exists(path): + """ Create file if it doesn't exist + + :param path: path of file to create + :type path: str + :rtype: None + """ + + if not os.path.exists(path): + try: + open(path, 'w').close() + except IOError as e: + raise DCOSException( + 'Cannot create file [{}]: {}'.format(path, e)) + + def read_file(path): """ :param path: path to file @@ -106,17 +122,36 @@ def read_file(path): return file_.read() -def get_config(): +def get_config_path(): + """ Returns the path to the DCOS config file. + + :returns: path to the DCOS config file + :rtype: str """ + + default = os.path.expanduser( + os.path.join("~", + constants.DCOS_DIR, + 'dcos.toml')) + + return os.environ.get(constants.DCOS_CONFIG_ENV, default) + + +def get_config(mutable=False): + """ Returns the DCOS configuration object + + :param mutable: True if the returned Toml object should be mutable + :type mutable: boolean :returns: Configuration object - :rtype: Toml + :rtype: Toml | MutableToml """ # avoid circular import from dcos import config - return config.load_from_path( - os.environ[constants.DCOS_CONFIG_ENV]) + path = get_config_path() + + return config.load_from_path(path, mutable) def get_config_vals(keys, config=None):