Add support for extracting default from embedded JSON object, and merging of embedded dict within options.
453 lines
11 KiB
Python
453 lines
11 KiB
Python
"""Install and manage DCOS software packages
|
|
|
|
Usage:
|
|
dcos package --config-schema
|
|
dcos package --info
|
|
dcos package describe <package_name>
|
|
dcos package info
|
|
dcos package install [--options=<file> --app-id=<app_id> --cli --app]
|
|
<package_name>
|
|
dcos package list-installed [--endpoints --app-id=<app-id> <package_name>]
|
|
dcos package search <query>
|
|
dcos package sources
|
|
dcos package uninstall [--all | --app-id=<app-id>] <package_name>
|
|
dcos package update [--validate]
|
|
|
|
Options:
|
|
-h, --help Show this screen
|
|
--info Show a short description of this subcommand
|
|
--version Show version
|
|
--all Apply the operation to all matching packages
|
|
--app Apply the operation only to the package's application
|
|
--app-id=<app-id> The application id
|
|
--cli Apply the operation only to the package's CLI
|
|
--options=<file> Path to a JSON file containing package installation
|
|
options
|
|
--validate Validate package content when updating sources
|
|
|
|
Configuration:
|
|
[package]
|
|
# Path to the local package cache.
|
|
cache_dir = "/var/dcos/cache"
|
|
|
|
# List of package sources, in search order.
|
|
#
|
|
# Three protocols are supported:
|
|
# - Local file
|
|
# - HTTPS
|
|
# - Git
|
|
sources = [
|
|
"file:///Users/me/test-registry",
|
|
"https://my.org/registry",
|
|
"git://github.com/mesosphere/universe.git"
|
|
]
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
import dcoscli
|
|
import docopt
|
|
import pkg_resources
|
|
from dcos.api import (cmds, config, constants, emitting, errors, marathon,
|
|
options, package, subcommand, util)
|
|
|
|
logger = util.get_logger(__name__)
|
|
|
|
emitter = emitting.FlatEmitter()
|
|
|
|
|
|
def main():
|
|
err = util.configure_logger_from_environ()
|
|
if err is not None:
|
|
emitter.publish(err)
|
|
return 1
|
|
|
|
args = docopt.docopt(
|
|
__doc__,
|
|
version='dcos-package version {}'.format(dcoscli.version))
|
|
|
|
returncode, err = cmds.execute(_cmds(), args)
|
|
if err is not None:
|
|
emitter.publish(err)
|
|
emitter.publish(options.make_generic_usage_message(__doc__))
|
|
return 1
|
|
|
|
return returncode
|
|
|
|
|
|
def _cmds():
|
|
"""
|
|
:returns: All of the supported commands
|
|
:rtype: dcos.api.cmds.Command
|
|
"""
|
|
|
|
return [
|
|
cmds.Command(
|
|
hierarchy=['package', 'sources'],
|
|
arg_keys=[],
|
|
function=_list_sources),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package', 'update'],
|
|
arg_keys=['--validate'],
|
|
function=_update),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package', 'describe'],
|
|
arg_keys=['<package_name>'],
|
|
function=_describe),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package', 'install'],
|
|
arg_keys=['<package_name>', '--options', '--app-id', '--cli',
|
|
'--app'],
|
|
function=_install),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package', 'list-installed'],
|
|
arg_keys=['--endpoints', '--app-id', '<package_name>'],
|
|
function=_list),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package', 'search'],
|
|
arg_keys=['<query>'],
|
|
function=_search),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package', 'uninstall'],
|
|
arg_keys=['<package_name>', '--all', '--app-id'],
|
|
function=_uninstall),
|
|
|
|
cmds.Command(
|
|
hierarchy=['package'],
|
|
arg_keys=['--config-schema', '--info'],
|
|
function=_package),
|
|
]
|
|
|
|
|
|
def _package(config_schema, info):
|
|
"""
|
|
:param config_schema: Whether to output the config schema
|
|
:type config_schema: boolean
|
|
:param info: Whether to output a description of this subcommand
|
|
:type info: boolean
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
if config_schema:
|
|
schema = json.loads(
|
|
pkg_resources.resource_string(
|
|
'dcoscli',
|
|
'data/config-schema/package.json').decode('utf-8'))
|
|
emitter.publish(schema)
|
|
elif info:
|
|
_info()
|
|
else:
|
|
emitter.publish(options.make_generic_usage_message(__doc__))
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
def _load_config():
|
|
"""
|
|
:returns: Configuration object
|
|
:rtype: Toml
|
|
"""
|
|
return config.load_from_path(
|
|
os.environ[constants.DCOS_CONFIG_ENV])
|
|
|
|
|
|
def _info():
|
|
"""Print package cli information.
|
|
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
emitter.publish(__doc__.split('\n')[0])
|
|
return 0
|
|
|
|
|
|
def _list_sources():
|
|
"""List configured package sources.
|
|
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
config = _load_config()
|
|
|
|
sources, errs = package.list_sources(config)
|
|
|
|
if len(errs) > 0:
|
|
for err in errs:
|
|
emitter.publish(err)
|
|
return 1
|
|
|
|
for source in sources:
|
|
emitter.publish("{} {}".format(source.hash(), source.url))
|
|
|
|
return 0
|
|
|
|
|
|
def _update(validate):
|
|
"""Update local package definitions from sources.
|
|
|
|
:param validate: Whether to validate package content when updating sources.
|
|
:type validate: bool
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
config = _load_config()
|
|
|
|
errs = package.update_sources(config, validate)
|
|
|
|
if len(errs) > 0:
|
|
for err in errs:
|
|
emitter.publish(err)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
def _describe(package_name):
|
|
"""Describe the specified package.
|
|
|
|
:param package_name: The package to describe
|
|
:type package_name: str
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
config = _load_config()
|
|
|
|
pkg = package.resolve_package(package_name, config)
|
|
|
|
if pkg is None:
|
|
emitter.publish("Package [{}] not found".format(package_name))
|
|
return 1
|
|
|
|
# TODO(CD): Make package version to describe configurable
|
|
pkg_version, version_error = pkg.latest_version()
|
|
if version_error is not None:
|
|
emitter.publish(version_error)
|
|
return 1
|
|
|
|
pkg_json, pkg_error = pkg.package_json(pkg_version)
|
|
|
|
if pkg_error is not None:
|
|
emitter.publish(pkg_error)
|
|
return 1
|
|
|
|
version_map, version_error = pkg.software_versions()
|
|
|
|
if version_error is not None:
|
|
emitter.publish(version_error)
|
|
return 1
|
|
|
|
versions = [version_map[pkg_ver] for pkg_ver in version_map]
|
|
|
|
del pkg_json['version']
|
|
pkg_json['versions'] = versions
|
|
emitter.publish(pkg_json)
|
|
|
|
return 0
|
|
|
|
|
|
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_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
|
|
:type cli: bool
|
|
:param app: indicate if the application should be installed
|
|
:type app: bool
|
|
:returns: process status
|
|
:rtype: int
|
|
"""
|
|
|
|
if cli is False and app is False:
|
|
# Install both if neither flag is specified
|
|
cli = True
|
|
app = True
|
|
|
|
config = _load_config()
|
|
|
|
pkg = package.resolve_package(package_name, config)
|
|
if pkg is None:
|
|
emitter.publish(
|
|
errors.DefaultError(
|
|
"Package [{}] not found".format(package_name)))
|
|
emitter.publish(
|
|
errors.DefaultError(
|
|
"You may need to run 'dcos package update' to update your "
|
|
"repositories"))
|
|
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:
|
|
user_options = {}
|
|
else:
|
|
with open(options_path) as options_file:
|
|
user_options, err = util.load_json(options_file)
|
|
if err is not None:
|
|
emitter.publish(err)
|
|
return 1
|
|
|
|
try:
|
|
options, err = pkg.options(pkg_version, user_options)
|
|
if err is not None:
|
|
emitter.publish(err)
|
|
return 1
|
|
except Exception as e:
|
|
logger.exception('Exception while generating options')
|
|
emitter.publish(errors.DefaultError(e.message))
|
|
return 1
|
|
|
|
if app:
|
|
# Install in Marathon
|
|
version_map, version_error = pkg.software_versions()
|
|
|
|
if version_error is not None:
|
|
emitter.publish(version_error)
|
|
return 1
|
|
|
|
sw_version = version_map.get(pkg_version, '?')
|
|
|
|
message = 'Installing package [{}] version [{}]'.format(
|
|
pkg.name(), sw_version)
|
|
if app_id is not None:
|
|
message += ' with app id [{}]'.format(app_id)
|
|
|
|
emitter.publish(message)
|
|
|
|
init_client = marathon.create_client(config)
|
|
|
|
install_error = package.install_app(
|
|
pkg,
|
|
pkg_version,
|
|
init_client,
|
|
options,
|
|
app_id)
|
|
|
|
if install_error is not None:
|
|
emitter.publish(install_error)
|
|
return 1
|
|
|
|
if cli and pkg.is_command_defined(pkg_version):
|
|
# Install subcommand
|
|
emitter.publish('Installing CLI subcommand for package [{}]'.format(
|
|
pkg.name()))
|
|
|
|
err = subcommand.install(pkg, pkg_version, options)
|
|
if err is not None:
|
|
emitter.publish(err)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
def _list(endpoints, app_id, package_name):
|
|
"""Show installed apps
|
|
|
|
:param endpoints: Whether to include a list of
|
|
endpoints as port-host pairs
|
|
:type endpoints: boolean
|
|
:param package_name: The package to show
|
|
:type package_name: str
|
|
:param app_id: App ID of app to show
|
|
:type app_id: str
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
config = _load_config()
|
|
|
|
init_client = marathon.create_client(config)
|
|
|
|
installed, error = package.installed_packages(init_client, endpoints)
|
|
if error is not None:
|
|
emitter.publish(error)
|
|
return 1
|
|
|
|
results = []
|
|
for pkg in installed:
|
|
if not ((package_name and pkg.name() != package_name) or
|
|
(app_id and pkg.app and pkg.app['appId'] != app_id)):
|
|
result, err = pkg.dict()
|
|
if err is not None:
|
|
emitter.publish(err)
|
|
return 1
|
|
|
|
results.append(result)
|
|
|
|
emitter.publish(results)
|
|
|
|
return 0
|
|
|
|
|
|
def _search(query):
|
|
"""Search for matching packages.
|
|
|
|
:param query: The search term
|
|
:type query: str
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
config = _load_config()
|
|
|
|
results, error = package.search(query, config)
|
|
|
|
if error is not None:
|
|
emitter.publish(error)
|
|
return 1
|
|
|
|
emitter.publish([r.as_dict() for r in results])
|
|
|
|
return 0
|
|
|
|
|
|
def _uninstall(package_name, remove_all, app_id):
|
|
"""Uninstall the specified package.
|
|
|
|
:param package_name: The package to uninstall
|
|
:type package_name: str
|
|
:param remove_all: Whether to remove all instances of the named package
|
|
:type remove_all: boolean
|
|
:param app_id: App ID of the package instance to uninstall
|
|
:type app_id: str
|
|
:returns: Process status
|
|
:rtype: int
|
|
"""
|
|
|
|
config = _load_config()
|
|
|
|
init_client = marathon.create_client(config)
|
|
|
|
uninstall_error = package.uninstall(
|
|
package_name,
|
|
remove_all,
|
|
app_id,
|
|
init_client)
|
|
|
|
if uninstall_error is not None:
|
|
emitter.publish(uninstall_error)
|
|
return 1
|
|
|
|
return 0
|