"""Deploy and manage applications on the DCOS Usage: dcos marathon --config-schema dcos marathon --info dcos marathon app add [] dcos marathon app list dcos marathon app remove [--force] dcos marathon app restart [--force] dcos marathon app show [--app-version=] dcos marathon app start [--force] [] dcos marathon app stop [--force] dcos marathon app update [--force] [...] dcos marathon app version list [--max-count=] dcos marathon deployment list [] dcos marathon deployment rollback dcos marathon deployment stop dcos marathon deployment watch [--max-count=] [--interval=] dcos marathon task list [] dcos marathon task show Options: -h, --help Show this screen --info Show a short description of this subcommand --version Show version --force This flag disable checks in Marathon during update operations --app-version= This flag specifies the application version to use for the command. The application version () can be specified as an absolute value or as relative value. Absolute version values must be in ISO8601 date format. Relative values must be specified as a negative integer and they represent the version from the currently deployed application definition --config-schema Show the configuration schema for the Marathon subcommand --max-count= Maximum number of entries to try to fetch and return --interval= Number of seconds to wait between actions Positional arguments: The application id The application resource; for a detailed description see (https://mesosphere.github.io/ marathon/docs/rest-api.html#post-/v2/apps) The deployment id The number of instances to start Optional key-value pairs to be included in the command. The separator between the key and value must be the '=' character. E.g. cpus=2.0 The task id """ import json import sys import time import dcoscli import docopt import pkg_resources from dcos import cmds, emitting, errors, jsonitem, marathon, options, 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-marathon 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 the supported commands :rtype: dcos.cmds.Command """ return [ cmds.Command( hierarchy=['marathon', 'version', 'list'], arg_keys=['', '--max-count'], function=_version_list), cmds.Command( hierarchy=['marathon', 'deployment', 'list'], arg_keys=[''], function=_deployment_list), cmds.Command( hierarchy=['marathon', 'deployment', 'rollback'], arg_keys=[''], function=_deployment_rollback), cmds.Command( hierarchy=['marathon', 'deployment', 'stop'], arg_keys=[''], function=_deployment_stop), cmds.Command( hierarchy=['marathon', 'deployment', 'watch'], arg_keys=['', '--max-count', '--interval'], function=_deployment_watch), cmds.Command( hierarchy=['marathon', 'task', 'list'], arg_keys=[''], function=_task_list), cmds.Command( hierarchy=['marathon', 'task', 'show'], arg_keys=[''], function=_task_show), cmds.Command( hierarchy=['marathon', 'app', 'add'], arg_keys=[''], function=_add), cmds.Command( hierarchy=['marathon', 'app', 'list'], arg_keys=[], function=_list), cmds.Command( hierarchy=['marathon', 'app', 'remove'], arg_keys=['', '--force'], function=_remove), cmds.Command( hierarchy=['marathon', 'app', 'show'], arg_keys=['', '--app-version'], function=_show), cmds.Command( hierarchy=['marathon', 'app', 'start'], arg_keys=['', '', '--force'], function=_start), cmds.Command( hierarchy=['marathon', 'app', 'stop'], arg_keys=['', '--force'], function=_stop), cmds.Command( hierarchy=['marathon', 'app', 'update'], arg_keys=['', '', '--force'], function=_update), cmds.Command( hierarchy=['marathon', 'app', 'restart'], arg_keys=['', '--force'], function=_restart), cmds.Command( hierarchy=['marathon'], arg_keys=['--config-schema', '--info'], function=_marathon), ] def _marathon(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/marathon.json').decode('utf-8')) emitter.publish(schema) elif info: _info() else: emitter.publish(options.make_generic_usage_message(__doc__)) return 1 return 0 def _info(): """ :returns: Process status :rtype: int """ emitter.publish(__doc__.split('\n')[0]) return 0 def _add(app_resource): """ :param app_resource: optional filename for the application resource :type app_resource: str :returns: Process status :rtype: int """ if app_resource is not None: with open(app_resource) as fd: application_resource, err = util.load_json(fd) else: # Check that stdin is not tty if sys.stdin.isatty(): # We don't support TTY right now. In the future we will start an # editor emitter.publish( "We currently don't support reading from the TTY. Please " "specify an application JSON.\n" "Usage: dcos app add < app_resource.json") return 1 application_resource, err = util.load_json(sys.stdin) if err is not None: emitter.publish(err) return 1 schema = json.loads( pkg_resources.resource_string( 'dcoscli', 'data/marathon-schema.json').decode('utf-8')) errs = util.validate_json(application_resource, schema) if len(errs) != 0: emitter.publish(util.list_to_err(errs)) return 1 # Add application to marathon client = marathon.create_client() # Check that the application doesn't exist app_id = client.normalize_app_id(application_resource['id']) app, err = client.get_app(app_id) if app is not None: emitter.publish("Application '{}' already exists".format(app_id)) return 1 _, err = client.add_app(application_resource) if err is not None: emitter.publish(err) return 1 return 0 def _list(): """ :returns: process status :rtype: int """ client = marathon.create_client() apps, err = client.get_apps() if err is not None: emitter.publish(err) return 1 emitter.publish(apps) return 0 def _remove(app_id, force): """ :param app_id: ID of the app to remove :type app_id: str :param force: Whether to override running deployments. :type force: bool :returns: Process status :rtype: int """ client = marathon.create_client() err = client.remove_app(app_id, force) if err is not None: emitter.publish(err) return 1 return 0 def _show(app_id, version): """Show details of a Marathon application. :param app_id: The id for the application :type app_id: str :param version: The version, either absolute (date-time) or relative :type version: str :returns: Process status :rtype: int """ client = marathon.create_client() if version is not None: version, err = _calculate_version(client, app_id, version) if err is not None: emitter.publish(err) return 1 app, err = client.get_app(app_id, version=version) if err is not None: emitter.publish(err) return 1 emitter.publish(app) return 0 def _start(app_id, instances, force): """Start a Marathon application. :param app_id: the id for the application :type app_id: str :param instances: the number of instances to start :type instances: str :param force: whether to override running deployments :type force: bool :returns: Process status :rtype: int """ # Check that the application exists client = marathon.create_client() desc, err = client.get_app(app_id) if err is not None: emitter.publish(err) return 1 if desc['instances'] > 0: emitter.publish( 'Application {!r} already started: {!r} instances.'.format( app_id, desc['instances'])) return 1 schema = json.loads( pkg_resources.resource_string( 'dcoscli', 'data/marathon-schema.json').decode('utf-8')) app_json = {} # Need to add the 'id' because it is required app_json['id'] = app_id # Set instances to 1 if not specified if instances is None: instances = 1 else: instances, err = util.parse_int(instances) if err is not None: emitter.publish(err) return 1 if instances <= 0: emitter.publish( 'The number of instances must be positive: {!r}.'.format( instances)) return 1 app_json['instances'] = instances errs = util.validate_json(app_json, schema) if len(errs) != 0: emitter.publish(util.list_to_err(errs)) return 1 deployment, err = client.update_app(app_id, app_json, force) if err is not None: emitter.publish(err) return 1 emitter.publish('Created deployment {}'.format(deployment)) return 0 def _stop(app_id, force): """Stop a Marathon application :param app_id: the id of the application :type app_id: str :param force: whether to override running deployments :type force: bool :returns: process status :rtype: int """ # Check that the application exists client = marathon.create_client() desc, err = client.get_app(app_id) if err is not None: emitter.publish(err) return 1 if desc['instances'] <= 0: emitter.publish( 'Application {!r} already stopped: {!r} instances.'.format( app_id, desc['instances'])) return 1 app_json = {'instances': 0} deployment, err = client.update_app(app_id, app_json, force) if err is not None: emitter.publish(err) return 1 emitter.publish('Created deployment {}'.format(deployment)) def _update(app_id, json_items, force): """ :param app_id: the id of the application :type app_id: str :param json_items: json update items :type json_items: list of str :param force: whether to override running deployments :type force: bool :returns: process status :rtype: int """ # Check that the application exists client = marathon.create_client() _, err = client.get_app(app_id) if err is not None: emitter.publish(err) return 1 if len(json_items) == 0: if sys.stdin.isatty(): # We don't support TTY right now. In the future we will start an # editor emitter.publish( "We currently don't support reading from the TTY. Please " "specify an application JSON.\n" "E.g. dcos app update < app_update.json") return 1 else: return _update_from_stdin(app_id, force) schema = json.loads( pkg_resources.resource_string( 'dcoscli', 'data/marathon-schema.json').decode('utf-8')) app_json = {} # Need to add the 'id' because it is required app_json['id'] = app_id for json_item in json_items: key_value, err = jsonitem.parse_json_item(json_item, schema) if err is not None: emitter.publish(err) return 1 key, value = key_value if key in app_json: emitter.publish( 'Key {!r} was specified more than once'.format(key)) return 1 else: app_json[key] = value errs = util.validate_json(app_json, schema) if len(errs) != 0: emitter.publish(util.list_to_err(errs)) return 1 deployment, err = client.update_app(app_id, app_json, force) if err is not None: emitter.publish(err) return 1 emitter.publish('Created deployment {}'.format(deployment)) return 0 def _restart(app_id, force): """ :param app_id: the id of the application :type app_id: str :param force: whether to override running deployments :type force: bool :returns: process status :rtype: int """ client = marathon.create_client() desc, err = client.get_app(app_id) if err is not None: emitter.publish(err) return 1 if desc['instances'] <= 0: app_id = client.normalize_app_id(app_id) emitter.publish( 'Unable to perform rolling restart of application {!r} ' 'because it has no running tasks'.format( app_id, desc['instances'])) return 1 payload, err = client.restart_app(app_id, force) if err is not None: emitter.publish(err) return 1 emitter.publish('Created deployment {}'.format(payload['deploymentId'])) return 0 def _version_list(app_id, max_count): """ :param app_id: the id of the application :type app_id: str :param max_count: the maximum number of version to fetch and return :type max_count: str :returns: process status :rtype: int """ if max_count is not None: max_count, err = util.parse_int(max_count) if err is not None: emitter.publish(err) return 1 client = marathon.create_client() versions, err = client.get_app_versions(app_id, max_count) if err is not None: emitter.publish(err) return 1 emitter.publish(versions) return 0 def _deployment_list(app_id): """ :param app_id: the application id :type app_id: str :returns: process status :rtype: int """ client = marathon.create_client() deployments, err = client.get_deployments(app_id) if err is not None: emitter.publish(err) return 1 emitter.publish(deployments) return 0 def _deployment_stop(deployment_id): """ :param deployment_id: the application id :type deployment_di: str :returns: process status :rtype: int """ client = marathon.create_client() err = client.stop_deployment(deployment_id) if err is not None: emitter.publish(err) return 1 return 0 def _deployment_rollback(deployment_id): """ :param deployment_id: the application id :type deployment_di: str :returns: process status :rtype: int """ client = marathon.create_client() deployment, err = client.rollback_deployment(deployment_id) if err is not None: emitter.publish(err) return 1 emitter.publish(deployment) return 0 def _deployment_watch(deployment_id, max_count, interval): """ :param deployment_id: the application id :type deployment_di: str :param max_count: maximum number of polling calls :type max_count: str :param interval: wait interval in seconds between polling calls :type interval: str :returns: process status :rtype: int """ if max_count is not None: max_count, err = util.parse_int(max_count) if err is not None: emitter.publish(err) return 1 if interval is not None: interval, err = util.parse_int(interval) if err is not None: emitter.publish(err) return 1 else: interval = 1 client = marathon.create_client() count = 0 while max_count is None or count < max_count: deployment, err = client.get_deployment(deployment_id) if err is not None: emitter.publish(err) return 1 if deployment is None: return 0 emitter.publish(deployment) time.sleep(interval) count += 1 return 0 def _task_list(app_id): """ :param app_id: the id of the application :type app_id: str :returns: process status :rtype: int """ client = marathon.create_client() tasks, err = client.get_tasks(app_id) if err is not None: emitter.publish(err) return 1 emitter.publish(tasks) return 0 def _task_show(task_id): """ :param task_id: the task id :type task_id: str :returns: process status :rtype: int """ client = marathon.create_client() task, err = client.get_task(task_id) if err is not None: emitter.publish(err) return 1 if task is None: emitter.publish( errors.DefaultError("Task '{}' does not exist".format(task_id))) return 1 emitter.publish(task) return 0 def _update_from_stdin(app_id, force): """ :param app_id: the id of the application :type app_id: str :param force: whether to override running deployments :type force: bool :returns: process status :rtype: int """ logger.info('Updating %r from JSON object from stdin', app_id) application_resource, err = util.load_jsons(sys.stdin.read()) if err is not None: emitter.publish(err) return 1 # Add application to marathon client = marathon.create_client() _, err = client.update_app(app_id, application_resource, force) if err is not None: emitter.publish(err) return 1 return 0 def _calculate_version(client, app_id, version): """ :param client: Marathon client :type client: dcos.marathon.Client :param app_id: The ID of the application :type app_id: str :param version: Relative or absolute version or None :type version: str :returns: The absolute version as an ISO8601 date-time; Error otherwise :rtype: (str, Error) """ # First let's try to parse it as a negative integer value, err = util.parse_int(version) if err is None and value < 0: value = -1 * value # We have a negative value let's ask Marathon for the last abs(value) versions, err = client.get_app_versions(app_id, value + 1) if err is not None: return (None, err) if len(versions) <= value: # We don't have enough versions. Return an error. msg = "Application {!r} only has {!r} version(s)." return ( None, errors.DefaultError(msg.format(app_id, len(versions), value)) ) else: return (versions[value], None) elif err is None: return ( None, errors.DefaultError( 'Relative versions must be negative: {}'.format(version)) ) else: # Let's assume that we have an absolute version return (version, None)