dcos-1254 Shutdown framework after uninstall
This commit is contained in:
17
README.rst
17
README.rst
@@ -72,18 +72,6 @@ environments.
|
||||
If you're using OS X, be sure to use the officially distributed Python 3.4
|
||||
installer_ since the Homebrew version is missing a necessary library.
|
||||
|
||||
To support subcommand integration tests, you'll need to clone, package and
|
||||
configure your environment to point to the packaged `dcos-helloworld` account.
|
||||
|
||||
#. Check out the dcos-helloworld_ project
|
||||
|
||||
#. :code:`cd dcos-helloworld`
|
||||
|
||||
#. :code:`make packages`
|
||||
|
||||
#. Set the :code:`DCOS_TEST_WHEEL` environment variable to the path of the created
|
||||
wheel package: :code:`export DCOS_TEST_WHEEL=$(pwd)/dist/dcos_helloworld-0.1.0-py2.py3-none-any.whl`
|
||||
|
||||
Running
|
||||
#######
|
||||
|
||||
@@ -97,6 +85,11 @@ instance running on localhost, set :code:`DCOS_CONFIG` as follows::
|
||||
|
||||
export DCOS_CONFIG=$(pwd)/tests/data/dcos.toml
|
||||
|
||||
If you are testing against the DCOS Image you can configure the URL to the
|
||||
Exhibitor::
|
||||
|
||||
export EXHIBITOR=http://<hostname>:8181/
|
||||
|
||||
There are two ways to run tests, you can either use the virtualenv created by
|
||||
:code:`make env` above::
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Options:
|
||||
and return
|
||||
--interval=<interval> Number of seconds to wait between actions
|
||||
|
||||
Positional arguments:
|
||||
Positional Arguments:
|
||||
<app-id> The application id
|
||||
<app-resource> The application resource; for a detailed
|
||||
description see (https://mesosphere.github.io/
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Usage:
|
||||
dcos service --info
|
||||
dcos service [--inactive --json]
|
||||
dcos service shutdown <service-id>
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
@@ -16,6 +17,9 @@ Options:
|
||||
master, but haven't yet reached their failover timeout.
|
||||
|
||||
--version Show version
|
||||
|
||||
Positional Arguments:
|
||||
<service-id> The ID for the DCOS Service
|
||||
"""
|
||||
|
||||
|
||||
@@ -57,6 +61,11 @@ def _cmds():
|
||||
"""
|
||||
|
||||
return [
|
||||
cmds.Command(
|
||||
hierarchy=['service', 'shutdown'],
|
||||
arg_keys=['<service-id>'],
|
||||
function=_shutdown),
|
||||
|
||||
cmds.Command(
|
||||
hierarchy=['service', '--info'],
|
||||
arg_keys=[],
|
||||
@@ -132,8 +141,7 @@ def _service(inactive, is_json):
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
master = mesos.get_master()
|
||||
services = master.frameworks(inactive=inactive)
|
||||
services = mesos.get_master().frameworks(inactive=inactive)
|
||||
|
||||
if is_json:
|
||||
emitter.publish([service.dict() for service in services])
|
||||
@@ -144,3 +152,16 @@ def _service(inactive, is_json):
|
||||
emitter.publish(output)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _shutdown(service_id):
|
||||
"""Shuts down a service
|
||||
|
||||
:param service_id: the id for the service
|
||||
:type service_id: str
|
||||
:returns: process return code
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
mesos.get_master_client().shutdown_framework(service_id)
|
||||
return 0
|
||||
|
||||
@@ -120,8 +120,8 @@ def _task(fltr, completed, is_json):
|
||||
:type fltr: str
|
||||
:param completed: If True, include completed tasks
|
||||
:type completed: bool
|
||||
:param is_json: If true, output json.
|
||||
Otherwise, output a human readable table.
|
||||
:param is_json: If True, output json. Otherwise, output a human readable
|
||||
table.
|
||||
:type is_json: bool
|
||||
:returns: process return code
|
||||
"""
|
||||
@@ -129,8 +129,7 @@ def _task(fltr, completed, is_json):
|
||||
if fltr is None:
|
||||
fltr = ""
|
||||
|
||||
master = mesos.get_master()
|
||||
tasks = sorted(master.tasks(completed=completed, fltr=fltr),
|
||||
tasks = sorted(mesos.get_master().tasks(completed=completed, fltr=fltr),
|
||||
key=lambda task: task['name'])
|
||||
|
||||
if is_json:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[core]
|
||||
email = "test@mail.com"
|
||||
reporting = true
|
||||
email = "test@mail.com"
|
||||
|
||||
7
cli/tests/data/package/chronos-1.json
Normal file
7
cli/tests/data/package/chronos-1.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"chronos": {
|
||||
"id": "chronos-user-1",
|
||||
"framework-name": "chronos-user",
|
||||
"zk-path": "/universe/chronos-user-1"
|
||||
}
|
||||
}
|
||||
7
cli/tests/data/package/chronos-2.json
Normal file
7
cli/tests/data/package/chronos-2.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"chronos": {
|
||||
"id": "chronos-user-2",
|
||||
"framework-name": "chronos-user",
|
||||
"zk-path": "/universe/chronos-user-2"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
def exec_command(cmd, env=None, stdin=None):
|
||||
"""Execute CLI command
|
||||
@@ -148,3 +154,58 @@ def list_deployments(expected_count=None, app_id=None):
|
||||
assert stderr == b''
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_services(expected_count=None, args=[]):
|
||||
"""Get services
|
||||
|
||||
:param expected_count: assert exactly this number of services are
|
||||
running
|
||||
:type expected_count: int | None
|
||||
:param args: cli arguments
|
||||
:type args: [str]
|
||||
:returns: services
|
||||
:rtype: [dict]
|
||||
"""
|
||||
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'service', '--json'] + args)
|
||||
|
||||
assert returncode == 0
|
||||
assert stderr == b''
|
||||
|
||||
services = json.loads(stdout.decode('utf-8'))
|
||||
assert isinstance(services, collections.Sequence)
|
||||
if expected_count is not None:
|
||||
assert len(services) == expected_count
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def service_shutdown(service_id):
|
||||
"""Shuts down a service using the command line program
|
||||
|
||||
:param service_id: the id of the service
|
||||
:type: service_id: str
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
assert_command(['dcos', 'service', 'shutdown', service_id])
|
||||
|
||||
|
||||
def delete_zk_nodes():
|
||||
"""Delete Zookeeper nodes that were created during the tests
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
|
||||
base_url = os.environ.get('EXHIBITOR_URL')
|
||||
if base_url:
|
||||
base_path = 'exhibitor/v1/explorer/znode/{}'
|
||||
|
||||
for znode in ['universe', 'cassandra-mesos', 'chronos']:
|
||||
znode_url = urllib.parse.urljoin(
|
||||
base_url,
|
||||
base_path.format(znode))
|
||||
|
||||
requests.delete(znode_url)
|
||||
|
||||
@@ -68,7 +68,7 @@ Options:
|
||||
and return
|
||||
--interval=<interval> Number of seconds to wait between actions
|
||||
|
||||
Positional arguments:
|
||||
Positional Arguments:
|
||||
<app-id> The application id
|
||||
<app-resource> The application resource; for a detailed
|
||||
description see (https://mesosphere.github.io/
|
||||
|
||||
@@ -4,7 +4,15 @@ import os
|
||||
import six
|
||||
from dcos import subcommand
|
||||
|
||||
from common import assert_command, exec_command
|
||||
import pytest
|
||||
from common import (assert_command, delete_zk_nodes, exec_command,
|
||||
get_services, service_shutdown, watch_all_deployments)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def zk_znode(request):
|
||||
request.addfinalizer(delete_zk_nodes)
|
||||
return request
|
||||
|
||||
|
||||
def test_package():
|
||||
@@ -120,8 +128,23 @@ icon-service-marathon-medium.png",
|
||||
"icon-small": "https://downloads.mesosphere.io/marathon/assets/\
|
||||
icon-service-marathon-small.png"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"name": "Apache License Version 2.0",
|
||||
"url": "https://github.com/mesosphere/marathon/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"maintainer": "support@mesosphere.io",
|
||||
"name": "marathon",
|
||||
"postInstallNotes": "Marathon DCOS Service has been successfully \
|
||||
installed!\\n\\n\\tDocumentation: https://\
|
||||
mesosphere.github.io/marathon\\n\\tIssues: https:/github.com/mesosphere/\
|
||||
marathon/issues\\n",
|
||||
"postUninstallNotes": "The Marathon DCOS Service has been uninstalled and \
|
||||
will no longer run.\\nPlease follow the instructions at http://beta-docs.\
|
||||
mesosphere.com/services/marathon/#uninstall to clean up any persisted state",
|
||||
"preInstallNotes": "We recommend a minimum of one node with at least 2 \
|
||||
CPU's and 1GB of RAM available for the Marathon Service.",
|
||||
"scm": "https://github.com/mesosphere/marathon.git",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
@@ -183,7 +206,10 @@ marathon-user --http_port $PORT0 ",
|
||||
},
|
||||
"type": "DOCKER"
|
||||
},
|
||||
"cpus": 1.0,
|
||||
"cpus": 2.0,
|
||||
"env": {
|
||||
"JVM_OPTS": "-Xms256m -Xmx768m"
|
||||
},
|
||||
"id": "marathon-user",
|
||||
"instances": 1,
|
||||
"labels": {
|
||||
@@ -196,17 +222,29 @@ RwczovL2Rvd25sb2Fkcy5tZXNvc3BoZXJlLmlvL21hcmF0aG9uL2Fzc2V0cy9pY29uLXNlcnZpY2Ut\
|
||||
bWFyYXRob24tbGFyZ2UucG5nIiwgImljb24tbWVkaXVtIjogImh0dHBzOi8vZG93bmxvYWRzLm1lc2\
|
||||
9zcGhlcmUuaW8vbWFyYXRob24vYXNzZXRzL2ljb24tc2VydmljZS1tYXJhdGhvbi1tZWRpdW0ucG5n\
|
||||
IiwgImljb24tc21hbGwiOiAiaHR0cHM6Ly9kb3dubG9hZHMubWVzb3NwaGVyZS5pby9tYXJhdGhvbi\
|
||||
9hc3NldHMvaWNvbi1zZXJ2aWNlLW1hcmF0aG9uLXNtYWxsLnBuZyJ9LCAibWFpbnRhaW5lciI6ICJz\
|
||||
dXBwb3J0QG1lc29zcGhlcmUuaW8iLCAibmFtZSI6ICJtYXJhdGhvbiIsICJzY20iOiAiaHR0cHM6Ly\
|
||||
9naXRodWIuY29tL21lc29zcGhlcmUvbWFyYXRob24uZ2l0IiwgInRhZ3MiOiBbIm1lc29zcGhlcmUi\
|
||||
LCAiZnJhbWV3b3JrIl0sICJ2ZXJzaW9uIjogIjAuOC4xIn0=",
|
||||
9hc3NldHMvaWNvbi1zZXJ2aWNlLW1hcmF0aG9uLXNtYWxsLnBuZyJ9LCAibGljZW5zZXMiOiBbeyJu\
|
||||
YW1lIjogIkFwYWNoZSBMaWNlbnNlIFZlcnNpb24gMi4wIiwgInVybCI6ICJodHRwczovL2dpdGh1Yi\
|
||||
5jb20vbWVzb3NwaGVyZS9tYXJhdGhvbi9ibG9iL21hc3Rlci9MSUNFTlNFIn1dLCAibWFpbnRhaW5l\
|
||||
ciI6ICJzdXBwb3J0QG1lc29zcGhlcmUuaW8iLCAibmFtZSI6ICJtYXJhdGhvbiIsICJwb3N0SW5zdG\
|
||||
FsbE5vdGVzIjogIk1hcmF0aG9uIERDT1MgU2VydmljZSBoYXMgYmVlbiBzdWNjZXNzZnVsbHkgaW5z\
|
||||
dGFsbGVkIVxuXG5cdERvY3VtZW50YXRpb246IGh0dHBzOi8vbWVzb3NwaGVyZS5naXRodWIuaW8vbW\
|
||||
FyYXRob25cblx0SXNzdWVzOiBodHRwczovZ2l0aHViLmNvbS9tZXNvc3BoZXJlL21hcmF0aG9uL2lz\
|
||||
c3Vlc1xuIiwgInBvc3RVbmluc3RhbGxOb3RlcyI6ICJUaGUgTWFyYXRob24gRENPUyBTZXJ2aWNlIG\
|
||||
hhcyBiZWVuIHVuaW5zdGFsbGVkIGFuZCB3aWxsIG5vIGxvbmdlciBydW4uXG5QbGVhc2UgZm9sbG93\
|
||||
IHRoZSBpbnN0cnVjdGlvbnMgYXQgaHR0cDovL2JldGEtZG9jcy5tZXNvc3BoZXJlLmNvbS9zZXJ2aW\
|
||||
Nlcy9tYXJhdGhvbi8jdW5pbnN0YWxsIHRvIGNsZWFuIHVwIGFueSBwZXJzaXN0ZWQgc3RhdGUiLCAi\
|
||||
cHJlSW5zdGFsbE5vdGVzIjogIldlIHJlY29tbWVuZCBhIG1pbmltdW0gb2Ygb25lIG5vZGUgd2l0aC\
|
||||
BhdCBsZWFzdCAyIENQVSdzIGFuZCAxR0Igb2YgUkFNIGF2YWlsYWJsZSBmb3IgdGhlIE1hcmF0aG9u\
|
||||
IFNlcnZpY2UuIiwgInNjbSI6ICJodHRwczovL2dpdGh1Yi5jb20vbWVzb3NwaGVyZS9tYXJhdGhvbi\
|
||||
5naXQiLCAidGFncyI6IFsibWVzb3NwaGVyZSIsICJmcmFtZXdvcmsiXSwgInZlcnNpb24iOiAiMC44\
|
||||
LjEifQ==",
|
||||
"DCOS_PACKAGE_NAME": "marathon",
|
||||
"DCOS_PACKAGE_REGISTRY_VERSION": "0.1.0-alpha",
|
||||
"DCOS_PACKAGE_RELEASE": "0",
|
||||
"DCOS_PACKAGE_SOURCE": "git://github.com/mesosphere/universe.git",
|
||||
"DCOS_PACKAGE_VERSION": "0.8.1"
|
||||
},
|
||||
"mem": 512.0,
|
||||
"mem": 1024.0,
|
||||
"ports": [
|
||||
0,
|
||||
0
|
||||
@@ -224,8 +262,23 @@ icon-service-marathon-medium.png",
|
||||
"icon-small": "https://downloads.mesosphere.io/marathon/assets/\
|
||||
icon-service-marathon-small.png"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"name": "Apache License Version 2.0",
|
||||
"url": "https://github.com/mesosphere/marathon/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"maintainer": "support@mesosphere.io",
|
||||
"name": "marathon",
|
||||
"postInstallNotes": "Marathon DCOS Service has been successfully \
|
||||
installed!\\n\\n\\tDocumentation: https://\
|
||||
mesosphere.github.io/marathon\\n\\tIssues: https:/github.com/mesosphere/\
|
||||
marathon/issues\\n",
|
||||
"postUninstallNotes": "The Marathon DCOS Service has been uninstalled and \
|
||||
will no longer run.\\nPlease follow the instructions at http://beta-docs.\
|
||||
mesosphere.com/services/marathon/#uninstall to clean up any persisted state",
|
||||
"preInstallNotes": "We recommend a minimum of one node with at least 2 \
|
||||
CPU's and 1GB of RAM available for the Marathon Service.",
|
||||
"scm": "https://github.com/mesosphere/marathon.git",
|
||||
"tags": [
|
||||
"mesosphere",
|
||||
@@ -258,17 +311,22 @@ Please create a JSON file with the appropriate options, and pass the \
|
||||
postInstallNotes=b'')
|
||||
|
||||
|
||||
def test_install():
|
||||
def test_install(zk_znode):
|
||||
_install_chronos()
|
||||
watch_all_deployments()
|
||||
_uninstall_chronos()
|
||||
get_services(expected_count=1, args=['--inactive'])
|
||||
|
||||
|
||||
def test_install_missing_options_file():
|
||||
"""Test that a missing options file results in the expected stderr
|
||||
message."""
|
||||
assert_command(
|
||||
['dcos', 'package', 'install', 'chronos', '--options=asdf.json'],
|
||||
['dcos', 'package', 'install', 'chronos', '--yes',
|
||||
'--options=asdf.json'],
|
||||
returncode=1,
|
||||
stdout=b'We recommend a minimum of one node with at least 1 CPU and '
|
||||
b'2GB of RAM available for the Chronos Service.\n',
|
||||
stderr=b"No such file: asdf.json\n")
|
||||
|
||||
|
||||
@@ -300,7 +358,7 @@ CJdfQ=="""
|
||||
'DCOS_PACKAGE_RELEASE': b'0',
|
||||
}
|
||||
|
||||
app_labels = get_app_labels('helloworld')
|
||||
app_labels = _get_app_labels('helloworld')
|
||||
|
||||
for label, value in expected_labels.items():
|
||||
assert value == six.b(app_labels.get(label))
|
||||
@@ -338,15 +396,15 @@ CJdfQ=="""
|
||||
_uninstall_helloworld()
|
||||
|
||||
|
||||
def test_install_with_id():
|
||||
def test_install_with_id(zk_znode):
|
||||
args = ['--app-id=chronos-1', '--yes']
|
||||
stdout = (b"""Installing package [chronos] version [2.3.4] with app """
|
||||
b"""id [chronos-1]\n""")
|
||||
stdout = (b'Installing package [chronos] version [2.3.4] with app id '
|
||||
b'[chronos-1]\n')
|
||||
_install_chronos(args=args, stdout=stdout)
|
||||
|
||||
args = ['--app-id=chronos-2', '--yes']
|
||||
stdout = (b"""Installing package [chronos] version [2.3.4] with app """
|
||||
b"""id [chronos-2]\n""")
|
||||
stdout = (b'Installing package [chronos] version [2.3.4] with app id '
|
||||
b'[chronos-2]\n')
|
||||
_install_chronos(args=args, stdout=stdout)
|
||||
|
||||
|
||||
@@ -359,19 +417,20 @@ You may need to run 'dcos package update' to update your repositories
|
||||
stderr=stderr)
|
||||
|
||||
|
||||
def test_uninstall_with_id():
|
||||
def test_uninstall_with_id(zk_znode):
|
||||
_uninstall_chronos(args=['--app-id=chronos-1'])
|
||||
|
||||
|
||||
def test_uninstall_all():
|
||||
def test_uninstall_all(zk_znode):
|
||||
_uninstall_chronos(args=['--all'])
|
||||
get_services(expected_count=1, args=['--inactive'])
|
||||
|
||||
|
||||
def test_uninstall_missing():
|
||||
stderr = b'Package [chronos] is not installed.\n'
|
||||
stderr = 'Package [chronos] is not installed.\n'
|
||||
_uninstall_chronos(returncode=1, stderr=stderr)
|
||||
|
||||
stderr = b'Package [chronos] with id [chronos-1] is not installed.\n'
|
||||
stderr = 'Package [chronos] with id [chronos-1] is not installed.\n'
|
||||
_uninstall_chronos(
|
||||
args=['--app-id=chronos-1'],
|
||||
returncode=1,
|
||||
@@ -417,7 +476,7 @@ def test_uninstall_cli():
|
||||
_uninstall_helloworld()
|
||||
|
||||
|
||||
def test_list_installed():
|
||||
def test_list_installed(zk_znode):
|
||||
assert_command(['dcos', 'package', 'list-installed'],
|
||||
stdout=b'[]\n')
|
||||
|
||||
@@ -446,14 +505,23 @@ service-chronos-medium.png",
|
||||
"icon-small": "https://downloads.mesosphere.io/chronos/assets/icon-\
|
||||
service-chronos-small.png"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"name": "Apache License Version 2.0",
|
||||
"url": "https://github.com/mesos/chronos/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"maintainer": "support@mesosphere.io",
|
||||
"name": "chronos",
|
||||
"packageSource": "git://github.com/mesosphere/universe.git",
|
||||
"postInstallNotes": "Chronos DCOS Service has been successfully installed!\
|
||||
\\nWe recommend a minimum of one node with at least 1 CPU and 2GB of RAM \
|
||||
available for the Chronos Service.\\n\\n\\tDocumentation: \
|
||||
http://mesos.github.io/chronos\\n\\tIssues: https://github.com/mesos/\
|
||||
chronos/issues",
|
||||
\\n\\n\\tDocumentation: http://mesos.github.io/chronos\\n\\tIssues: https://\
|
||||
github.com/mesos/chronos/issues",
|
||||
"postUninstallNotes": "The Chronos DCOS Service has been uninstalled and \
|
||||
will no longer run.\\nPlease follow the instructions at http://beta-docs.\
|
||||
mesosphere.com/services/chronos/#uninstall to clean up any persisted state",
|
||||
"preInstallNotes": "We recommend a minimum of one node with at least 1 \
|
||||
CPU and 2GB of RAM available for the Chronos Service.",
|
||||
"releaseVersion": "0",
|
||||
"scm": "https://github.com/mesos/chronos.git",
|
||||
"tags": [
|
||||
@@ -570,6 +638,30 @@ def test_list_installed_cli():
|
||||
_uninstall_helloworld()
|
||||
|
||||
|
||||
def test_uninstall_multiple_frameworknames(zk_znode):
|
||||
_install_chronos(
|
||||
args=['--yes', '--options=tests/data/package/chronos-1.json'])
|
||||
_install_chronos(
|
||||
args=['--yes', '--options=tests/data/package/chronos-2.json'])
|
||||
|
||||
watch_all_deployments()
|
||||
|
||||
_uninstall_chronos(
|
||||
args=['--app-id=chronos-user-1'],
|
||||
returncode=1,
|
||||
stderr='Unable to shutdown the framework for [chronos-user] because '
|
||||
'there are multiple frameworks with the same name: ')
|
||||
_uninstall_chronos(
|
||||
args=['--app-id=chronos-user-2'],
|
||||
returncode=1,
|
||||
stderr='Unable to shutdown the framework for [chronos-user] because '
|
||||
'there are multiple frameworks with the same name: ')
|
||||
|
||||
for framework in get_services(args=['--inactive']):
|
||||
if framework['name'] == 'chronos-user':
|
||||
service_shutdown(framework['id'])
|
||||
|
||||
|
||||
def test_search():
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos',
|
||||
@@ -607,7 +699,7 @@ def test_search():
|
||||
assert stderr == b''
|
||||
|
||||
|
||||
def get_app_labels(app_id):
|
||||
def _get_app_labels(app_id):
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'marathon', 'app', 'show', app_id])
|
||||
|
||||
@@ -635,9 +727,13 @@ def _uninstall_helloworld(args=[]):
|
||||
assert_command(['dcos', 'package', 'uninstall', 'helloworld'] + args)
|
||||
|
||||
|
||||
def _uninstall_chronos(args=[], returncode=0, stdout=b'', stderr=b''):
|
||||
cmd = ['dcos', 'package', 'uninstall', 'chronos'] + args
|
||||
assert_command(cmd, returncode, stdout, stderr)
|
||||
def _uninstall_chronos(args=[], returncode=0, stdout=b'', stderr=''):
|
||||
result_returncode, result_stdout, result_stderr = exec_command(
|
||||
['dcos', 'package', 'uninstall', 'chronos'] + args)
|
||||
|
||||
assert result_returncode == returncode
|
||||
assert result_stdout == stdout
|
||||
assert result_stderr.decode('utf-8').startswith(stderr)
|
||||
|
||||
|
||||
def _install_chronos(
|
||||
@@ -645,10 +741,11 @@ def _install_chronos(
|
||||
returncode=0,
|
||||
stdout=b'Installing package [chronos] version [2.3.4]\n',
|
||||
stderr=b'',
|
||||
preInstallNotes=b'We recommend a minimum of one node with at least 1 '
|
||||
b'CPU and 2GB of RAM available for the Chronos '
|
||||
b'Service.\n',
|
||||
postInstallNotes=b'Chronos DCOS Service has been successfully '
|
||||
b'installed!\nWe recommend a minimum of one node '
|
||||
b'with at least 1 CPU and 2GB of RAM available for '
|
||||
b'''the Chronos Service.
|
||||
b'''installed!
|
||||
|
||||
\tDocumentation: http://mesos.github.io/chronos
|
||||
\tIssues: https://github.com/mesos/chronos/issues\n''',
|
||||
@@ -658,6 +755,6 @@ def _install_chronos(
|
||||
assert_command(
|
||||
cmd,
|
||||
returncode,
|
||||
stdout + postInstallNotes,
|
||||
preInstallNotes + stdout + postInstallNotes,
|
||||
stderr,
|
||||
stdin=stdin)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import collections
|
||||
import json
|
||||
import time
|
||||
|
||||
import dcos.util as util
|
||||
@@ -8,7 +6,14 @@ from dcos.util import create_schema
|
||||
from dcoscli.service.main import _service_table
|
||||
|
||||
import pytest
|
||||
from common import assert_command, exec_command, watch_all_deployments
|
||||
from common import (assert_command, delete_zk_nodes, exec_command,
|
||||
get_services, service_shutdown, watch_all_deployments)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def zk_znode(request):
|
||||
request.addfinalizer(delete_zk_nodes)
|
||||
return request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -60,6 +65,7 @@ def test_help():
|
||||
Usage:
|
||||
dcos service --info
|
||||
dcos service [--inactive --json]
|
||||
dcos service shutdown <service-id>
|
||||
|
||||
Options:
|
||||
-h, --help Show this screen
|
||||
@@ -73,6 +79,9 @@ Options:
|
||||
master, but haven't yet reached their failover timeout.
|
||||
|
||||
--version Show version
|
||||
|
||||
Positional Arguments:
|
||||
<service-id> The ID for the DCOS Service
|
||||
"""
|
||||
assert_command(['dcos', 'service', '--help'], stdout=stdout)
|
||||
|
||||
@@ -85,7 +94,7 @@ def test_info():
|
||||
def test_service(service):
|
||||
returncode, stdout, stderr = exec_command(['dcos', 'service', '--json'])
|
||||
|
||||
services = _get_services(1)
|
||||
services = get_services(1)
|
||||
|
||||
schema = _get_schema(service)
|
||||
for srv in services:
|
||||
@@ -103,19 +112,20 @@ def _get_schema(service):
|
||||
return schema
|
||||
|
||||
|
||||
def test_service_inactive():
|
||||
def test_service_inactive(zk_znode):
|
||||
# install cassandra
|
||||
stdout = b"""Installing package [cassandra] version \
|
||||
[0.1.0-SNAPSHOT-447-master-3ad1bbf8f7]
|
||||
The Apache Cassandra DCOS Service implementation is alpha and there may \
|
||||
be bugs, incomplete features, incorrect documentation or other discrepancies.
|
||||
In order for Cassandra to start successfully, all resources must be \
|
||||
available in the cluster, including ports, CPU shares, RAM and disk.
|
||||
stdout = b"""The Apache Cassandra DCOS Service implementation is alpha \
|
||||
and there may be bugs, incomplete features, incorrect documentation or other \
|
||||
discrepancies.
|
||||
The default configuration requires 3 nodes each with 0.3 CPU shares, 1184MB \
|
||||
of memory and 272MB of disk.
|
||||
Installing package [cassandra] version [0.1.0-SNAPSHOT-447-master-3ad1bbf8f7]
|
||||
Thank you for installing the Apache Cassandra DCOS Service.
|
||||
|
||||
\tDocumentation: http://mesosphere.github.io/cassandra-mesos/
|
||||
\tIssues: https://github.com/mesosphere/cassandra-mesos/issues
|
||||
"""
|
||||
assert_command(['dcos', 'package', 'install', 'cassandra'],
|
||||
assert_command(['dcos', 'package', 'install', 'cassandra', '--yes'],
|
||||
stdout=stdout)
|
||||
|
||||
# wait for it to deploy
|
||||
@@ -125,11 +135,10 @@ available in the cluster, including ports, CPU shares, RAM and disk.
|
||||
time.sleep(5)
|
||||
|
||||
# assert marathon and cassandra are listed
|
||||
_get_services(2)
|
||||
get_services(2)
|
||||
|
||||
# uninstall cassandra. For now, need to explicitly remove the
|
||||
# group that is left by cassandra. See MARATHON-144
|
||||
assert_command(['dcos', 'package', 'uninstall', 'cassandra'])
|
||||
# uninstall cassandra using marathon. For now, need to explicitly remove
|
||||
# the group that is left by cassandra. See MARATHON-144
|
||||
assert_command(['dcos', 'marathon', 'group', 'remove', '/cassandra'])
|
||||
|
||||
watch_all_deployments(300)
|
||||
@@ -139,11 +148,19 @@ available in the cluster, including ports, CPU shares, RAM and disk.
|
||||
time.sleep(5)
|
||||
|
||||
# assert only marathon is active
|
||||
_get_services(1)
|
||||
get_services(1)
|
||||
# assert marathon and cassandra are listed with --inactive
|
||||
services = _get_services(None, ['--inactive'])
|
||||
services = get_services(None, ['--inactive'])
|
||||
assert len(services) >= 2
|
||||
|
||||
# shutdown the cassandra framework
|
||||
for framework in get_services(args=['--inactive']):
|
||||
if framework['name'] == 'cassandra.dcos':
|
||||
service_shutdown(framework['id'])
|
||||
|
||||
# assert marathon is only listed with --inactive
|
||||
get_services(1, ['--inactive'])
|
||||
|
||||
|
||||
# not an integration test
|
||||
def test_task_table(service):
|
||||
@@ -155,29 +172,3 @@ def test_task_table(service):
|
||||
marathon mesos.vm True 0 0.2 32 0 \
|
||||
20150502-231327-16842879-5050-3889-0000 """
|
||||
assert str(table) == stdout
|
||||
|
||||
|
||||
def _get_services(expected_count=None, args=[]):
|
||||
"""Get services
|
||||
|
||||
:param expected_count: assert exactly this number of services are
|
||||
running
|
||||
:type expected_count: int
|
||||
:param args: cli arguments
|
||||
:type args: [str]
|
||||
:returns: services
|
||||
:rtype: [dict]
|
||||
"""
|
||||
|
||||
returncode, stdout, stderr = exec_command(
|
||||
['dcos', 'service', '--json'] + args)
|
||||
|
||||
assert returncode == 0
|
||||
assert stderr == b''
|
||||
|
||||
services = json.loads(stdout.decode('utf-8'))
|
||||
assert isinstance(services, collections.Sequence)
|
||||
if expected_count is not None:
|
||||
assert len(services) == expected_count
|
||||
|
||||
return services
|
||||
|
||||
@@ -31,6 +31,7 @@ def _default_to_error(response):
|
||||
return DefaultError('{}: {}'.format(response.status_code, response.text))
|
||||
|
||||
|
||||
@util.duration
|
||||
def request(method,
|
||||
url,
|
||||
timeout=3.0,
|
||||
|
||||
125
dcos/mesos.py
125
dcos/mesos.py
@@ -1,8 +1,7 @@
|
||||
import fnmatch
|
||||
import itertools
|
||||
|
||||
import dcos.http
|
||||
from dcos import util
|
||||
from dcos import http, util
|
||||
from dcos.errors import DCOSException
|
||||
|
||||
from six.moves import urllib
|
||||
@@ -11,23 +10,43 @@ logger = util.get_logger(__name__)
|
||||
|
||||
|
||||
def get_master(config=None):
|
||||
"""Create a MesosMaster object using the url stored in the
|
||||
'core.master' property of the user's config.
|
||||
"""Create a Master object using the URLs stored in the user's
|
||||
configuration.
|
||||
|
||||
:param config: config
|
||||
:type config: Toml
|
||||
:returns: MesosMaster object
|
||||
:rtype: MesosMaster
|
||||
|
||||
:returns: master state object
|
||||
:rtype: Master
|
||||
"""
|
||||
|
||||
return Master(get_master_client(config).get_state())
|
||||
|
||||
|
||||
def get_master_client(config=None):
|
||||
"""Create a Mesos master client using the URLs stored in the user's
|
||||
configuration.
|
||||
|
||||
:param config: config
|
||||
:type config: Toml
|
||||
:returns: mesos master client
|
||||
:rtype: MasterClient
|
||||
"""
|
||||
|
||||
if config is None:
|
||||
config = util.get_config()
|
||||
|
||||
mesos_url = get_mesos_url(config)
|
||||
return MesosMaster(mesos_url)
|
||||
mesos_url = _get_mesos_url(config)
|
||||
return MasterClient(mesos_url)
|
||||
|
||||
|
||||
def get_mesos_url(config):
|
||||
def _get_mesos_url(config):
|
||||
"""
|
||||
:param config: configuration
|
||||
:type config: Toml
|
||||
:returns: url for the Mesos master
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
mesos_master_url = config.get('core.mesos_master_url')
|
||||
if mesos_master_url is None:
|
||||
dcos_url = util.get_config_vals(config, ['core.dcos_url'])[0]
|
||||
@@ -36,30 +55,65 @@ def get_mesos_url(config):
|
||||
return mesos_master_url
|
||||
|
||||
|
||||
MESOS_TIMEOUT = 3
|
||||
class MasterClient:
|
||||
"""Client for communicating with the Mesos master
|
||||
|
||||
|
||||
class MesosMaster(object):
|
||||
"""Mesos Master Model
|
||||
|
||||
:param url: master url (e.g. "http://localhost:5050")
|
||||
:param url: URL for the Mesos master
|
||||
:type url: str
|
||||
"""
|
||||
|
||||
def __init__(self, url):
|
||||
self._url = url
|
||||
self._state = None
|
||||
self._base_url = url
|
||||
|
||||
def _create_url(self, path):
|
||||
"""Creates the url from the provided path.
|
||||
|
||||
:param path: url path
|
||||
:type path: str
|
||||
:returns: constructed url
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
return urllib.parse.urljoin(self._base_url, path)
|
||||
|
||||
def get_state(self):
|
||||
"""Get the Mesos master state json object
|
||||
|
||||
:returns: Mesos' master state json object
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
return http.get(self._create_url('master/state.json')).json()
|
||||
|
||||
def shutdown_framework(self, framework_id):
|
||||
"""Shuts down a Mesos framework
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
logger.info('Shutting down framework {}'.format(framework_id))
|
||||
|
||||
data = 'frameworkId={}'.format(framework_id)
|
||||
http.post(self._create_url('master/shutdown'), data=data)
|
||||
|
||||
|
||||
class Master(object):
|
||||
"""Mesos Master Model
|
||||
|
||||
:param state: Mesos master state json
|
||||
:type state: dict
|
||||
"""
|
||||
|
||||
def __init__(self, state):
|
||||
self._state = state
|
||||
|
||||
def state(self):
|
||||
"""Returns master's /master/state.json. Fetches and saves it if we
|
||||
haven't already.
|
||||
"""Returns master's master/state.json.
|
||||
|
||||
:returns: state.json
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if not self._state:
|
||||
self._state = self.fetch('master/state.json').json()
|
||||
return self._state
|
||||
|
||||
def slave(self, fltr):
|
||||
@@ -70,7 +124,7 @@ class MesosMaster(object):
|
||||
:param fltr: filter string
|
||||
:type fltr: str
|
||||
:returns: the slave that has `fltr` in its id
|
||||
:rtype: MesosSlave
|
||||
:rtype: Slave
|
||||
"""
|
||||
|
||||
slaves = self.slaves(fltr)
|
||||
@@ -93,10 +147,10 @@ class MesosMaster(object):
|
||||
:param fltr: filter string
|
||||
:type fltr: str
|
||||
:returns: Those slaves that have `fltr` in their 'id'
|
||||
:rtype: [MesosSlave]
|
||||
:rtype: [Slave]
|
||||
"""
|
||||
|
||||
return [MesosSlave(slave)
|
||||
return [Slave(slave)
|
||||
for slave in self.state()['slaves']
|
||||
if fltr in slave['id']]
|
||||
|
||||
@@ -198,25 +252,8 @@ class MesosMaster(object):
|
||||
if inactive or framework['active']:
|
||||
yield framework
|
||||
|
||||
@util.duration
|
||||
def fetch(self, path, **kwargs):
|
||||
"""GET the resource located at `path`
|
||||
|
||||
:param path: the URL path
|
||||
:type path: str
|
||||
:param **kwargs: requests.get kwargs
|
||||
:type **kwargs: dict
|
||||
:returns: the response object
|
||||
:rtype: Response
|
||||
"""
|
||||
|
||||
url = urllib.parse.urljoin(self._url, path)
|
||||
return dcos.http.get(url,
|
||||
timeout=MESOS_TIMEOUT,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class MesosSlave(object):
|
||||
class Slave(object):
|
||||
"""Mesos Slave Model
|
||||
|
||||
:param slave: dictionary representing the slave.
|
||||
@@ -256,7 +293,7 @@ class Task(object):
|
||||
:param task: task properties
|
||||
:type task: dict
|
||||
:param master: mesos master
|
||||
:type master: MesosMaster
|
||||
:type master: Master
|
||||
"""
|
||||
|
||||
def __init__(self, task, master):
|
||||
|
||||
@@ -14,7 +14,7 @@ import git
|
||||
import portalocker
|
||||
import pystache
|
||||
import six
|
||||
from dcos import constants, emitting, errors, marathon, subcommand, util
|
||||
from dcos import constants, emitting, errors, marathon, mesos, subcommand, util
|
||||
from dcos.errors import DCOSException
|
||||
|
||||
from six.moves import urllib
|
||||
@@ -159,12 +159,12 @@ def uninstall(package_name, remove_all, app_id, cli, app):
|
||||
uninstalled = True
|
||||
|
||||
if app:
|
||||
init_client = marathon.create_client()
|
||||
|
||||
num_apps = uninstall_app(package_name,
|
||||
remove_all,
|
||||
app_id,
|
||||
init_client)
|
||||
num_apps = uninstall_app(
|
||||
package_name,
|
||||
remove_all,
|
||||
app_id,
|
||||
marathon.create_client(),
|
||||
mesos.get_master_client())
|
||||
|
||||
if num_apps > 0:
|
||||
uninstalled = True
|
||||
@@ -191,7 +191,7 @@ def uninstall_subcommand(distribution_name):
|
||||
return subcommand.uninstall(distribution_name)
|
||||
|
||||
|
||||
def uninstall_app(app_name, remove_all, app_id, init_client):
|
||||
def uninstall_app(app_name, remove_all, app_id, init_client, master_client):
|
||||
"""Uninstalls an app.
|
||||
|
||||
:param app_name: The app to uninstall
|
||||
@@ -202,6 +202,8 @@ def uninstall_app(app_name, remove_all, app_id, init_client):
|
||||
:type app_id: str
|
||||
:param init_client: The program to use to run the app
|
||||
:type init_client: object
|
||||
:param master_client: the mesos master client
|
||||
:type master_client: dcos.mesos.MasterClient
|
||||
:returns: number of apps uninstalled
|
||||
:rtype: int
|
||||
"""
|
||||
@@ -226,14 +228,46 @@ def uninstall_app(app_name, remove_all, app_id, init_client):
|
||||
|
||||
if not remove_all and len(matching_apps) > 1:
|
||||
app_ids = [a.get('id') for a in matching_apps]
|
||||
raise DCOSException("""Multiple instances of app [{}] are installed. \
|
||||
Please specify the app id of the instance to uninstall or uninstall all. \
|
||||
The app ids of the installed package instances are: [{}].""".format(
|
||||
app_name, ', '.join(app_ids)))
|
||||
raise DCOSException(
|
||||
("Multiple instances of app [{}] are installed. Please specify "
|
||||
"the app id of the instance to uninstall or uninstall all. The "
|
||||
"app ids of the installed package instances are: [{}].").format(
|
||||
app_name,
|
||||
', '.join(app_ids)))
|
||||
|
||||
for app in matching_apps:
|
||||
# First, remove the app from Marathon
|
||||
init_client.remove_app(app['id'], force=True)
|
||||
|
||||
# Second, shutdown the framework with Mesos
|
||||
framework_name = app.get('labels', {}).get(PACKAGE_FRAMEWORK_NAME_KEY)
|
||||
if framework_name is not None:
|
||||
logger.info(
|
||||
'Trying to shutdown framework {}'.format(framework_name))
|
||||
frameworks = mesos.Master(master_client.get_state()).frameworks(
|
||||
inactive=True)
|
||||
|
||||
# Look up all the framework names
|
||||
framework_ids = [
|
||||
framework['id']
|
||||
for framework in frameworks
|
||||
if framework['name'] == framework_name
|
||||
]
|
||||
|
||||
logger.info(
|
||||
'Found the following frameworks: {}'.format(framework_ids))
|
||||
|
||||
if len(framework_ids) == 1:
|
||||
master_client.shutdown_framework(framework_ids[0])
|
||||
elif len(framework_ids) > 1:
|
||||
raise DCOSException(
|
||||
"Unable to shutdown the framework for [{}] because there "
|
||||
"are multiple frameworks with the same name: [{}]. "
|
||||
"Manually shut them down using 'dcos service "
|
||||
"shutdown'.".format(
|
||||
framework_name,
|
||||
', '.join(framework_ids)))
|
||||
|
||||
return len(matching_apps)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user