Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mgr/iostat: implement 'ceph iostat' as a mgr plugin #20100

Merged
merged 8 commits into from Apr 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions doc/mgr/plugins.rst
Expand Up @@ -51,13 +51,16 @@ like this::
{
"cmd": "foobar name=myarg,type=CephString",
"desc": "Do something awesome",
"perm": "rw"
"perm": "rw",
# optional:
"poll": "true"
}
]

The ``cmd`` part of each entry is parsed in the same way as internal
Ceph mon and admin socket commands (see mon/MonCommands.h in
the Ceph source for examples)
the Ceph source for examples). Note that the "poll" field is optional,
and is set to False by default.

Config settings
---------------
Expand Down
3 changes: 3 additions & 0 deletions qa/tasks/mgr/test_module_selftest.py
Expand Up @@ -42,6 +42,9 @@ def test_prometheus(self):
def test_influx(self):
self._selftest_plugin("influx")

def test_iostat(self):
self._selftest_plugin("iostat")

def test_selftest_run(self):
self._load_module("selftest")
self.mgr_cluster.mon_manager.raw_cluster_cmd("mgr", "self-test", "run")
Expand Down
75 changes: 45 additions & 30 deletions src/ceph.in
Expand Up @@ -20,6 +20,7 @@ Foundation. See file COPYING.
"""

from __future__ import print_function
from time import sleep
import codecs
import os
import sys
Expand All @@ -42,6 +43,7 @@ CEPH_RELEASE_TYPE = "@CEPH_RELEASE_TYPE@"
FLAG_NOFORWARD = (1 << 0)
FLAG_OBSOLETE = (1 << 1)
FLAG_DEPRECATED = (1 << 2)
FLAG_POLL = (1 << 4)

# priorities from src/common/perf_counters.h
PRIO_CRITICAL = 10
Expand Down Expand Up @@ -317,6 +319,9 @@ def parse_cmdargs(args=None, target=''):

parser.add_argument('--block', action='store_true',
help='block until completion (scrub and deep-scrub only)')
parser.add_argument('--period', '-p', default=1, type=float,
help='polling period, default 1.0 second (for ' \
'polling commands only)')

# returns a Namespace with the parsed args, and a list of all extras
parsed_args, extras = parser.parse_known_args(args)
Expand Down Expand Up @@ -515,6 +520,41 @@ else:
return line


def do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose):
''' Validate a command, and handle the polling flag '''

valid_dict = validate_command(sigdict, cmdargs, verbose)
# Validate input args against list of sigs
if valid_dict:
if parsed_args.output_format:
valid_dict['format'] = parsed_args.output_format
if verbose:
print("Submitting command: ", valid_dict, file=sys.stderr)
else:
return -errno.EINVAL, '', 'invalid command'

while True:
ret, outbuf, outs = json_command(cluster_handle, target=target, argdict=valid_dict,
inbuf=inbuf)
if 'poll' not in valid_dict or not valid_dict['poll']:
# Don't print here if it's not a polling command
break
if ret:
ret = abs(ret)
print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
file=sys.stderr)
break
if outbuf:
print(outbuf.decode('utf-8'))
if outs:
print(outs, file=sys.stderr)
if parsed_args.period <= 0:
break
sleep(parsed_args.period)

return ret, outbuf, outs


def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
"""
Do new-style command dance.
Expand All @@ -531,14 +571,10 @@ def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):

if True:
if cmdargs:
# Validate input args against list of sigs
valid_dict = validate_command(sigdict, cmdargs, verbose)
if valid_dict:
if parsed_args.output_format:
valid_dict['format'] = parsed_args.output_format
else:
return -errno.EINVAL, '', 'invalid command'
# Non interactive mode
ret, outbuf, outs = do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)
else:
# Interactive mode (ceph cli)
if sys.stdin.isatty():
# do the command-interpreter looping
# for input to do readline cmd editing
Expand All @@ -563,30 +599,9 @@ def new_style_command(parsed_args, cmdargs, target, sigdict, inbuf, verbose):
print('Can not use \'tell\' in interactive mode.',
file=sys.stderr)
continue
valid_dict = validate_command(sigdict, cmdargs, verbose)
if valid_dict:
if parsed_args.output_format:
valid_dict['format'] = parsed_args.output_format
if verbose:
print("Submitting command: ", valid_dict, file=sys.stderr)
ret, outbuf, outs = json_command(cluster_handle,
target=target,
argdict=valid_dict)
if ret:
ret = abs(ret)
print('Error: {0} {1}'.format(ret, errno.errorcode.get(ret, 'Unknown')),
file=sys.stderr)
if outbuf:
print(outbuf)
if outs:
print('Status:\n', outs, file=sys.stderr)
else:
print("Invalid command", file=sys.stderr)
do_command(parsed_args, target, cmdargs, sigdict, inbuf, verbose)

if verbose:
print("Submitting command: ", valid_dict, file=sys.stderr)
return json_command(cluster_handle, target=target, argdict=valid_dict,
inbuf=inbuf)
return ret, outbuf, outs


def complete(sigdict, args, target):
Expand Down
8 changes: 8 additions & 0 deletions src/mgr/ActivePyModules.cc
Expand Up @@ -282,6 +282,14 @@ PyObject *ActivePyModules::get_python(const std::string &what)
}
);
return f.get();
} else if (what == "io_rate") {
PyFormatter f;
cluster_state.with_pgmap(
[&f](const PGMap &pg_map) {
pg_map.dump_delta(&f);
}
);
return f.get();
} else if (what == "df") {
PyFormatter f;

Expand Down
10 changes: 10 additions & 0 deletions src/mgr/PyModule.cc
Expand Up @@ -26,6 +26,7 @@

// Courtesy of http://stackoverflow.com/questions/1418015/how-to-get-python-exception-text
#include <boost/python.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include "include/assert.h" // boost clobbers this
// decode a Python exception into a string
std::string handle_pyerror()
Expand Down Expand Up @@ -384,6 +385,15 @@ int PyModule::load_commands()
assert(pPerm != nullptr);
item.perm = PyString_AsString(pPerm);

item.polling = false;
PyObject *pPoll = PyDict_GetItemString(command, "poll");
if (pPoll) {
std::string polling = PyString_AsString(pPoll);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could just use boost::iequals(polling, "true")

if (boost::iequals(polling, "true")) {
item.polling = true;
}
}

item.module_name = module_name;

commands.push_back(item);
Expand Down
1 change: 1 addition & 0 deletions src/mgr/PyModule.h
Expand Up @@ -30,6 +30,7 @@ class ModuleCommand {
std::string cmdstring;
std::string helpstring;
std::string perm;
bool polling;

// Call the ActivePyModule of this name to handle the command
std::string module_name;
Expand Down
6 changes: 5 additions & 1 deletion src/mgr/PyModuleRegistry.cc
Expand Up @@ -316,8 +316,12 @@ std::vector<MonCommand> PyModuleRegistry::get_commands() const
std::vector<ModuleCommand> commands = get_py_commands();
std::vector<MonCommand> result;
for (auto &pyc: commands) {
uint64_t flags = MonCommand::FLAG_MGR;
if (pyc.polling) {
flags |= MonCommand::FLAG_POLL;
}
result.push_back({pyc.cmdstring, pyc.helpstring, "mgr",
pyc.perm, "cli", MonCommand::FLAG_MGR});
pyc.perm, "cli", flags});
}
return result;
}
Expand Down
1 change: 1 addition & 0 deletions src/mon/MonCommand.h
Expand Up @@ -30,6 +30,7 @@ struct MonCommand {
static const uint64_t FLAG_OBSOLETE = 1 << 1;
static const uint64_t FLAG_DEPRECATED = 1 << 2;
static const uint64_t FLAG_MGR = 1 << 3;
static const uint64_t FLAG_POLL = 1 << 4;

bool has_flag(uint64_t flag) const { return (flags & flag) != 0; }
void set_flag(uint64_t flag) { flags |= flag; }
Expand Down
2 changes: 2 additions & 0 deletions src/mon/MonCommands.h
Expand Up @@ -112,6 +112,8 @@
* OBSOLETE - command is considered obsolete
* DEPRECATED - command is considered deprecated
* MGR - command goes to ceph-mgr (for luminous+)
* POLL - command is intended to be called periodically by the
* client (see iostat)
*
* A command should always be first considered DEPRECATED before being
* considered OBSOLETE, giving due consideration to users and conforming
Expand Down
1 change: 1 addition & 0 deletions src/mon/PGMap.cc
Expand Up @@ -1395,6 +1395,7 @@ void PGMap::dump_delta(Formatter *f) const
{
f->open_object_section("pg_stats_delta");
pg_sum_delta.dump(f);
f->dump_stream("stamp_delta") << stamp_delta;
f->close_section();
}

Expand Down
9 changes: 6 additions & 3 deletions src/pybind/ceph_argparse.py
Expand Up @@ -23,8 +23,9 @@
import uuid


# Flags are from MonCommand.h
FLAG_MGR = 8 # command is intended for mgr

FLAG_POLL = 16 # command is intended to be ran continuously by the client

try:
basestring
Expand Down Expand Up @@ -999,6 +1000,9 @@ def validate(args, signature, flags=0, partial=False):
if flags & FLAG_MGR:
d['target'] = ('mgr','')

if flags & FLAG_POLL:
d['poll'] = True
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tchaikov The problem was that ceph.in looks at len(valid_dict) (ie the number of parameters) to decide whether it's a pg command or not. See here. Setting valid_dict['poll'] = False (previously done here) broke that logic. The fix is to keep valid_dict['poll'] unset, unless the command server-side expects it. This is OK because the length of valid_dict isn't used for the plugin. There's already a check that 'poll' in valid_dict whenever we want to access it, so having it unset is not an issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, thanks for the explanation! actually, i was debugging this yesterday.


# Finally, success
return d

Expand Down Expand Up @@ -1239,7 +1243,7 @@ def send_command(cluster, target=('mon', ''), cmd=None, inbuf=b'', timeout=0,
verbose=False):
"""
Send a command to a daemon using librados's
mon_command, osd_command, or pg_command. Any bulk input data
mon_command, osd_command, mgr_command, or pg_command. Any bulk input data
comes in inbuf.

Returns (ret, outbuf, outs); ret is the return code, outbuf is
Expand Down Expand Up @@ -1351,7 +1355,6 @@ def json_command(cluster, target=('mon', ''), prefix=None, argdict=None,
except:
# use the target we were originally given
pass

ret, outbuf, outs = send_command_retry(cluster,
target, [json.dumps(cmddict)],
inbuf, timeout, verbose)
Expand Down
1 change: 1 addition & 0 deletions src/pybind/mgr/iostat/__init__.py
@@ -0,0 +1 @@
from .module import Module
53 changes: 53 additions & 0 deletions src/pybind/mgr/iostat/module.py
@@ -0,0 +1,53 @@

from mgr_module import MgrModule


class Module(MgrModule):
COMMANDS = [
{
"cmd": "iostat",
"desc": "Get IO rates",
"perm": "r",
"poll": "true"
},
{
"cmd": "iostat self-test",
"desc": "Run a self test the iostat module",
"perm": "r"
}
]


def __init__(self, *args, **kwargs):
super(Module, self).__init__(*args, **kwargs)


def handle_command(self, command):
rd = 0
wr = 0
ops = 0
ret = ''

if command['prefix'] == 'iostat':
r = self.get('io_rate')

stamp_delta = float(r['pg_stats_delta']['stamp_delta'])
if (stamp_delta > 0):
rd = int(r['pg_stats_delta']['stat_sum']['num_read_kb']) / stamp_delta
wr = int(r['pg_stats_delta']['stat_sum']['num_write_kb']) / stamp_delta
ops = ( int(r['pg_stats_delta']['stat_sum']['num_write']) + int(r['pg_stats_delta']['stat_sum']['num_read']) ) / stamp_delta

ret = "wr: {0} kB/s, rd: {1} kB/s, iops: {2}".format(int(wr), int(rd), int(ops))

elif command['prefix'] == 'iostat self-test':
r = self.get('io_rate')
assert('pg_stats_delta' in r)
assert('stamp_delta' in r['pg_stats_delta'])
assert('stat_sum' in r['pg_stats_delta'])
assert('num_read_kb' in r['pg_stats_delta']['stat_sum'])
assert('num_write_kb' in r['pg_stats_delta']['stat_sum'])
assert('num_write' in r['pg_stats_delta']['stat_sum'])
assert('num_read' in r['pg_stats_delta']['stat_sum'])
ret = 'iostat self-test OK'

return 0, '', ret
2 changes: 1 addition & 1 deletion src/pybind/mgr/mgr_module.py
Expand Up @@ -314,7 +314,7 @@ def get(self, data_name):

:param str data_name: Valid things to fetch are osd_crush_map_text,
osd_map, osd_map_tree, osd_map_crush, config, mon_map, fs_map,
osd_metadata, pg_summary, df, osd_stats, health, mon_status.
osd_metadata, pg_summary, io_rate, pg_dump, df, osd_stats, health, mon_status.

Note:
All these structures have their own JSON representations: experiment
Expand Down