add snapshot report to dcos cli (#661)
This commit is contained in:
@@ -12,6 +12,11 @@ Usage:
|
|||||||
[--master-proxy]
|
[--master-proxy]
|
||||||
(--leader | --master | --mesos-id=<mesos-id> | --slave=<slave-id>)
|
(--leader | --master | --mesos-id=<mesos-id> | --slave=<slave-id>)
|
||||||
[<command>]
|
[<command>]
|
||||||
|
dcos node snapshot create (<nodes>)...
|
||||||
|
dcos node snapshot delete <snapshot>
|
||||||
|
dcos node snapshot download <snapshot> [--location=<location>]
|
||||||
|
dcos node snapshot (--list | --status | --cancel)
|
||||||
|
[--json]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
log
|
log
|
||||||
@@ -19,6 +24,12 @@ Commands:
|
|||||||
ssh
|
ssh
|
||||||
Establish an SSH connection to the master or agent nodes of your DC/OS
|
Establish an SSH connection to the master or agent nodes of your DC/OS
|
||||||
cluster.
|
cluster.
|
||||||
|
snapshot create
|
||||||
|
Create a snapshot. Nodes can be: ip address, hostname, mesos ID or key words "all", "masters", "agents".
|
||||||
|
snapshot download
|
||||||
|
Download a snapshot.
|
||||||
|
snapshot delete
|
||||||
|
Delete a snapshot.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--config-file=<path>
|
--config-file=<path>
|
||||||
@@ -50,6 +61,14 @@ Options:
|
|||||||
Agent node with the provided ID.
|
Agent node with the provided ID.
|
||||||
--user=<user>
|
--user=<user>
|
||||||
The SSH user, where the default user [default: core].
|
The SSH user, where the default user [default: core].
|
||||||
|
--list
|
||||||
|
List available snapshots.
|
||||||
|
--status
|
||||||
|
Print snapshot job status.
|
||||||
|
--cancel
|
||||||
|
Cancel a running snapshot job.
|
||||||
|
--location=<location>
|
||||||
|
Download a snapshot to a particular location. If not set default to present working directory.
|
||||||
--version
|
--version
|
||||||
Print version information.
|
Print version information.
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import dcoscli
|
import dcoscli
|
||||||
import docopt
|
import docopt
|
||||||
import six
|
import six
|
||||||
from dcos import cmds, emitting, errors, mesos, util
|
from dcos import (cmds, config, cosmospackage, emitting, errors, http, mesos,
|
||||||
|
util)
|
||||||
from dcos.errors import DCOSException, DefaultError
|
from dcos.errors import DCOSException, DefaultError
|
||||||
from dcoscli import log, tables
|
from dcoscli import log, tables
|
||||||
|
from dcoscli.package.main import confirm, get_cosmos_url
|
||||||
from dcoscli.subcommand import default_command_info, default_doc
|
from dcoscli.subcommand import default_command_info, default_doc
|
||||||
from dcoscli.util import decorate_docopt_usage
|
from dcoscli.util import decorate_docopt_usage
|
||||||
|
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
logger = util.get_logger(__name__)
|
logger = util.get_logger(__name__)
|
||||||
emitter = emitting.FlatEmitter()
|
emitter = emitting.FlatEmitter()
|
||||||
|
|
||||||
|
SNAPSHOT_BASE_URL = '/system/health/v1/report/snapshot/'
|
||||||
|
|
||||||
|
# if snapshot size if more then 100Mb then warn user.
|
||||||
|
SNAPSHOT_WARN_SIZE = 1000000
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
try:
|
try:
|
||||||
@@ -64,13 +74,358 @@ def _cmds():
|
|||||||
'--user', '--master-proxy', '<command>'],
|
'--user', '--master-proxy', '<command>'],
|
||||||
function=_ssh),
|
function=_ssh),
|
||||||
|
|
||||||
|
cmds.Command(
|
||||||
|
hierarchy=['node', 'snapshot', 'create'],
|
||||||
|
arg_keys=['<nodes>'],
|
||||||
|
function=_snapshot_create),
|
||||||
|
|
||||||
|
cmds.Command(
|
||||||
|
hierarchy=['node', 'snapshot', 'delete'],
|
||||||
|
arg_keys=['<snapshot>'],
|
||||||
|
function=_snapshot_delete),
|
||||||
|
|
||||||
|
cmds.Command(
|
||||||
|
hierarchy=['node', 'snapshot', 'download'],
|
||||||
|
arg_keys=['<snapshot>', '--location'],
|
||||||
|
function=_snapshot_download),
|
||||||
|
|
||||||
|
cmds.Command(
|
||||||
|
hierarchy=['node', 'snapshot'],
|
||||||
|
arg_keys=['--list', '--status', '--cancel', '--json'],
|
||||||
|
function=_snapshot_manage),
|
||||||
|
|
||||||
cmds.Command(
|
cmds.Command(
|
||||||
hierarchy=['node'],
|
hierarchy=['node'],
|
||||||
arg_keys=['--json'],
|
arg_keys=['--json'],
|
||||||
function=_list),
|
function=_list)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_error(fn):
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def check_for_snapshot_error(*args, **kwargs):
|
||||||
|
response = fn(*args, **kwargs)
|
||||||
|
if response.status_code != 200:
|
||||||
|
err_msg = ('Error making {} request\nURL: '
|
||||||
|
'{}, status_code: {}.'.format(args[1], args[0],
|
||||||
|
response.status_code))
|
||||||
|
if not kwargs.get('stream'):
|
||||||
|
err_status = _read_http_response_body(response).get('status')
|
||||||
|
if err_status:
|
||||||
|
err_msg = err_status
|
||||||
|
raise DCOSException(err_msg)
|
||||||
|
return response
|
||||||
|
return check_for_snapshot_error
|
||||||
|
|
||||||
|
|
||||||
|
def _check_3dt_version():
|
||||||
|
"""
|
||||||
|
The function checks if cluster has snapshot capability.
|
||||||
|
|
||||||
|
:raises: DCOSException if cluster does not have snapshot capability
|
||||||
|
"""
|
||||||
|
|
||||||
|
cosmos = cosmospackage.Cosmos(get_cosmos_url())
|
||||||
|
if not cosmos.has_capability('SUPPORT_CLUSTER_REPORT'):
|
||||||
|
raise DCOSException(
|
||||||
|
'DC/OS backend does not support snapshot capabilities in this '
|
||||||
|
'version. Must be DC/OS >= 1.8')
|
||||||
|
|
||||||
|
|
||||||
|
# http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size # noqa
|
||||||
|
def sizeof_fmt(num, suffix='B'):
|
||||||
|
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||||
|
if abs(num) < 1024.0:
|
||||||
|
return "%3.1f%s%s" % (num, unit, suffix)
|
||||||
|
num /= 1024.0
|
||||||
|
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_snapshots_json():
|
||||||
|
"""
|
||||||
|
Get a json with a list of snapshots
|
||||||
|
|
||||||
|
:return: available snapshots on a cluster.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
return _do_snapshot_request(
|
||||||
|
urllib.parse.urljoin(SNAPSHOT_BASE_URL, 'list/all'),
|
||||||
|
'GET')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_snapshots_list():
|
||||||
|
"""
|
||||||
|
Get a list of tuples (snapshot_file_name, file_size), ..
|
||||||
|
|
||||||
|
:return: list of snapshots
|
||||||
|
:rtype: list of tuples
|
||||||
|
"""
|
||||||
|
|
||||||
|
available_snapshots = []
|
||||||
|
for _, snapshot_files in _get_snapshots_json().items():
|
||||||
|
if snapshot_files is None:
|
||||||
|
continue
|
||||||
|
for snapshot_file_obj in snapshot_files:
|
||||||
|
if ('file_name' not in snapshot_file_obj
|
||||||
|
or 'file_size' not in snapshot_file_obj):
|
||||||
|
raise DCOSException(
|
||||||
|
'Request to get a list of available snapshot returned '
|
||||||
|
'unexpected response {}'.format(snapshot_file_obj))
|
||||||
|
|
||||||
|
available_snapshots.append(
|
||||||
|
(os.path.basename(snapshot_file_obj['file_name']),
|
||||||
|
snapshot_file_obj['file_size']))
|
||||||
|
return available_snapshots
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_manage(list_snapshots, status, cancel, json):
|
||||||
|
"""
|
||||||
|
Manage snapshots
|
||||||
|
|
||||||
|
:param list_snapshots: a list of available snapshots
|
||||||
|
:type list_snapshots: bool
|
||||||
|
:param status: show snapshot job status
|
||||||
|
:type status: bool
|
||||||
|
:param cancel: cancel snapshot job
|
||||||
|
:type cancel: bool
|
||||||
|
:return: process return code
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
_check_3dt_version()
|
||||||
|
if list_snapshots:
|
||||||
|
if json:
|
||||||
|
emitter.publish(_get_snapshots_json())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
available_snapshots = _get_snapshots_list()
|
||||||
|
if not available_snapshots:
|
||||||
|
emitter.publish("No snapshots")
|
||||||
|
return 0
|
||||||
|
emitter.publish("Available snapshots:")
|
||||||
|
for available_snapshot in sorted(available_snapshots,
|
||||||
|
key=lambda t: t[0]):
|
||||||
|
emitter.publish('{} {}'.format(available_snapshot[0],
|
||||||
|
sizeof_fmt(available_snapshot[1])))
|
||||||
|
return 0
|
||||||
|
elif status:
|
||||||
|
url = urllib.parse.urljoin(SNAPSHOT_BASE_URL, 'status/all')
|
||||||
|
snapshot_response = _do_snapshot_request(url, 'GET')
|
||||||
|
if json:
|
||||||
|
emitter.publish(snapshot_response)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for host, props in sorted(snapshot_response.items()):
|
||||||
|
emitter.publish(host)
|
||||||
|
for key, value in sorted(props.items()):
|
||||||
|
emitter.publish(' {}: {}'.format(key, value))
|
||||||
|
emitter.publish('\n')
|
||||||
|
return 0
|
||||||
|
elif cancel:
|
||||||
|
url = urllib.parse.urljoin(SNAPSHOT_BASE_URL, 'cancel')
|
||||||
|
snapshot_response = _do_snapshot_request(url, 'POST')
|
||||||
|
if json:
|
||||||
|
emitter.publish(snapshot_response)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if 'status' not in snapshot_response:
|
||||||
|
raise DCOSException(
|
||||||
|
'Request to cancel a snapshot job {} returned '
|
||||||
|
'an unexpected response {}'.format(url, snapshot_response))
|
||||||
|
|
||||||
|
emitter.publish(snapshot_response['status'])
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
raise DCOSException(
|
||||||
|
'Must specify one of list_snapshots, status, cancel')
|
||||||
|
|
||||||
|
|
||||||
|
@snapshot_error
|
||||||
|
def _do_request(url, method, timeout=None, stream=False, **kwargs):
|
||||||
|
"""
|
||||||
|
make HTTP request
|
||||||
|
|
||||||
|
:param url: url
|
||||||
|
:type url: string
|
||||||
|
:param method: HTTP method, GET or POST
|
||||||
|
:type method: string
|
||||||
|
:param timeout: HTTP request timeout, default 3 seconds
|
||||||
|
:type timeout: integer
|
||||||
|
:param stream: stream parameter for requests lib
|
||||||
|
:type stream: bool
|
||||||
|
:return: http response
|
||||||
|
:rtype: requests.Response
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _is_success(status_code):
|
||||||
|
# consider 400 and 503 to be successful status codes.
|
||||||
|
# API will return the error message.
|
||||||
|
if status_code in [200, 400, 503]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# if timeout is not passed, try to read `core.timeout`
|
||||||
|
# if `core.timeout` is not set, default to 3 min.
|
||||||
|
if timeout is None:
|
||||||
|
timeout = config.get_config_val('core.timeout')
|
||||||
|
if not timeout:
|
||||||
|
timeout = 180
|
||||||
|
|
||||||
|
# POST to snapshot api
|
||||||
|
base_url = config.get_config_val("core.dcos_url")
|
||||||
|
if not base_url:
|
||||||
|
raise config.missing_config_exception(['core.dcos_url'])
|
||||||
|
|
||||||
|
url = urllib.parse.urljoin(base_url, url)
|
||||||
|
if method.lower() == 'get':
|
||||||
|
http_response = http.get(url, is_success=_is_success, timeout=timeout,
|
||||||
|
**kwargs)
|
||||||
|
elif method.lower() == 'post':
|
||||||
|
http_response = http.post(url, is_success=_is_success, timeout=timeout,
|
||||||
|
stream=stream, **kwargs)
|
||||||
|
else:
|
||||||
|
raise DCOSException('Unsupported HTTP method: ' + method)
|
||||||
|
return http_response
|
||||||
|
|
||||||
|
|
||||||
|
def _do_snapshot_request(url, method, **kwargs):
|
||||||
|
"""
|
||||||
|
Make HTTP request and expect a JSON response.
|
||||||
|
|
||||||
|
:param url: url
|
||||||
|
:type url: string
|
||||||
|
:param method: HTTP method, GET or POST
|
||||||
|
:type method: string
|
||||||
|
:return: snapshot JSON repsponse
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_response = _do_request(url, method, **kwargs)
|
||||||
|
return _read_http_response_body(http_response)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_http_response_body(http_response):
|
||||||
|
"""
|
||||||
|
Get an requests HTTP response, read it and deserialize to json.
|
||||||
|
|
||||||
|
:param http_response: http response
|
||||||
|
:type http_response: requests.Response onject
|
||||||
|
:return: deserialized json
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = b''
|
||||||
|
try:
|
||||||
|
for chunk in http_response.iter_content(1024):
|
||||||
|
data += chunk
|
||||||
|
snapshot_response = util.load_jsons(data.decode('utf-8'))
|
||||||
|
return snapshot_response
|
||||||
|
except DCOSException:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_download(snapshot, location):
|
||||||
|
"""
|
||||||
|
Download snapshot and put in the the current directory.
|
||||||
|
|
||||||
|
:param snapshot: snapshot file name.
|
||||||
|
:type snapshot: string
|
||||||
|
:return: status code
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
# make sure the requested snapshot exists
|
||||||
|
snapshot_size = 0
|
||||||
|
for available_snapshot in _get_snapshots_list():
|
||||||
|
# _get_snapshot_list must return a list of tuples
|
||||||
|
# where first element is file name and second is its size.
|
||||||
|
if len(available_snapshot) != 2:
|
||||||
|
raise DCOSException(
|
||||||
|
'Request to get a list of snapshots returned an '
|
||||||
|
'unexpected response: {}'.format(available_snapshot))
|
||||||
|
|
||||||
|
# available_snapshot[0] is a snapshot file name
|
||||||
|
# available_snapshot[1] is a snapshot file size
|
||||||
|
if available_snapshot[0] == snapshot:
|
||||||
|
snapshot_size = available_snapshot[1]
|
||||||
|
|
||||||
|
url = urllib.parse.urljoin(SNAPSHOT_BASE_URL, 'serve/' + snapshot)
|
||||||
|
snapshot_location = os.path.join(os.getcwd(), snapshot)
|
||||||
|
if location:
|
||||||
|
if os.path.isdir(location):
|
||||||
|
snapshot_location = os.path.join(location, snapshot)
|
||||||
|
else:
|
||||||
|
snapshot_location = location
|
||||||
|
|
||||||
|
if snapshot_size > SNAPSHOT_WARN_SIZE:
|
||||||
|
if not confirm('Snapshot size is {}, are you sure you want '
|
||||||
|
'to download it?'.format(sizeof_fmt(snapshot_size)),
|
||||||
|
False):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
r = _do_request(url, 'GET', stream=True)
|
||||||
|
try:
|
||||||
|
with open(snapshot_location, 'wb') as f:
|
||||||
|
for chunk in r.iter_content(1024):
|
||||||
|
f.write(chunk)
|
||||||
|
except Exception as e:
|
||||||
|
raise DCOSException(e)
|
||||||
|
emitter.publish('Snapshot downloaded to ' + snapshot_location)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_delete(snapshot):
|
||||||
|
"""
|
||||||
|
Delete a snapshot
|
||||||
|
|
||||||
|
:param snapshot: snapshot file name
|
||||||
|
:type: str
|
||||||
|
:return: status code
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
_check_3dt_version()
|
||||||
|
url = urllib.parse.urljoin(
|
||||||
|
SNAPSHOT_BASE_URL, 'delete/' + snapshot)
|
||||||
|
response = _do_snapshot_request(url, 'POST')
|
||||||
|
|
||||||
|
if 'status' not in response:
|
||||||
|
raise DCOSException(
|
||||||
|
'Request to delete the snapshot {} returned an '
|
||||||
|
'unexpected response {}'.format(url, response))
|
||||||
|
|
||||||
|
emitter.publish(response['status'])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_create(nodes):
|
||||||
|
"""
|
||||||
|
Create a snapshot.
|
||||||
|
|
||||||
|
:param nodes: a list of nodes to collect the logs from.
|
||||||
|
:type nodes: list
|
||||||
|
:returns: process return code
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
_check_3dt_version()
|
||||||
|
url = urllib.parse.urljoin(SNAPSHOT_BASE_URL, 'create')
|
||||||
|
response = _do_snapshot_request(url,
|
||||||
|
'POST',
|
||||||
|
json={'nodes': nodes})
|
||||||
|
if ('status' not in response or 'extra' not in response
|
||||||
|
or 'snapshot_name' not in response['extra']):
|
||||||
|
raise DCOSException(
|
||||||
|
'Request to create snapshot {} returned an '
|
||||||
|
'unexpected response {}'.format(url, response))
|
||||||
|
|
||||||
|
emitter.publish('\n{}, available snapshot: {}'.format(
|
||||||
|
response['status'],
|
||||||
|
response['extra']['snapshot_name']))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _info():
|
def _info():
|
||||||
"""Print node cli information.
|
"""Print node cli information.
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ def _user_options(path):
|
|||||||
return util.load_json(options_file)
|
return util.load_json(options_file)
|
||||||
|
|
||||||
|
|
||||||
def _confirm(prompt, yes):
|
def confirm(prompt, yes):
|
||||||
"""
|
"""
|
||||||
:param prompt: message to display to the terminal
|
:param prompt: message to display to the terminal
|
||||||
:type prompt: str
|
:type prompt: str
|
||||||
@@ -365,7 +365,7 @@ def _install(package_name, package_version, options_path, app_id, cli, app,
|
|||||||
pre_install_notes = pkg_json.get('preInstallNotes')
|
pre_install_notes = pkg_json.get('preInstallNotes')
|
||||||
if app and pre_install_notes:
|
if app and pre_install_notes:
|
||||||
emitter.publish(pre_install_notes)
|
emitter.publish(pre_install_notes)
|
||||||
if not _confirm('Continue installing?', yes):
|
if not confirm('Continue installing?', yes):
|
||||||
emitter.publish('Exiting installation.')
|
emitter.publish('Exiting installation.')
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -519,7 +519,7 @@ def _uninstall(package_name, remove_all, app_id, cli, app):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _get_cosmos_url():
|
def get_cosmos_url():
|
||||||
"""
|
"""
|
||||||
:returns: cosmos base url
|
:returns: cosmos base url
|
||||||
:rtype: str
|
:rtype: str
|
||||||
@@ -540,7 +540,7 @@ def _get_package_manager():
|
|||||||
:rtype: PackageManager
|
:rtype: PackageManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cosmos_url = _get_cosmos_url()
|
cosmos_url = get_cosmos_url()
|
||||||
cosmos_manager = cosmospackage.Cosmos(cosmos_url)
|
cosmos_manager = cosmospackage.Cosmos(cosmos_url)
|
||||||
if cosmos_manager.enabled():
|
if cosmos_manager.enabled():
|
||||||
return cosmos_manager
|
return cosmos_manager
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ Usage:
|
|||||||
[--master-proxy]
|
[--master-proxy]
|
||||||
(--leader | --master | --mesos-id=<mesos-id> | --slave=<slave-id>)
|
(--leader | --master | --mesos-id=<mesos-id> | --slave=<slave-id>)
|
||||||
[<command>]
|
[<command>]
|
||||||
|
dcos node snapshot create (<nodes>)...
|
||||||
|
dcos node snapshot delete <snapshot>
|
||||||
|
dcos node snapshot download <snapshot> [--location=<location>]
|
||||||
|
dcos node snapshot (--list | --status | --cancel)
|
||||||
|
[--json]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
log
|
log
|
||||||
@@ -19,6 +24,12 @@ Commands:
|
|||||||
ssh
|
ssh
|
||||||
Establish an SSH connection to the master or agent nodes of your DC/OS
|
Establish an SSH connection to the master or agent nodes of your DC/OS
|
||||||
cluster.
|
cluster.
|
||||||
|
snapshot create
|
||||||
|
Create a snapshot. Nodes can be: ip address, hostname, mesos ID or key words "all", "masters", "agents".
|
||||||
|
snapshot download
|
||||||
|
Download a snapshot.
|
||||||
|
snapshot delete
|
||||||
|
Delete a snapshot.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--config-file=<path>
|
--config-file=<path>
|
||||||
@@ -50,6 +61,14 @@ Options:
|
|||||||
Agent node with the provided ID.
|
Agent node with the provided ID.
|
||||||
--user=<user>
|
--user=<user>
|
||||||
The SSH user, where the default user [default: core].
|
The SSH user, where the default user [default: core].
|
||||||
|
--list
|
||||||
|
List available snapshots.
|
||||||
|
--status
|
||||||
|
Print snapshot job status.
|
||||||
|
--cancel
|
||||||
|
Cancel a running snapshot job.
|
||||||
|
--location=<location>
|
||||||
|
Download a snapshot to a particular location. If not set default to present working directory.
|
||||||
--version
|
--version
|
||||||
Print version information.
|
Print version information.
|
||||||
|
|
||||||
|
|||||||
186
cli/tests/unit/test_node.py
Normal file
186
cli/tests/unit/test_node.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import dcoscli.node.main as main
|
||||||
|
from dcos.errors import DCOSException
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
def test_check_version_fail(mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _check_3dt_version(), should throw DCOSException exception.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
mock_cosmos().has_capability.return_value = False
|
||||||
|
|
||||||
|
with pytest.raises(DCOSException) as excinfo:
|
||||||
|
main._check_3dt_version()
|
||||||
|
assert str(excinfo.value) == (
|
||||||
|
'DC/OS backend does not support snapshot capabilities in this version.'
|
||||||
|
' Must be DC/OS >= 1.8')
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcos.http.get')
|
||||||
|
def test_check_version_success(mock_get, mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _check_3dt_version(), should not fail.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
m = mock.MagicMock()
|
||||||
|
m.json.return_value = {
|
||||||
|
'capabilities': [{'name': 'SUPPORT_CLUSTER_REPORT'}]
|
||||||
|
}
|
||||||
|
mock_get.return_value = m
|
||||||
|
main._check_3dt_version()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcos.http.get')
|
||||||
|
@mock.patch('dcoscli.node.main._do_snapshot_request')
|
||||||
|
def test_node_snapshot_create(mock_do_snapshot_request, mock_get, mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _snapshot_create(), should not fail.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
m = mock.MagicMock()
|
||||||
|
m.json.return_value = {
|
||||||
|
'capabilities': [{'name': 'SUPPORT_CLUSTER_REPORT'}]
|
||||||
|
}
|
||||||
|
mock_get.return_value = m
|
||||||
|
mock_do_snapshot_request.return_value = {
|
||||||
|
'status': 'OK',
|
||||||
|
'extra': {
|
||||||
|
'snapshot_name': 'snapshot.zip'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main._snapshot_create(['10.10.0.1'])
|
||||||
|
mock_do_snapshot_request.assert_called_once_with(
|
||||||
|
'/system/health/v1/report/snapshot/create',
|
||||||
|
'POST',
|
||||||
|
json={'nodes': ['10.10.0.1']})
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcos.http.get')
|
||||||
|
@mock.patch('dcoscli.node.main._do_snapshot_request')
|
||||||
|
def test_node_snapshot_delete(mock_do_snapshot_request, mock_get, mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _snapshot_delete(), should not fail
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
m = mock.MagicMock()
|
||||||
|
m.json.return_value = {
|
||||||
|
'capabilities': [{'name': 'SUPPORT_CLUSTER_REPORT'}]
|
||||||
|
}
|
||||||
|
mock_get.return_value = m
|
||||||
|
mock_do_snapshot_request.return_value = {
|
||||||
|
'status': 'OK'
|
||||||
|
}
|
||||||
|
main._snapshot_delete('snapshot.zip')
|
||||||
|
mock_do_snapshot_request.assert_called_once_with(
|
||||||
|
'/system/health/v1/report/snapshot/delete/snapshot.zip',
|
||||||
|
'POST'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcos.http.get')
|
||||||
|
@mock.patch('dcoscli.node.main._do_snapshot_request')
|
||||||
|
def test_node_snapshot_list(mock_do_snapshot_request, mock_get, mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _snapshot_manage(), should not fail
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
m = mock.MagicMock()
|
||||||
|
m.json.return_value = {
|
||||||
|
'capabilities': [{'name': 'SUPPORT_CLUSTER_REPORT'}]
|
||||||
|
}
|
||||||
|
mock_get.return_value = m
|
||||||
|
mock_do_snapshot_request.return_value = {
|
||||||
|
'127.0.0.1': [
|
||||||
|
{
|
||||||
|
'file_name': 'snapshot.zip',
|
||||||
|
'file_size': 123
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# _snapshot_manage(list_snapshots, status, cancel, json)
|
||||||
|
main._snapshot_manage(True, False, False, False)
|
||||||
|
mock_do_snapshot_request.assert_called_once_with(
|
||||||
|
'/system/health/v1/report/snapshot/list/all',
|
||||||
|
'GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcos.http.get')
|
||||||
|
@mock.patch('dcoscli.node.main._do_snapshot_request')
|
||||||
|
def test_node_snapshot_status(mock_do_snapshot_request, mock_get, mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _snapshot_manage(), should not fail
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
m = mock.MagicMock()
|
||||||
|
m.json.return_value = {
|
||||||
|
'capabilities': [{'name': 'SUPPORT_CLUSTER_REPORT'}]
|
||||||
|
}
|
||||||
|
mock_get.return_value = m
|
||||||
|
mock_do_snapshot_request.return_value = {
|
||||||
|
'host1': {
|
||||||
|
'prop1': 'value1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# _snapshot_manage(list_snapshots, status, cancel, json)
|
||||||
|
main._snapshot_manage(False, True, False, False)
|
||||||
|
mock_do_snapshot_request.assert_called_once_with(
|
||||||
|
'/system/health/v1/report/snapshot/status/all',
|
||||||
|
'GET'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcos.http.get')
|
||||||
|
@mock.patch('dcoscli.node.main._do_snapshot_request')
|
||||||
|
def test_node_snapshot_cancel(mock_do_snapshot_request, mock_get, mock_cosmos):
|
||||||
|
"""
|
||||||
|
Test _snapshot_manage(), should not fail
|
||||||
|
"""
|
||||||
|
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
m = mock.MagicMock()
|
||||||
|
m.json.return_value = {
|
||||||
|
'capabilities': [{'name': 'SUPPORT_CLUSTER_REPORT'}]
|
||||||
|
}
|
||||||
|
mock_get.return_value = m
|
||||||
|
mock_do_snapshot_request.return_value = {
|
||||||
|
'status': 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
# _snapshot_manage(list_snapshots, status, cancel, json)
|
||||||
|
main._snapshot_manage(False, False, True, False)
|
||||||
|
mock_do_snapshot_request.assert_called_once_with(
|
||||||
|
'/system/health/v1/report/snapshot/cancel',
|
||||||
|
'POST'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('dcos.cosmospackage.Cosmos')
|
||||||
|
@mock.patch('dcoscli.node.main._do_request')
|
||||||
|
@mock.patch('dcoscli.node.main._get_snapshots_list')
|
||||||
|
def test_node_snapshot_download(mock_get_snapshot_list, mock_do_request,
|
||||||
|
mock_cosmos):
|
||||||
|
mock_cosmos().enabled.return_value = True
|
||||||
|
mock_get_snapshot_list.return_value = [('snap.zip', 123)]
|
||||||
|
main._snapshot_download('snap.zip', None)
|
||||||
|
mock_do_request.assert_called_with(
|
||||||
|
'/system/health/v1/report/snapshot/serve/snap.zip', 'GET',
|
||||||
|
stream=True)
|
||||||
@@ -20,8 +20,37 @@ class Cosmos():
|
|||||||
def __init__(self, cosmos_url):
|
def __init__(self, cosmos_url):
|
||||||
self.cosmos_url = cosmos_url
|
self.cosmos_url = cosmos_url
|
||||||
|
|
||||||
|
def has_capability(self, capability):
|
||||||
|
"""Check if cluster has a capability.
|
||||||
|
|
||||||
|
:param capability: capability name
|
||||||
|
:type capability: string
|
||||||
|
:return: does the cluster has capability
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.enabled():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = urllib.parse.urljoin(self.cosmos_url, 'capabilities')
|
||||||
|
response = http.get(url,
|
||||||
|
headers=_get_capabilities_header()).json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if 'capabilities' not in response:
|
||||||
|
logger.error(
|
||||||
|
'Request to get cluster capabilities: {} '
|
||||||
|
'returned unexpected response: {}. '
|
||||||
|
'Missing "capabilities" field'.format(url, response))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return {'name': capability} in response['capabilities']
|
||||||
|
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
"""Returns whether or not cosmos is enabled on specified dcos cluter
|
"""Returns whether or not cosmos is enabled on specified dcos cluster
|
||||||
|
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user