Merge pull request #10 from mesosphere/more-marathon

More marathon
This commit is contained in:
José Armando García Sancio
2015-01-26 16:57:58 -08:00
10 changed files with 328 additions and 35 deletions

View File

@@ -1,6 +1,8 @@
import requests
import json
import urllib
from . import errors
import requests
from dcos.api import errors
class Client(object):
@@ -19,22 +21,87 @@ class Client(object):
self._host = host
self._port = port
def _create_url(self, path):
def _create_url(self, path, query_params=None):
"""Creates the url from the provided path
:param path: Url path
:type path: str
:return: Constructed url
:param query_params: Query string parameters
:type query_params: dict
:returns: Constructed url
:rtype: str
"""
return self._url_pattern.format(
url = self._url_pattern.format(
host=self._host,
port=self._port,
path=path)
if query_params is not None:
query_string = urllib.urlencode(query_params)
url = (url + '?{}').format(query_string)
return url
def _sanitize_app_id(self, app_id):
"""
:param app_id: Raw application ID
:type app_id: str
:returns: Sanitized application ID
:rtype: str
"""
# Add a leading '/' if necessary.
if not app_id.startswith('/'):
app_id = '/' + app_id
return app_id
def _response_to_error(self, response):
"""
:param response: HTTP resonse object
:type response: requests.Response
:returns: The error embedded in the response JSON
:rtype: Error
"""
return Error('Error: {}'.format(response.json()['message']))
def get_app(self, app_id):
"""Returns a representation of the requested application.
:param app_id: The ID of the application.
:type app_id: str
:returns: The requested Marathon application
:rtype: (dict, Error)
"""
app_id = self._sanitize_app_id(app_id)
url = self._create_url('v2/apps' + app_id)
response = requests.get(url)
if response.status_code == 200:
app = response.json()['app']
return (app, None)
else:
return (None, self._response_to_error(response))
def get_apps(self):
"""Get a list of known applications.
:returns: List of known applications.
:rtype: (list of dict, Error)
"""
url = self._create_url('v2/apps')
response = requests.get(url)
if response.status_code == 200:
apps = response.json()['apps']
return (apps, None)
else:
return (None, self._response_to_error(response))
def start_app(self, app_resource):
"""Create and start a new application
"""Create and start a new application.
:param app_resource: Application resource
:type app_resource: dict, bytes, or file
@@ -48,11 +115,77 @@ class Client(object):
if response.status_code == 201:
return (True, None)
else:
return (
None,
Error(
'Error talking to Marathon: {}'.format(
response.json()['message'])))
return (None, self._response_to_error(response))
def scale_app(self, app_id, instances, force=None):
"""Scales an application to the requested number of instances.
:param app_id: The ID of the application to scale.
:type app_id: str
:param instances: The requested number of instances.
:type instances: int
:param force: Whether to override running deployments.
:type force: bool
:returns: The resulting deployment ID.
:rtype: (bool, Error)
"""
if force is None:
force = False
app_id = self._sanitize_app_id(app_id)
params = None
if force:
params = {'force': True}
url = self._create_url('v2/apps{}'.format(app_id), params)
scale_json = json.loads('{{ "instances": {} }}'.format(int(instances)))
response = requests.put(url, json=scale_json)
if response.status_code == 200:
deployment = response.json()['deploymentId']
return (deployment, None)
else:
return (None, self._response_to_error(response))
def suspend_app(self, app_id, force=None):
"""Scales an application to zero instances.
:param app_id: The ID of the application to suspend.
:type app_id: str
:param force: Whether to override running deployments.
:type force: bool
:returns: The resulting deployment ID.
:rtype: (bool, Error)
"""
return self.scale_app(app_id, 0, force)
def remove_app(self, app_id, force=None):
"""Completely removes the requested application.
:param app_id: The ID of the application to suspend.
:type app_id: str
:param force: Whether to override running deployments.
:type force: bool
:returns: Status of trying to remove the application.
:rtype: (bool, Error)
"""
if force is None:
force = False
app_id = self._sanitize_app_id(app_id)
params = None
if force:
params = {'force': True}
url = self._create_url('v2/apps{}'.format(app_id), params)
response = requests.delete(url)
if response.status_code == 200:
return (True, None)
else:
return (None, self._response_to_error(response))
class Error(errors.Error):

View File

@@ -14,8 +14,7 @@ import os
import docopt
import toml
from ...api import config, constants
from dcos.api import config, constants
def main():
@@ -34,7 +33,7 @@ def main():
elif args['config'] and args['<value>'] is None:
toml_config = config.Toml.load_from_path(config_path)
print(config[args['<name>']])
print(toml_config[args['<name>']])
elif args['config']:
toml_config = config.Toml.load_from_path(config_path)

View File

@@ -15,8 +15,7 @@ import os
import subprocess
import docopt
from ...api import constants, options
from dcos.api import constants, options
def main():

View File

@@ -18,8 +18,7 @@ import os
import subprocess
import docopt
from ..api import constants
from dcos.api import constants
def main():

View File

@@ -1,7 +1,12 @@
"""
Usage:
dcos marathon info
dcos marathon list
dcos marathon describe <app_id>
dcos marathon start <app_resource>
dcos marathon scale <app_id> <instances> [--force]
dcos marathon suspend <app_id> [--force]
dcos marathon remove <app_id> [--force]
dcos marathon --help
dcos marathon --version
@@ -10,11 +15,11 @@ Options:
--version Show version
"""
import json
import os
import docopt
from ...api import config, constants, marathon, options
from dcos.api import config, constants, marathon, options
def main():
@@ -25,22 +30,99 @@ def main():
if args['marathon'] and args['info']:
return _info()
elif args['marathon'] and args['list']:
toml_config = config.Toml.load_from_path(config_path)
return _list(toml_config)
elif args['marathon'] and args['describe']:
toml_config = config.Toml.load_from_path(config_path)
return _describe(args['<app_id>'], toml_config)
elif args['marathon'] and args['start']:
toml_config = config.Toml.load_from_path(config_path)
return _start(args['<app_resource>'], toml_config)
elif args['marathon'] and args['scale']:
toml_config = config.Toml.load_from_path(config_path)
return _scale(args['<app_id>'],
args['<instances>'],
args['--force'],
toml_config)
elif args['marathon'] and args['suspend']:
toml_config = config.Toml.load_from_path(config_path)
return _suspend(args['<app_id>'], args['--force'], toml_config)
elif args['marathon'] and args['remove']:
toml_config = config.Toml.load_from_path(config_path)
return _remove(args['<app_id>'], args['--force'], toml_config)
else:
print(options.make_generic_usage_error(__doc__))
return 1
def _info():
"""Print marathon cli information
"""Print marathon cli information.
:returns: Process status
:rtype: int
"""
print('Deploy and manage containers for Mesos')
print('Deploy and manage applications on Apache Mesos')
return 0
def _create_client(config):
"""Creates a Marathon client with the supplied configuration.
:param config: Configuration dictionary
:type config: config.Toml
:returns: Marathon client
:rtype: dcos.api.marathon.Client
"""
return marathon.Client(config['marathon.host'], config['marathon.port'])
def _list(config):
"""Lists known Marathon applications.
:param config: Configuration dictionary
:type config: config.Toml
:returns: Process status
:rtype: int
"""
client = _create_client(config)
apps, err = client.get_apps()
if err is not None:
print(err.error())
return 1
if not apps:
print("No apps to list.")
for app in apps:
print(app['id'])
return 0
def _describe(app_id, config):
"""Show details of a Marathon applications.
:param app_id: ID of the app to suspend
:type app_id: str
:param config: Configuration dictionary
:type config: config.Toml
:returns: Process status
:rtype: int
"""
client = _create_client(config)
app, err = client.get_app(app_id)
if err is not None:
print(err.error())
return 1
print(json.dumps(app,
sort_keys=True,
indent=2))
return 0
@@ -54,7 +136,7 @@ def _start(app_resource_path, config):
:returns: Process status
:rtype: int
"""
client = marathon.Client(config['marathon.host'], config['marathon.port'])
client = _create_client(config)
with open(app_resource_path) as app_resource_file:
success, err = client.start_app(app_resource_file)
@@ -63,3 +145,75 @@ def _start(app_resource_path, config):
return 1
return 0
def _scale(app_id, instances, force, config):
"""Suspends a running Marathon application.
:param app_id: ID of the app to suspend
:type app_id: str
:param instances: The requested number of instances.
:type instances: int
:param force: Whether to override running deployments.
:type force: bool
:param config: Configuration dictionary
:type config: config.Toml
:returns: Process status
:rtype: int
"""
client = _create_client(config)
deployment, err = client.scale_app(app_id, instances, force)
if err is not None:
print(err.error())
return 1
print('Created deployment {}'.format(deployment))
return 0
def _suspend(app_id, force, config):
"""Suspends a running Marathon application.
:param app_id: ID of the app to suspend
:type app_id: str
:param force: Whether to override running deployments.
:type force: bool
:param config: Configuration dictionary
:type config: config.Toml
:returns: Process status
:rtype: int
"""
client = _create_client(config)
deployment, err = client.suspend_app(app_id, force)
if err is not None:
print(err.error())
return 1
print('Created deployment {}'.format(deployment))
return 0
def _remove(app_id, force, config):
"""Remove a Marathon application.
:param app_id: ID of the app to remove
:type app_id: str
:param force: Whether to override running deployments.
:type force: bool
:param config: Configuration dictionary
:type config: config.Toml
:returns: Process status
:rtype: int
"""
client = _create_client(config)
success, err = client.remove_app(app_id, force)
if err is not None:
print(err.error())
return 1
return 0

View File

@@ -11,8 +11,7 @@ Options:
import subprocess
import docopt
from ...api import constants
from dcos.api import constants
def main():
@@ -21,7 +20,7 @@ def main():
version='dcos-subcommand version {}'.format(constants.version))
if args['subcommand'] and args['info']:
print('Manage DCOS external commands')
print('Manage external DCOS commands')
elif args['subcommand'] and args['install'] and args['python']:
print('Trying to install a python subcommand')
command = ['pip', 'install', args['<uri>']]

View File

@@ -71,7 +71,13 @@ setup(
# project is installed. For an analysis of "install_requires" vs pip's
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=['docopt', 'toml', 'requests'],
install_requires=[
'docopt',
'jsonschema',
'pystache',
'requests',
'toml',
],
# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax, for

View File

@@ -0,0 +1,11 @@
{
"id": "test-app",
"cmd": "sleep 1000",
"cpus": 0.1,
"mem": 16,
"instances": 1,
"labels": {
"PACKAGE_ID": "test-app",
"PACKAGE_VERSION": "1.2.3"
}
}

View File

@@ -1,7 +0,0 @@
{
"cmd": "sleep 1000",
"cpus": 0.25,
"id": "sleeping-app",
"instances": 1,
"mem": 50
}

View File

@@ -10,5 +10,5 @@ deps =
commands =
flake8 --verbose dcos tests
isort -rc -c -vb {envsitepackagesdir}/dcos
isort --recursive --check-only --diff --verbose {envsitepackagesdir}/dcos
py.test --cov {envsitepackagesdir}/dcos tests