463 lines
12 KiB
Python
463 lines
12 KiB
Python
from __future__ import print_function
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
from dcos import constants, errors, util
|
|
|
|
logger = util.get_logger(__name__)
|
|
|
|
|
|
def command_executables(subcommand, dcos_path):
|
|
"""List the real path to executable dcos program for specified subcommand.
|
|
|
|
:param subcommand: name of subcommand. E.g. marathon
|
|
:type subcommand: str
|
|
:param dcos_path: path to the dcos cli directory
|
|
:type dcos_path: str
|
|
:returns: the dcos program path
|
|
:rtype: (str, dcos.errors.Error)
|
|
"""
|
|
|
|
executables = [
|
|
command_path
|
|
for command_path in list_paths(dcos_path)
|
|
if noun(command_path) == subcommand
|
|
]
|
|
|
|
if len(executables) > 1:
|
|
msg = 'Found more than one executable for command {!r}.'
|
|
return (None, errors.DefaultError(msg.format(subcommand)))
|
|
|
|
if len(executables) == 0:
|
|
msg = "{!r} is not a dcos command."
|
|
return (None, errors.DefaultError(msg.format(subcommand)))
|
|
|
|
return (executables[0], None)
|
|
|
|
|
|
def list_paths(dcos_path):
|
|
"""List the real path to executable dcos subcommand programs.
|
|
|
|
:param dcos_path: path to the dcos cli directory
|
|
:type dcos_path: str
|
|
:returns: list of all the dcos program paths
|
|
:rtype: list of str
|
|
"""
|
|
|
|
# Let's get all the default subcommands
|
|
binpath = os.path.join(dcos_path, BIN_DIRECTORY)
|
|
commands = [
|
|
os.path.join(binpath, filename)
|
|
for filename in os.listdir(binpath)
|
|
if (filename.startswith(constants.DCOS_COMMAND_PREFIX) and
|
|
_is_executable(os.path.join(binpath, filename)))
|
|
]
|
|
|
|
subcommands = []
|
|
for package in distributions(dcos_path):
|
|
bin_dir = os.path.join(package_dir(package),
|
|
constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR,
|
|
BIN_DIRECTORY)
|
|
|
|
for filename in os.listdir(bin_dir):
|
|
path = os.path.join(bin_dir, filename)
|
|
|
|
if (filename.startswith(constants.DCOS_COMMAND_PREFIX) and
|
|
_is_executable(path)):
|
|
|
|
subcommands.append(path)
|
|
|
|
return commands + subcommands
|
|
|
|
|
|
def _is_executable(path):
|
|
"""
|
|
:param path: the path to a program
|
|
:type path: str
|
|
:returns: True if the path is an executable; False otherwise
|
|
:rtype: bool
|
|
"""
|
|
|
|
return os.access(path, os.X_OK) and (
|
|
not util.is_windows_platform() or path.endswith('.exe'))
|
|
|
|
|
|
def distributions(dcos_path):
|
|
"""List all of the installed subcommand packages
|
|
|
|
:param dcos_path: path to the dcos cli directory
|
|
:type dcos_path: str
|
|
:returns: a list of packages
|
|
:rtype: list of str
|
|
"""
|
|
|
|
subcommand_dir = _subcommand_dir()
|
|
|
|
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 []
|
|
|
|
|
|
def documentation(executable_path):
|
|
"""Gather subcommand summary
|
|
|
|
:param executable_path: real path to the dcos subcommands
|
|
:type executable_path: str
|
|
:returns: subcommand and its summary
|
|
:rtype: (str, str)
|
|
"""
|
|
|
|
path_noun = noun(executable_path)
|
|
return (path_noun, info(executable_path, path_noun))
|
|
|
|
|
|
def info(executable_path, path_noun):
|
|
"""Collects subcommand information
|
|
|
|
:param executable_path: real path to the dcos subcommand
|
|
:type executable_path: str
|
|
:param path_noun: subcommand
|
|
:type path_noun: str
|
|
:returns: the subcommand information
|
|
:rtype: str
|
|
"""
|
|
|
|
out = subprocess.check_output(
|
|
[executable_path, path_noun, '--info'])
|
|
|
|
return out.decode('utf-8').strip()
|
|
|
|
|
|
def config_schema(executable_path):
|
|
"""Collects subcommand config schema
|
|
|
|
:param executable_path: real path to the dcos subcommand
|
|
:type executable_path: str
|
|
:returns: the subcommand config schema
|
|
:rtype: dict
|
|
"""
|
|
|
|
out = subprocess.check_output(
|
|
[executable_path, noun(executable_path), '--config-schema'])
|
|
|
|
return json.loads(out.decode('utf-8'))
|
|
|
|
|
|
def noun(executable_path):
|
|
"""Extracts the subcommand single noun from the path to the executable.
|
|
E.g for :code:`bin/dcos-subcommand` this method returns :code:`subcommand`.
|
|
|
|
:param executable_path: real pth to the dcos subcommand
|
|
:type executable_path: str
|
|
:returns: the subcommand
|
|
:rtype: str
|
|
"""
|
|
|
|
basename = os.path.basename(executable_path)
|
|
noun = basename[len(constants.DCOS_COMMAND_PREFIX):].replace('.exe', '')
|
|
return noun
|
|
|
|
|
|
def _write_package_json(pkg, version):
|
|
""" Write package.json locally.
|
|
|
|
: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.errors.Error
|
|
"""
|
|
|
|
pkg_dir = package_dir(pkg.name())
|
|
|
|
install_operation, err = pkg.command_json(version, options)
|
|
if err is not None:
|
|
return err
|
|
|
|
env_dir = os.path.join(pkg_dir,
|
|
constants.DCOS_SUBCOMMAND_VIRTUALENV_SUBDIR)
|
|
|
|
if 'pip' in install_operation:
|
|
return install_with_pip(
|
|
pkg.name(),
|
|
env_dir,
|
|
install_operation['pip'])
|
|
else:
|
|
return errors.DefaultError(
|
|
"Installation methods '{}' not supported".format(
|
|
install_operation.keys()))
|
|
|
|
|
|
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.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/<name>
|
|
|
|
:param name: package name
|
|
:type name: str
|
|
:rtype: str
|
|
"""
|
|
return os.path.join(_subcommand_dir(),
|
|
name)
|
|
|
|
|
|
def uninstall(package_name):
|
|
"""Uninstall the dcos cli subcommand
|
|
|
|
:param package_name: the name of the package
|
|
:type package_name: str
|
|
:returns: True if the subcommand was uninstalled
|
|
:rtype: bool
|
|
"""
|
|
|
|
pkg_dir = package_dir(package_name)
|
|
|
|
if os.path.isdir(pkg_dir):
|
|
shutil.rmtree(pkg_dir)
|
|
return True
|
|
|
|
return False
|
|
|
|
BIN_DIRECTORY = 'Scripts' if util.is_windows_platform() else 'bin'
|
|
|
|
|
|
# TODO (mgummelt): should be made private after "dcos subcommand" is removed
|
|
def install_with_pip(
|
|
package_name,
|
|
env_directory,
|
|
requirements):
|
|
"""
|
|
: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.errors.Error
|
|
"""
|
|
|
|
bin_directory = os.path.join(util.dcos_path(), BIN_DIRECTORY)
|
|
new_package_dir = not os.path.exists(env_directory)
|
|
|
|
pip_path = os.path.join(env_directory, BIN_DIRECTORY, 'pip')
|
|
if not os.path.exists(pip_path):
|
|
cmd = [os.path.join(bin_directory, 'virtualenv'), env_directory]
|
|
|
|
if _execute_command(cmd) != 0:
|
|
return _generic_error(package_name)
|
|
|
|
with util.temptext() as text_file:
|
|
fd, requirement_path = text_file
|
|
|
|
# Write the requirements to the file
|
|
with os.fdopen(fd, 'w') as requirements_file:
|
|
for line in requirements:
|
|
print(line, file=requirements_file)
|
|
|
|
cmd = [
|
|
os.path.join(env_directory, BIN_DIRECTORY, 'pip'),
|
|
'install',
|
|
'--requirement',
|
|
requirement_path,
|
|
]
|
|
|
|
if _execute_command(cmd) != 0:
|
|
# We should remove the diretory that we just created
|
|
if new_package_dir:
|
|
shutil.rmtree(env_directory)
|
|
|
|
return _generic_error(package_name)
|
|
|
|
return None
|
|
|
|
|
|
def _execute_command(command):
|
|
"""
|
|
:param command: the command to execute
|
|
:type command: list of str
|
|
:returns: the process return code
|
|
:rtype: int
|
|
"""
|
|
|
|
logger.info('Calling: %r', command)
|
|
|
|
process = subprocess.Popen(
|
|
command,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
stdout, stderr = process.communicate()
|
|
|
|
if process.returncode != 0:
|
|
logger.error("Install script's stdout: %s", stdout)
|
|
logger.error("Install script's stderr: %s", stderr)
|
|
else:
|
|
logger.info("Install script's stdout: %s", stdout)
|
|
logger.info("Install script's stderr: %s", stderr)
|
|
|
|
return process.returncode
|
|
|
|
|
|
def _generic_error(package_name):
|
|
"""
|
|
:param package: package name
|
|
:type: str
|
|
:returns: generic error when installing package
|
|
:rtype: dcos.errors.Error
|
|
"""
|
|
|
|
return errors.DefaultError(
|
|
'Error installing {!r} package'.format(package_name))
|
|
|
|
|
|
class InstalledSubcommand(object):
|
|
""" Represents an installed subcommand.
|
|
|
|
:param name: The name of the subcommand
|
|
:type name: str
|
|
"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def _dir(self):
|
|
"""
|
|
:returns: path to this subcommand's directory.
|
|
:rtype: (str, Error)
|
|
"""
|
|
|
|
return package_dir(self.name)
|
|
|
|
def package_version(self):
|
|
"""
|
|
:returns: this subcommand's version.
|
|
:rtype: (str, Error)
|
|
"""
|
|
|
|
version_path = os.path.join(self._dir(), 'version')
|
|
return util.read_file(version_path)
|
|
|
|
def package_source(self):
|
|
"""
|
|
:returns: this subcommand's source.
|
|
:rtype: (str, Error)
|
|
"""
|
|
|
|
source_path = os.path.join(self._dir(), 'source')
|
|
return util.read_file(source_path)
|
|
|
|
def package_json(self):
|
|
"""
|
|
:returns: contents of this subcommand's package.json file.
|
|
:rtype: (dict, Error)
|
|
"""
|
|
|
|
package_json_path = os.path.join(self._dir(), 'package.json')
|
|
with open(package_json_path) as package_json_file:
|
|
return util.load_json(package_json_file)
|