diff --git a/cli/dcoscli/analytics.py b/cli/dcoscli/analytics.py index c70ea4f..d22c558 100644 --- a/cli/dcoscli/analytics.py +++ b/cli/dcoscli/analytics.py @@ -1,5 +1,4 @@ import json -import logging import os import sys import uuid @@ -7,7 +6,7 @@ import uuid import dcoscli import requests import rollbar -from dcos.api import config, constants +from dcos.api import config, constants, util from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY, SEGMENT_IO_CLI_ERROR_EVENT, SEGMENT_IO_CLI_EVENT, SEGMENT_IO_WRITE_KEY_DEV, @@ -15,7 +14,7 @@ from dcoscli.constants import (ROLLBAR_SERVER_POST_KEY, from futures import ThreadPoolExecutor from requests.auth import HTTPBasicAuth -logger = logging.getLogger(__name__) +logger = util.get_logger(__name__) session_id = uuid.uuid4().hex diff --git a/cli/dcoscli/package/main.py b/cli/dcoscli/package/main.py index 8c65d45..594174e 100644 --- a/cli/dcoscli/package/main.py +++ b/cli/dcoscli/package/main.py @@ -50,7 +50,7 @@ import dcoscli import docopt import pkg_resources from dcos.api import (cmds, config, constants, emitting, errors, marathon, - options, package, util) + options, package, subcommand, util) emitter = emitting.FlatEmitter() @@ -256,13 +256,13 @@ def _describe(package_name): return 0 -def _install(package_name, options_file, app_id, cli, app): +def _install(package_name, options_path, app_id, cli, app): """Install the specified package. :param package_name: the package to install :type package_name: str - :param options_file: path to file containing option values - :type options_file: str + :param options_path: path to file containing option values + :type options_path: str :param app_id: app ID for installation of this package :type app_id: str :param cli: indicates if the cli should be installed @@ -291,23 +291,26 @@ def _install(package_name, options_file, app_id, cli, app): "repositories")) return 1 - options_json = {} - - if options_file is not None: - try: - options_fd = open(options_file) - options_json = json.load(options_fd) - except Exception as e: - emitter.publish(e.message) - return 1 - # TODO(CD): Make package version to install configurable pkg_version, version_error = pkg.latest_version() - if version_error is not None: emitter.publish(version_error) return 1 + if options_path is None: + options = {} + else: + try: + with open(options_path) as options_file: + user_options = json.load(options_file) + options, err = pkg.options(pkg_version, user_options) + if err is not None: + emitter.publish(err) + return 1 + except Exception as e: + emitter.publish(errors.DefaultError(e.message)) + return 1 + if app: # Install in Marathon version_map, version_error = pkg.software_versions() @@ -331,8 +334,9 @@ def _install(package_name, options_file, app_id, cli, app): pkg, pkg_version, init_client, - options_json, + options, app_id) + if install_error is not None: emitter.publish(install_error) return 1 @@ -342,7 +346,7 @@ def _install(package_name, options_file, app_id, cli, app): emitter.publish('Installing CLI subcommand for package [{}]'.format( pkg.name())) - err = package.install_subcommand(pkg, pkg_version, options_json) + err = subcommand.install(pkg, pkg_version, options) if err is not None: emitter.publish(err) return 1 diff --git a/cli/dcoscli/subcommand/main.py b/cli/dcoscli/subcommand/main.py index f80a475..2a2f37b 100644 --- a/cli/dcoscli/subcommand/main.py +++ b/cli/dcoscli/subcommand/main.py @@ -129,11 +129,9 @@ def _install(package): dcos_config = config.load_from_path(os.environ[constants.DCOS_CONFIG_ENV]) - install_operation = { - 'pip': [package] - } + pip_operation = [package] if 'subcommand.pip_find_links' in dcos_config: - install_operation['pip'].append( + pip_operation.append( '--find-links {}'.format(dcos_config['subcommand.pip_find_links'])) distribution_name, err = _distribution_name(package) @@ -143,10 +141,13 @@ def _install(package): subcommand for a package, run `dcos package install --cli` instead")) return 1 - err = subcommand.install( + env_dir = os.path.join(subcommand.package_dir(distribution_name), + constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR) + + err = subcommand.install_with_pip( distribution_name, - install_operation, - util.dcos_path()) + env_dir, + pip_operation) if err is not None: emitter.publish(err) return 1 diff --git a/cli/tests/integrations/cli/test_package.py b/cli/tests/integrations/cli/test_package.py index 9dce6db..80e8da3 100644 --- a/cli/tests/integrations/cli/test_package.py +++ b/cli/tests/integrations/cli/test_package.py @@ -1,6 +1,8 @@ import json +import os import six +from dcos.api import subcommand from common import exec_command @@ -149,7 +151,7 @@ def test_bad_install(): '--options=tests/data/package/mesos-dns-config-bad.json']) assert returncode == 1 - assert stdout == b'Installing package [mesos-dns] version [alpha]\n' + assert stdout == b'' assert stderr == b"""\ Error: 'mesos-dns/config-url' is a required property @@ -174,36 +176,81 @@ def test_install(): assert stderr == b'' -def test_package_labels(): - app_labels = get_app_labels('mesos-dns') - expected_metadata = b"""\ -eyJkZXNjcmlwdGlvbiI6ICJETlMtYmFzZWQgc2VydmljZSBkaXNjb3ZlcnkgZm9yIE1lc29zLiIsI\ -CJtYWludGFpbmVyIjogInN1cHBvcnRAbWVzb3NwaGVyZS5pbyIsICJuYW1lIjogIm1lc29zLWRucy\ -IsICJwb3N0SW5zdGFsbE5vdGVzIjogIlBsZWFzZSByZWZlciB0byB0aGUgdHV0b3JpYWwgaW5zdHJ\ -1Y3Rpb25zIGZvciBmdXJ0aGVyIHNldHVwIHJlcXVpcmVtZW50czogaHR0cDovL21lc29zcGhlcmUu\ -Z2l0aHViLmlvL21lc29zLWRucy9kb2NzL3R1dG9yaWFsLWdjZS5odG1sIiwgInNjbSI6ICJodHRwc\ -zovL2dpdGh1Yi5jb20vbWVzb3NwaGVyZS9tZXNvcy1kbnMuZ2l0IiwgInRhZ3MiOiBbIm1lc29zcG\ -hlcmUiXSwgInZlcnNpb24iOiAiYWxwaGEiLCAid2Vic2l0ZSI6ICJodHRwOi8vbWVzb3NwaGVyZS5\ -naXRodWIuaW8vbWVzb3MtZG5zIn0=\ +def test_package_metadata(): + returncode, stdout, stderr = exec_command(['dcos', + 'package', + 'install', + 'helloworld']) + + assert returncode == 0 + assert stdout == b"""Installing package [helloworld] version [0.1.0] +Installing CLI subcommand for package [helloworld] """ - actual_metadata = app_labels.get('DCOS_PACKAGE_METADATA') - assert(six.b(actual_metadata) == expected_metadata) + assert stderr == b'' - expected_registry_version = b'0.1.0-alpha' - actual_registry_version = app_labels.get('DCOS_PACKAGE_REGISTRY_VERSION') - assert(six.b(actual_registry_version) == expected_registry_version) + # test marathon labels + expected_metadata = b"""eyJkZXNjcmlwdGlvbiI6ICJFeGFtcGxlIERDT1MgYXBwbGljYX\ +Rpb24gcGFja2FnZSIsICJtYWludGFpbmVyIjogInN1cHBvcnRAbWVzb3NwaGVyZS5pbyIsICJuYW1l\ +IjogImhlbGxvd29ybGQiLCAidGFncyI6IFsibWVzb3NwaGVyZSIsICJleGFtcGxlIiwgInN1YmNvbW\ +1hbmQiXSwgInZlcnNpb24iOiAiMC4xLjAiLCAid2Vic2l0ZSI6ICJodHRwczovL2dpdGh1Yi5jb20v\ +bWVzb3NwaGVyZS9kY29zLWhlbGxvd29ybGQifQ==""" - expected_name = b'mesos-dns' - actual_name = app_labels.get('DCOS_PACKAGE_NAME') - assert(six.b(actual_name) == expected_name) - - expected_version = b'alpha' - actual_version = app_labels.get('DCOS_PACKAGE_VERSION') - assert(six.b(actual_version) == expected_version) + expected_command = b"""eyJwaXAiOiBbImh0dHA6Ly9kb3dubG9hZHMubWVzb3NwaGVyZS5\ +pby9kY29zLWNsaS9kY29zLTAuMS4wLXB5Mi5weTMtbm9uZS1hbnkud2hsIiwgImdpdCtodHRwczovL\ +2dpdGh1Yi5jb20vbWVzb3NwaGVyZS9kY29zLWhlbGxvd29ybGQuZ2l0I2Rjb3MtaGVsbG93b3JsZD0\ +wLjEuMCJdfQ==""" expected_source = b'git://github.com/mesosphere/universe.git' - actual_source = app_labels.get('DCOS_PACKAGE_SOURCE') - assert(six.b(actual_source) == expected_source) + + expected_labels = { + 'DCOS_PACKAGE_METADATA': expected_metadata, + 'DCOS_PACKAGE_COMMAND': expected_command, + 'DCOS_PACKAGE_REGISTRY_VERSION': b'0.1.0-alpha', + 'DCOS_PACKAGE_NAME': b'helloworld', + 'DCOS_PACKAGE_VERSION': b'0.1.0', + 'DCOS_PACKAGE_SOURCE': expected_source, + 'DCOS_PACKAGE_RELEASE': b'0', + } + + app_labels = get_app_labels('helloworld') + + for label, value in expected_labels.items(): + assert value == six.b(app_labels.get(label)) + + # test local package.json + package = { + "website": "https://github.com/mesosphere/dcos-helloworld", + "maintainer": "support@mesosphere.io", + "name": "helloworld", + "tags": ["mesosphere", "example", "subcommand"], + "version": "0.1.0", + "description": "Example DCOS application package" + } + + package_dir = subcommand.package_dir('helloworld') + + # test local package.json + package_path = os.path.join(package_dir, 'package.json') + with open(package_path) as f: + assert json.load(f) == package + + # test local source + source_path = os.path.join(package_dir, 'source') + with open(source_path) as f: + assert six.b(f.read()) == expected_source + + # test local version + version_path = os.path.join(package_dir, 'version') + with open(version_path) as f: + assert six.b(f.read()) == b'0' + + # uninstall helloworld + returncode, stdout, stderr = exec_command( + ['dcos', 'package', 'uninstall', 'helloworld']) + + assert returncode == 0 + assert stdout == b'' + assert stderr == b'' def test_install_with_id(): diff --git a/dcos/api/constants.py b/dcos/api/constants.py index 8a72b7c..87193d3 100644 --- a/dcos/api/constants.py +++ b/dcos/api/constants.py @@ -1,6 +1,9 @@ DCOS_DIR = ".dcos" """DCOS data directory. Can store subcommands and the config file.""" +DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR = 'env' +"""In a package's directory, this is the virtualenv subdirectory.""" + DCOS_SUBCOMMAND_SUBDIR = 'subcommands' """Name of the subdirectory that contains all of the subcommands. This is relative to the location of the executable.""" diff --git a/dcos/api/package.py b/dcos/api/package.py index af1e91c..e73ed2f 100644 --- a/dcos/api/package.py +++ b/dcos/api/package.py @@ -28,42 +28,11 @@ PACKAGE_NAME_KEY = 'DCOS_PACKAGE_NAME' PACKAGE_VERSION_KEY = 'DCOS_PACKAGE_VERSION' PACKAGE_SOURCE_KEY = 'DCOS_PACKAGE_SOURCE' PACKAGE_FRAMEWORK_KEY = 'DCOS_PACKAGE_IS_FRAMEWORK' +PACKAGE_RELEASE_KEY = 'DCOS_PACKAGE_RELEASE' +PACKAGE_COMMAND_KEY = 'DCOS_PACKAGE_COMMAND' -def _merge_options(pkg, version, user_options): - """ - :param pkg: the package to install - :type pkg: Package - :param version: the package version to install - :type version: str - :param user_options: package parameters - :type user_options: dict - :returns: a dictionary with the user supplied options - :rtype: (dict, dcos.api.errors.Error) - """ - if user_options is None: - user_options = {} - - config_schema, err = pkg.config_json(version) - if err is not None: - return (None, err) - - default_options, err = _extract_default_values(config_schema) - if err is not None: - return (None, err) - - # Merge option overrides - options = dict(list(default_options.items()) + list(user_options.items())) - - # Validate options with the config schema - err = util.validate_json(options, config_schema) - if err is not None: - return (None, err) - - return (options, None) - - -def install_app(pkg, version, init_client, user_options, app_id): +def install_app(pkg, version, init_client, options, app_id): """Installs a package's application :param pkg: the package to install @@ -72,29 +41,20 @@ def install_app(pkg, version, init_client, user_options, app_id): :type version: str :param init_client: the program to use to run the package :type init_client: object - :param user_options: package parameters - :type user_options: dict + :param options: package parameters + :type options: dict :param app_id: app ID for installation of this package :type app_id: str :rtype: Error """ - options, err = _merge_options(pkg, version, user_options) - if err is not None: - return err - # Insert option parameters into the init template - template, err = pkg.marathon_template(version) - if err is not None: - return err - - # Render the init template with the marshaled options - init_desc, err = util.render_mustache_json(template, options) + init_desc, err = pkg.marathon_json(version, options) if err is not None: return err # Add package metadata - package_labels, err = _make_package_labels(pkg, version) + package_labels, err = _make_package_labels(pkg, version, options) if err is not None: return err @@ -115,23 +75,24 @@ def install_app(pkg, version, init_client, user_options, app_id): return err -def _make_package_labels(pkg, version): - """ +def _make_package_labels(pkg, version, options): + """Returns Marathon app labels for a package. + :param pkg: The package to install :type pkg: Package :param version: The package version to install :type version: str + :param options: package parameters + :type options: dict + :returns: Marathon app labels :rtype: (dict, Error) """ metadata, meta_error = pkg.package_json(version) - if meta_error is not None: return (None, meta_error) - metadata_json_string = json.dumps(metadata, sort_keys=True) - metadata_bytes = six.b(metadata_json_string) - encoded_metadata = base64.b64encode(metadata_bytes).decode('utf-8') + encoded_metadata = _base64_encode(metadata) is_framework = metadata.get('framework') if not is_framework: @@ -148,43 +109,32 @@ def _make_package_labels(pkg, version): PACKAGE_VERSION_KEY: metadata['version'], PACKAGE_SOURCE_KEY: pkg.registry.source.url, PACKAGE_FRAMEWORK_KEY: str(is_framework), - PACKAGE_REGISTRY_VERSION_KEY: package_registry_version + PACKAGE_REGISTRY_VERSION_KEY: package_registry_version, + PACKAGE_RELEASE_KEY: str(version) } + if pkg.is_command_defined(version): + command, cmd_error = pkg.command_json(version, options) + if cmd_error is not None: + return (None, cmd_error) + + package_labels[PACKAGE_COMMAND_KEY] = _base64_encode(command) + return (package_labels, None) -def install_subcommand(pkg, version, user_options): - """Installs a package's command line interface +def _base64_encode(dictionary): + """Returns base64(json(dictionary)). - :param pkg: the package to install - :type pkg: Package - :param version: the package version to install - :type version: str - :param user_options: package parameters - :type user_options: dict - :rtype: dcos.api.errors.Error + :param dictionary: dict to encode + :type dictionary: dict + :returns: base64 encoding + :rtype: str """ - options, err = _merge_options(pkg, version, user_options) - if err is not None: - return err - - # Insert option parameters into the init template - init_template, err = pkg.command_template(version) - if err is not None: - return err - - rendered_template = pystache.render(init_template, options) - - install_operation, err = util.load_jsons(rendered_template) - if err is not None: - return err - - return subcommand.install( - pkg.name(), - install_operation, - util.dcos_path()) + json_str = json.dumps(dictionary, sort_keys=True) + str_bytes = six.b(json_str) + return base64.b64encode(str_bytes).decode('utf-8') def uninstall(package_name, remove_all, app_id, init_client): @@ -875,7 +825,7 @@ class Registry(): def source(self): """Returns the associated upstream package source for this registry. - :rtype: str + :rtype: Source """ return self._source @@ -972,7 +922,6 @@ class Package(): """ def __init__(self, registry, path): - assert os.path.isdir(path) self._registry = registry self.path = path @@ -986,6 +935,40 @@ class Package(): return os.path.basename(self.path) + def options(self, version, user_options): + """Merges package options with user supplied options, validates, and + returns the result. + + :param version: the package version to install + :type version: str + :param user_options: package parameters + :type user_options: dict + :returns: a dictionary with the user supplied options + :rtype: (dict, dcos.api.errors.Error) + """ + + if user_options is None: + user_options = {} + + config_schema, err = self.config_json(version) + if err is not None: + return (None, err) + + default_options, err = _extract_default_values(config_schema) + if err is not None: + return (None, err) + + # Merge option overrides + options = dict(list(default_options.items()) + + list(user_options.items())) + + # Validate options with the config schema + err = util.validate_json(options, config_schema) + if err is not None: + return (None, err) + + return (options, None) + @property def registry(self): """Returns the containing registry for this package. @@ -1008,15 +991,6 @@ class Package(): self.path, os.path.join(version, 'command.json'))) - def command_template(self, version): - """Returns the JSON content of the command.json file. - - :returns: Package command data - :rtype: (str, Error) - """ - - return self._data(os.path.join(version, 'command.json')) - def config_json(self, version): """Returns the JSON content of the config.json file. @@ -1029,20 +1003,67 @@ class Package(): def package_json(self, version): """Returns the JSON content of the package.json file. + :param version: the package version + :type version: str :returns: Package data :rtype: (dict, Error) """ return self._json(os.path.join(version, 'package.json')) - def marathon_template(self, version): - """Returns the JSON content of the marathon.json file. + def marathon_json(self, version, options): + """Returns the JSON content of the marathon.json template, after + rendering it with options. - :returns: Package marathon data - :rtype: (str, Error) + :param version: the package version + :type version: str + :param options: the template options to use in rendering + :type options: dict + :rtype: (dict, Error) """ - return self._data(os.path.join(version, 'marathon.json')) + return self._render_template('marathon.json', version, options) + + def command_json(self, version, options): + """Returns the JSON content of the comand.json template, after + rendering it with options. + + :param version: the package version + :type version: str + :param options: the template options to use in rendering + :type options: dict + :returns: Package data + :rtype: (dict, Error) + """ + + template, err = self._data(os.path.join(version, 'command.json')) + if err is not None: + return (None, err) + + rendered = pystache.render(template, options) + return (json.loads(rendered), None) + + def _render_template(self, name, version, options): + """Render a template. + + :param name: the file name of the template + :type name: str + :param version: the package version + :type version: str + :param options: the template options to use in rendering + :type options: dict + :rtype: (dict, Error) + """ + + template, err = self._data(os.path.join(version, name)) + if err is not None: + return (None, err) + + json, err = util.render_mustache_json(template, options) + if err is not None: + return (None, err) + + return (json, None) def _json(self, path): """Returns the json content of the supplied file, relative to the diff --git a/dcos/api/subcommand.py b/dcos/api/subcommand.py index 2b521a0..fa26069 100644 --- a/dcos/api/subcommand.py +++ b/dcos/api/subcommand.py @@ -38,21 +38,6 @@ def command_executables(subcommand, dcos_path): return (executables[0], None) -BIN_DIRECTORY = 'Scripts' if util.is_windows_platform() else 'bin' - - -def _subcommand_dir(): - """Returns path to the subcommand directory. This directory contains - a virtualenv for each installed subcommand. - - :returns: path to the subcommand directory - :rtype: str - """ - return os.path.expanduser(os.path.join("~", - constants.DCOS_DIR, - constants.DCOS_SUBCOMMAND_SUBDIR)) - - def list_paths(dcos_path): """List the real path to executable dcos subcommand programs. @@ -71,24 +56,19 @@ def list_paths(dcos_path): _is_executable(os.path.join(binpath, filename))) ] - subcommand_directory = _subcommand_dir() + subcommands = [] + for package in distributions(dcos_path): + bin_dir = os.path.join(package_dir(package), + constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR, + BIN_DIRECTORY) - subcommands = [ - os.path.join(subcommand_directory, package, BIN_DIRECTORY, filename) + for filename in os.listdir(bin_dir): + path = os.path.join(bin_dir, filename) - for package in distributions(dcos_path) + if (filename.startswith(constants.DCOS_COMMAND_PREFIX) and + _is_executable(path)): - for filename in os.listdir( - os.path.join(subcommand_directory, package, BIN_DIRECTORY)) - - if (filename.startswith(constants.DCOS_COMMAND_PREFIX) and - _is_executable( - os.path.join( - subcommand_directory, - package, - BIN_DIRECTORY, - filename))) - ] + subcommands.append(path) return commands + subcommands @@ -114,10 +94,17 @@ def distributions(dcos_path): :rtype: list of str """ - subcommand_directory = _subcommand_dir() + subcommand_dir = _subcommand_dir() - if os.path.isdir(subcommand_directory): - return os.listdir(subcommand_directory) + if os.path.isdir(subcommand_dir): + return [ + subdir for subdir in os.listdir(subcommand_dir) + if os.path.isdir( + os.path.join( + subcommand_dir, + subdir, + constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR)) + ] else: return [] @@ -182,32 +169,88 @@ def noun(executable_path): return noun -def install(distribution_name, install_operation, dcos_path): - """Installs the dcos cli subcommand +def _write_package_json(pkg, version): + """ Write package.json locally. - :param distribution_name: the name of the package - :type distribution_name: str - :param install_operation: operation to use to install subcommand - :type install_operation: dict - :param dcos_path: path to the dcos cli directory - :type dcos_path: str + :param pkg: the package being installed + :type pkg: Package + :param version: the package version to install + :type version: str + :rtype: Error + """ + + pkg_dir = package_dir(pkg.name()) + + package_path = os.path.join(pkg_dir, 'package.json') + + package_json, err = pkg.package_json(version) + if err is not None: + return err + + with open(package_path, 'w') as package_file: + json.dump(package_json, package_file) + + +def _write_package_version(pkg, version): + """ Write package version locally. + + :param pkg: the package being installed + :type pkg: Package + :param version: the package version to install + :type version: str + :rtype: None + """ + + pkg_dir = package_dir(pkg.name()) + + version_path = os.path.join(pkg_dir, 'version') + + with open(version_path, 'w') as version_file: + version_file.write(version) + + +def _write_package_source(pkg): + """ Write package source locally. + + :param pkg: the package being installed + :type pkg: Package + :rtype: None + """ + + pkg_dir = package_dir(pkg.name()) + + source_path = os.path.join(pkg_dir, 'source') + + with open(source_path, 'w') as source_file: + source_file.write(pkg.registry.source.url) + + +def _install_env(pkg, version, options): + """ Install subcommand virtual env. + + :param pkg: the package to install + :type pkg: Package + :param version: the package version to install + :type version: str + :param options: package parameters + :type options: dict :returns: an error if the subcommand failed; None otherwise :rtype: dcos.api.errors.Error """ - subcommand_directory = _subcommand_dir() + pkg_dir = package_dir(pkg.name()) - if not os.path.exists(subcommand_directory): - logger.info('Creating directory: %r', subcommand_directory) - os.makedirs(subcommand_directory, 0o775) + install_operation, err = pkg.command_json(version, options) + if err is not None: + return err - package_directory = os.path.join(subcommand_directory, distribution_name) + env_dir = os.path.join(pkg_dir, + constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR) if 'pip' in install_operation: - return _install_with_pip( - distribution_name, - os.path.join(dcos_path, BIN_DIRECTORY), - package_directory, + return install_with_pip( + pkg.name(), + env_dir, install_operation['pip']) else: return errors.DefaultError( @@ -215,53 +258,100 @@ def install(distribution_name, install_operation, dcos_path): install_operation.keys())) -def uninstall(distribution_name, dcos_path): +def install(pkg, version, options): + """Installs the dcos cli subcommand + + :param pkg: the package to install + :type pkg: Package + :param version: the package version to install + :type version: str + :param options: package parameters + :type options: dict + :returns: an error if the subcommand failed; None otherwise + :rtype: dcos.api.errors.Error + """ + + pkg_dir = package_dir(pkg.name()) + util.ensure_dir(pkg_dir) + + err = _write_package_json(pkg, version) + if err is not None: + return err + + _write_package_version(pkg, version) + + _write_package_source(pkg) + + return _install_env(pkg, version, options) + + +def _subcommand_dir(): + """ Returns ~/.dcos/subcommands """ + return os.path.expanduser(os.path.join("~", + constants.DCOS_DIR, + constants.DCOS_SUBCOMMAND_SUBDIR)) + + +# TODO(mgummelt): should be made private after "dcos subcommand" is removed +def package_dir(name): + """ Returns ~/.dcos/subcommands/ + + :param name: package name + :type name: str + :rtype: str + """ + return os.path.join(_subcommand_dir(), + name) + + +def uninstall(package_name, dcos_path): """Uninstall the dcos cli subcommand - :param distribution_name: the name of the package - :type distribution_name: str + :param package_name: the name of the package + :type package_name: str :param dcos_path: the path to the dcos cli directory :type dcos_path: str :returns: True if the subcommand was uninstalled :rtype: bool """ - subcommand_directory = os.path.join(_subcommand_dir(), distribution_name) + pkg_dir = package_dir(package_name) - if os.path.isdir(subcommand_directory): - shutil.rmtree(subcommand_directory) + if os.path.isdir(pkg_dir): + shutil.rmtree(pkg_dir) return True return False +BIN_DIRECTORY = 'Scripts' if util.is_windows_platform() else 'bin' -def _install_with_pip( - distribution_name, - bin_directory, - package_directory, + +# TODO (mgummelt): should be made private after "dcos subcommand" is removed +def install_with_pip( + package_name, + env_directory, requirements): """ - :param distribution_name: the name of the package - :type distribution_name: str - :param bin_directory: the path to the directory containing the - executables (virtualenv, etc). - :type bin_directory: str - :param package_directory: the path to the directory for the package - :type package_directory: str + :param package_name: the name of the package + :type package_name: str + :param env_directory: the path to the directory in which to install the + package's virtual env + :type env_directory: str :param requirements: the list of pip requirements :type requirements: list of str :returns: an Error if it failed to install the package; None otherwise :rtype: dcos.api.errors.Error """ - new_package_dir = not os.path.exists(package_directory) + bin_directory = os.path.join(util.dcos_path(), BIN_DIRECTORY) + new_package_dir = not os.path.exists(env_directory) - pip_path = os.path.join(package_directory, BIN_DIRECTORY, 'pip') + pip_path = os.path.join(env_directory, BIN_DIRECTORY, 'pip') if not os.path.exists(pip_path): - cmd = [os.path.join(bin_directory, 'virtualenv'), package_directory] + cmd = [os.path.join(bin_directory, 'virtualenv'), env_directory] if _execute_command(cmd) != 0: - return _generic_error(distribution_name) + return _generic_error(package_name) with util.temptext() as text_file: fd, requirement_path = text_file @@ -272,7 +362,7 @@ def _install_with_pip( print(line, file=requirements_file) cmd = [ - os.path.join(package_directory, BIN_DIRECTORY, 'pip'), + os.path.join(env_directory, BIN_DIRECTORY, 'pip'), 'install', '--requirement', requirement_path, @@ -281,9 +371,9 @@ def _install_with_pip( if _execute_command(cmd) != 0: # We should remove the diretory that we just created if new_package_dir: - shutil.rmtree(package_directory) + shutil.rmtree(env_directory) - return _generic_error(distribution_name) + return _generic_error(package_name) return None @@ -315,7 +405,7 @@ def _execute_command(command): return process.returncode -def _generic_error(distribution_name): +def _generic_error(package_name): """ :param package: package name :type: str @@ -324,4 +414,4 @@ def _generic_error(distribution_name): """ return errors.DefaultError( - 'Error installing {!r} package'.format(distribution_name)) + 'Error installing {!r} package'.format(package_name)) diff --git a/dcos/api/util.py b/dcos/api/util.py index b06d3a5..3f8dfe4 100644 --- a/dcos/api/util.py +++ b/dcos/api/util.py @@ -57,6 +57,19 @@ def temptext(): shutil.rmtree(path, ignore_errors=True) +def ensure_dir(directory): + """If `directory` does not exist, create it. + + :param directory: path to the directory + :type directory: string + :rtype: None + """ + + if not os.path.exists(directory): + logger.info('Creating directory: %r', directory) + os.makedirs(directory, 0o775) + + def which(program): """Returns the path to the named executable program. @@ -178,7 +191,6 @@ def load_jsons(value): return (json.loads(value), None) except: error = sys.exc_info()[0] - logger = get_logger(__name__) logger.error( 'Unhandled exception while loading JSON: %r -- %r', value, @@ -298,3 +310,5 @@ class CustomJsonRenderer(pystache.Renderer): :rtype: str """ return json.dumps(val) + +logger = get_logger(__name__)