Giuseppe Lavagetto has uploaded a new change for review.
https://gerrit.wikimedia.org/r/278552
Change subject: Add select mode, refactor conftool.cli.tool
......................................................................
Add select mode, refactor conftool.cli.tool
Bug: T128199
Change-Id: I2ee04cd3e8eedf8d3f985b85099932f1d48d175b
---
M conftool/__init__.py
M conftool/cli/tool.py
M conftool/drivers/etcd.py
M conftool/tests/integration/test_tool.py
M conftool/tests/unit/test_cli_tool.py
5 files changed, 203 insertions(+), 67 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/operations/software/conftool
refs/changes/52/278552/1
diff --git a/conftool/__init__.py b/conftool/__init__.py
index 19e1b26..5da9615 100644
--- a/conftool/__init__.py
+++ b/conftool/__init__.py
@@ -51,7 +51,8 @@
def tags(self):
"""Returns a dict of the current tags"""
res = {}
- # The current key, minus the basepath, is the list of tags + the node
name
+ # The current key, minus the basepath, is the list of tags +
+ # the node name
tags = self.key.replace(self.base_path(), '').split('/')[1:-1]
for i in range(len(self._tags)):
res[self._tags[i]] = tags[i]
diff --git a/conftool/cli/tool.py b/conftool/cli/tool.py
index b802955..d784b68 100644
--- a/conftool/cli/tool.py
+++ b/conftool/cli/tool.py
@@ -1,14 +1,17 @@
# Conftool cli module
#
import argparse
-import sys
+from collections import defaultdict
import logging
import json
+import os
+import re
+import sys
+
from conftool import configuration, action, _log, KVObject
from conftool.drivers import BackendError
# TODO: auto import these somehow
from conftool import service, node
-import re
class ToolCli(object):
@@ -16,11 +19,7 @@
def __init__(self, args):
self.args = args
- if self.args.tags:
- self._tags = self.args.tags.split(',')
- elif not self.args.find:
- _log.critical("Either tags or find should be provided")
- sys.exit(1)
+ self._tags = self.args.taglist.split(',')
self.entity = self.object_types[args.object_type]
def setup(self):
@@ -29,26 +28,22 @@
@property
def tags(self):
- if self.args.find:
- return []
- else:
- try:
- return self.entity.parse_tags(self._tags)
- except KeyError as e:
- _log.critical(
- "Invalid tag list %s - we're missing tag: %s",
- self.args.tags, e)
- sys.exit(1)
+ try:
+ return self.entity.parse_tags(self._tags)
+ except KeyError as e:
+ _log.critical(
+ "Invalid tag list %s - we're missing tag: %s",
+ self.args.taglist, e)
+ sys.exit(1)
+ except ValueError:
+ _log.critical("Invalid tag list %s", self.args.taglist)
+ sys.exit(1)
def host_list(self):
- if self.args.find:
- for o in self.entity.find(self._namedef):
- yield o
- else:
- for objname in self._tagged_host_list():
- arguments = list(self.tags)
- arguments.append(objname)
- yield self.entity(*arguments)
+ for objname in self._tagged_host_list():
+ arguments = list(self.tags)
+ arguments.append(objname)
+ yield self.entity(*arguments)
def _tagged_host_list(self):
cur_dir = self.entity.dir(*self.tags)
@@ -78,20 +73,24 @@
ToolCli.raise_warning()
return retval
- def run_action(self, act, namedef):
- self._action = act
- self._namedef = namedef
+ def run_action(self, unit):
+ self._action, self._namedef = unit
+ return self._run_action()
+
+ def _run_action(self):
for obj in self.host_list():
try:
- a = action.Action(obj, act)
+ a = action.Action(obj, self._action)
msg = a.run()
except action.ActionError as e:
_log.error("Invalid action, reason: %s", str(e))
except BackendError as e:
- _log.error("Error when trying to %s on %s", act, namedef)
+ _log.error("Error when trying to %s on %s", self._action,
+ self._namedef)
_log.error("Failure writing to the kvstore: %s", str(e))
except Exception as e:
- _log.error("Error when trying to %s on %s", act, namedef)
+ _log.error("Error when trying to %s on %s", self._action,
+ self._namedef)
_log.exception("Generic action failure: %s", str(e))
else:
print(msg)
@@ -113,11 +112,95 @@
sys.exit(1)
-def main(cmdline=None):
- if cmdline is None:
- cmdline = list(sys.argv)
- cmdline.pop(0)
+class ToolCliFind(ToolCli):
+ """Subclass used for the --find mode"""
+ def __init__(self, args):
+ self.args = args
+ self.entity = self.object_types[args.object_type]
+ @property
+ def tags(self):
+ return []
+
+ def host_list(self):
+ for o in self.entity.find(self._namedef):
+ yield o
+
+
+class ToolCliByLabel(ToolCli):
+ """Subclass used for the select mode"""
+ def __init__(self, args):
+ self.args = args
+ self.selectors = {}
+ self.parse_selectors()
+ self.entity = self.object_types[args.object_type]
+
+ def parse_selectors(self):
+ for tag in self.args.selector.split(','):
+ k, expr = tag.split('=', 1)
+ self.selectors[k] = re.compile(expr, re.I)
+
+ @property
+ def tags(self):
+ return []
+
+ def get_matching(self, paths, tag):
+ newpaths = []
+ try:
+ regex = self.selectors[tag]
+ except:
+ regex = re.compile('.*')
+
+ for path in paths:
+ for name, _ in KVObject.backend.driver.ls(path):
+ if regex.match(name):
+ newpaths.append(os.path.join(path, name))
+ return newpaths
+
+ def host_list(self):
+ """Gets all the hosts matching our selectors"""
+ places = [self.entity.base_path()]
+ for tag in self.entity._tags:
+ # Find all the paths matching at this level
+ places = self.get_matching(places, tag)
+
+ try:
+ regex = self.selectors['name']
+ except:
+ regex = re.compile('.*')
+
+ objects = []
+ for path in places:
+ for name, entity in KVObject.backend.driver.ls(path):
+ if regex.match(name):
+ objects.append(entity)
+
+ # Any selector that includes multiple objects will show a list of
+ # host that have been selected
+ if self._action != 'get' and len(objects) > 1:
+ self._raise_warning(objects)
+ return objects
+
+ def run_action(self, unit):
+ self._action = unit
+ self._namedef = self.args.selector
+ return self._run_action()
+
+ def raise_warning(self, objects):
+ tag_hosts = defaultdict(list)
+ for obj in objects:
+ dir = os.path.dirname(obj.key).replace(self.entity.base_path(), '')
+ tag_hosts[dir].append(obj.name)
+ print "The selector you chose has selected the following objects:"
+ print json.dumps(tag_hosts)
+ print "Ok to continue? [y/N]"
+ a = raw_input("confctl>")
+ if a.lower() != 'y':
+ print "Aborting"
+ sys.exit(1)
+
+
+def parse_args(cmdline):
parser = argparse.ArgumentParser(
description="Tool to interact with the WMF config store",
epilog="More details at"
@@ -125,29 +208,71 @@
fromfile_prefix_chars='@')
parser.add_argument('--config', help="Optional config file",
default="/etc/conftool/config.yaml")
- parser.add_argument('--tags',
- help="List of comma-separated tags; they need to "
- "match the base tags of the object type you chose.",
- required=False, default=[])
- parser.add_argument('--find', help="Find all instances of the node",
- required=False, default=False, action='store_true')
parser.add_argument('--object-type', dest="object_type",
choices=ToolCli.object_types.keys(), default='node')
- parser.add_argument('--action', action="append", metavar="ACTIONS",
- help="the action to take: "
- " [set/k1=v1:k2=v2...|get|delete]"
- " node|all|re:<regex>|find:node", nargs=2,
- required=True)
parser.add_argument('--debug', action="store_true",
default=False, help="print debug info")
- args = parser.parse_args(cmdline)
+
+ # Subparsers for the three operating models
+ subparsers = parser.add_subparsers(
+ help='Program mode: tags, find or select', dest='mode')
+
+ # Tags mode
+ tags = subparsers.add_parser(
+ 'tags',
+ help="Select a specific service via full list of tags")
+ tags.add_argument(
+ 'taglist',
+ help="List of comma-separated tags; they need to "
+ "match the base tags of the object type you chose.")
+
+ find = subparsers.add_parser('find',
+ help="Find all instances of the node by name")
+ for subparser in [tags, find]:
+ subparser.add_argument('--action', action="append", metavar="ACTIONS",
+ help="the action to take: "
+ " [set/k1=v1:k2=v2...|get|delete]"
+ " node|all|re:<regex>|find:node", nargs=2)
+ select = subparsers.add_parser('select',
+ help="Select nodes via tag selectors")
+ select.add_argument(
+ 'selector',
+ help="Label selector in the form tag=regex: "
+ "dc=eqiad,cluster=cache_.*,service=nginx,name=.*.eqiad.wmnet")
+ select.add_argument('action', action="append", metavar="ACTIONS",
+ help="the action to take: "
+ " [set/k1=v1:k2=v2...|get|delete]")
+
+ return parser.parse_args(cmdline)
+
+
+def mangle_argv():
+ """Basic mangling of the command line arguments"""
+ cmdline = list(sys.argv)
+ cmdline.pop(0)
+ # Backwards compatibility. Ugly but passable
+ for i, arg in enumerate(cmdline):
+ if arg in ['--tags', '--find']:
+ cmdline[i] = arg.replace('--', '')
+ return cmdline
+
+
+def main(cmdline=None):
+ if cmdline is None:
+ cmdline = mangle_argv()
+ args = parse_args(cmdline)
if args.debug:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.WARN)
- cli = ToolCli(args)
+ if args.mode == 'select':
+ cli = ToolCliByLabel(args)
+ elif args.mode == 'find':
+ cli = ToolCliFind(args)
+ else:
+ cli = ToolCli(args)
try:
cli.setup()
@@ -156,8 +281,8 @@
sys.exit(1)
for unit in args.action:
- act, name_def = unit
- cli.run_action(act, name_def)
+ # TODO: fix base class
+ cli.run_action(unit)
if __name__ == '__main__':
diff --git a/conftool/drivers/etcd.py b/conftool/drivers/etcd.py
index e439bfc..8bb10b0 100644
--- a/conftool/drivers/etcd.py
+++ b/conftool/drivers/etcd.py
@@ -68,6 +68,7 @@
@drivers.wrap_exception(etcd.EtcdException)
def ls(self, path):
+ """Given a path, returns a tuple (key, data) for each value found"""
key = self.abspath(path)
try:
res = self.client.read(key)
diff --git a/conftool/tests/integration/test_tool.py
b/conftool/tests/integration/test_tool.py
index f79c526..a078a5b 100644
--- a/conftool/tests/integration/test_tool.py
+++ b/conftool/tests/integration/test_tool.py
@@ -26,7 +26,7 @@
syncer.main(arguments=args)
def generate_args(self, *actions):
- args = ['--tags', "dc=eqiad,cluster=cache_text,service=https"]
+ args = ['tags', "dc=eqiad,cluster=cache_text,service=https"]
for action in actions:
args.append('--action')
args.extend(action.split())
@@ -42,7 +42,7 @@
self.assertEquals(output['cp1008']['pooled'], 'no')
def test_find_node(self):
- args = ['--find', '--action', 'get', 'cp1008']
+ args = ['find', '--action', 'get', 'cp1008']
with captured_output() as (out, err):
tool.main(cmdline=args)
res = out.getvalue().strip()
diff --git a/conftool/tests/unit/test_cli_tool.py
b/conftool/tests/unit/test_cli_tool.py
index 2821879..0e11eec 100644
--- a/conftool/tests/unit/test_cli_tool.py
+++ b/conftool/tests/unit/test_cli_tool.py
@@ -18,49 +18,50 @@
def _mock_args(self, **kw):
arg = mock.MagicMock()
arg.object_type = 'node'
- arg.find = False
+ arg.mode = 'tags'
for prop, value in kw.items():
setattr(arg, prop, value)
return arg
def test_init(self):
"""Test case for `conftool.cli.tool.ToolCli.__init__`"""
- args = self._mock_args(find=False, tags="")
+ args = self._mock_args(taglist="")
with self.assertRaises(SystemExit) as cm:
t = tool.ToolCli(args)
+ t.tags
self.assertEquals(cm.exception.code, 1)
- args = self._mock_args(tags="a=b,b=c,d=2")
+ args = self._mock_args(taglist="a=b,b=c,d=2")
t = tool.ToolCli(args)
- self.assertEquals(t.args.find, False)
+ self.assertEquals(t.args.mode, 'tags')
self.assertItemsEqual(t._tags, ['a=b', 'b=c', 'd=2'])
def test_tags(self):
- args = self._mock_args(tags="dc=a,cluster=b,service=apache2")
+ args = self._mock_args(taglist="dc=a,cluster=b,service=apache2")
t = tool.ToolCli(args)
self.assertItemsEqual(t.tags, ['a', 'b', 'apache2'])
- args = self._mock_args(find=True)
- t = tool.ToolCli(args)
+ args = self._mock_args(mode='find')
+ t = tool.ToolCliFind(args)
self.assertEquals(t.tags, [])
args = self._mock_args(
- find=True,
+ mode='find',
object_type='node',
)
- t = tool.ToolCli(args)
+ t = tool.ToolCliFind(args)
self.assertEquals(t.entity.__name__, 'Node')
def test_host_list_find(self):
args = self._mock_args(
- find=True,
+ mode="find",
object_type='node',
)
- t = tool.ToolCli(args)
+ t = tool.ToolCliFind(args)
t._namedef = 'cp1048.example.com'
res = [
- node.Node('mycloset', 'cache_unicorns',
+ node.Node('test', 'cache',
'ssl', 'cp1048.example.com'),
- node.Node('mycloset', 'cache_unicorns',
+ node.Node('test', 'cache',
'http', 'cp1048.example.com'),
]
t.entity.find = mock.MagicMock(return_value=res)
@@ -76,7 +77,7 @@
('cp1014.local', {'pooled': 'no'})
]
self._mock_list(host_dir)
- args = self._mock_args(tags="dc=a,cluster=b,service=apache2")
+ args = self._mock_args(taglist="dc=a,cluster=b,service=apache2")
def tagged(args, name, act):
t = tool.ToolCli(args)
@@ -107,3 +108,11 @@
# Majority of nodes via a regex will raise a system exit
with self.assertRaises(SystemExit):
tagged(args, 're:cp10(11|20)\.example\.com', 'set')
+
+ def test_parse_args(self):
+ # Taglist
+ cmdline = ['tags', 'dc=a,cluster=b', '--action', 'get', 'all']
+ args = tool.parse_args(cmdline)
+ self.assertEquals(args.mode, 'tags')
+ self.assertEquals(args.taglist, 'dc=a,cluster=b')
+ self.assertEquals(args.action, [['get', 'all']])
--
To view, visit https://gerrit.wikimedia.org/r/278552
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I2ee04cd3e8eedf8d3f985b85099932f1d48d175b
Gerrit-PatchSet: 1
Gerrit-Project: operations/software/conftool
Gerrit-Branch: master
Gerrit-Owner: Giuseppe Lavagetto <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits