Giuseppe Lavagetto has uploaded a new change for review.
https://gerrit.wikimedia.org/r/215891
Change subject: conftool: adding the cli-tool
......................................................................
conftool: adding the cli-tool
Change-Id: I87147db34746d9f9b02cc93d97fecaec69aa6912
---
M conftool/__init__.py
A conftool/action.py
M conftool/cli/syncer.py
A conftool/cli/tool.py
M conftool/node.py
5 files changed, 154 insertions(+), 2 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/operations/software/conftool
refs/changes/91/215891/1
diff --git a/conftool/__init__.py b/conftool/__init__.py
index 1ba4c8a..36aea6d 100644
--- a/conftool/__init__.py
+++ b/conftool/__init__.py
@@ -1,5 +1,6 @@
import os
import logging
+import json
from conftool import backend
from conftool import drivers
@@ -22,6 +23,10 @@
def key(self):
raise NotImplementedError("All kvstore objects should implement this")
+ @property
+ def name(self):
+ return os.path.basename(self.key)
+
def get_default(self, what):
raise NotImplementedError("All kvstore objects should implement this.")
@@ -43,6 +48,23 @@
def delete(self):
self.backend.driver.delete(self.key)
+ @classmethod
+ def get_tags(cls, taglist):
+ tuplestrip = lambda tup: tuple(map(lambda x: x.strip(), tup))
+ tagdict = dict([tuplestrip(el.split('=')) for el in taglist])
+ # will raise a KeyError if not all tags are matched
+ return [tagdict[t] for t in cls._tags]
+
+ def update(self, values):
+ """
+ Update values of properties in the schema
+ """
+ for k, v in values.items():
+ if k not in self._schema:
+ continue
+ self._set_value(k, self._schema[k], {k: v}, set_defaults=False)
+ self.write()
+
def _from_net(self, values):
"""
Fetch the values from the kvstore into the object
@@ -59,9 +81,14 @@
values[key] = self.get_default(key)
return values
- def _set_value(self, key, validator, values):
+ def _set_value(self, key, validator, values, set_defaults=True):
try:
setattr(self, key, validator(values[key]))
except Exception as e:
# TODO: log validation error
- setattr(self, key, self.get_default(key))
+ if set_defaults:
+ setattr(self, key, self.get_default(key))
+
+ def __str__(self):
+ d = {self.name: self._to_net()}
+ return json.dumps(d)
diff --git a/conftool/action.py b/conftool/action.py
new file mode 100644
index 0000000..2f73aa8
--- /dev/null
+++ b/conftool/action.py
@@ -0,0 +1,50 @@
+config = {}
+backend = None
+
+
+class ActionError(Exception):
+ pass
+
+
+class Action(object):
+
+ def __init__(self, obj, act):
+ self.action, self.args = self._parse_action(act)
+ self.entity = obj
+ self.description = ""
+
+ def _parse_action(self, act):
+ if act.startswith('get'):
+ return ('get', None)
+ elif act.startswith('delete'):
+ return ('delete', None)
+ elif not act.startswith('set/'):
+ raise ActionError("Cannot parse action %s" % act)
+ set_arg = act.strip('set/')
+ try:
+ values = dict((el.strip().split('=')) for el in set_arg.split(':'))
+ except Exception as e:
+ raise ActionError("Could not parse set instructions: %s" % set_arg)
+ return ('set', values)
+
+ def run(self):
+ if self.action == 'get':
+ self.entity.fetch()
+ if self.entity.exists:
+ return str(self.entity)
+ else:
+ return "%s not found" % self.entity.name
+ elif self.action == 'delete':
+ self.entity.delete()
+ entity_type = self.entity.__class__.__name__,
+ return "Deleted %s %s." % (entity_type,
+ self.entity.name)
+ else:
+ desc = []
+ for (k, v) in self.args.items():
+ msg = "%s: %s changed %s => %s" % (
+ self.entity.name, k,
+ getattr(self.entity, k), v)
+ desc.append(msg)
+ self.entity.update(self.args)
+ return "\n".join(desc)
diff --git a/conftool/cli/syncer.py b/conftool/cli/syncer.py
index 73c797d..6f030ab 100644
--- a/conftool/cli/syncer.py
+++ b/conftool/cli/syncer.py
@@ -10,6 +10,7 @@
import functools
import logging
+
# Generic exception handling decorator
def catch_and_log(log_msg):
def actual_wrapper(fn):
diff --git a/conftool/cli/tool.py b/conftool/cli/tool.py
new file mode 100644
index 0000000..8a2a9d9
--- /dev/null
+++ b/conftool/cli/tool.py
@@ -0,0 +1,73 @@
+# Conftool cli module
+#
+import argparse
+import sys
+import logging
+import json
+from conftool import configuration, action, _log, KVObject
+from conftool.drivers import BackendError
+# TODO: auto import these somehow
+from conftool import service, node
+
+object_types = {"node": node.Node, "service": service.Service}
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Tool to interact with the WMF config store",
+ epilog="More details at"
+ " <https://wikitech.wikimedia.org/wiki/conftool>.",
+ fromfile_prefix_chars='@')
+ parser.add_argument('--config', nargs=1, help="Optional config file",
+ default="/etc/conftool/config")
+ parser.add_argument('--tags',
+ help="List of comma-separated tags; they need to "
+ "match the base tags of the object type you chose.",
+ required=True)
+ parser.add_argument('--object-type', dest="object_type",
+ choices=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", nargs=2,
+ required=True)
+ args = parser.parse_args()
+ logging.basicConfig(level=logging.WARN)
+ try:
+ c = configuration.get(args.config)
+ KVObject.setup(c)
+ except Exception as e:
+ _log.critical("Invalid configuration: %s", e)
+ sys.exit(1)
+
+ cls = object_types[args.object_type]
+ try:
+ tags = cls.get_tags(args.tags.split(','))
+ except KeyError as e:
+ _log.critical("Invalid tag list %s - reason: %s", args.tags, e)
+ sys.exit(1)
+
+ for unit in args.action:
+ try:
+ act, name = unit
+ if act == 'get' and name == "all":
+ cur_dir = cls.dir(*tags)
+ print json.dumps(dict(KVObject.backend.driver.ls(cur_dir)))
+ return
+ # Oh python I <3 you...
+ arguments = list(tags)
+ arguments.append(name)
+ obj = cls(*arguments)
+ a = action.Action(obj, act)
+ msg = a.run()
+ except action.ActionError as e:
+ _log.error("Invalid action, reason: %s", str(e))
+ except BackendError as e:
+ _log.error("Failure writing to the kvstore: %s", str(e))
+ except Exception as e:
+ raise
+ _log.error("Generic action failure: %s", str(e))
+ else:
+ print(msg)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/conftool/node.py b/conftool/node.py
index 9262a84..4dfedd9 100644
--- a/conftool/node.py
+++ b/conftool/node.py
@@ -13,6 +13,7 @@
class Node(KVObject):
_schema = {'weight': int, 'pooled': choice("yes", "no", "inactive")}
+ _tags = ['dc', 'cluster', 'service']
def __init__(self, datacenter, cluster, servname, host):
self.base_path = self.config.pools_path
--
To view, visit https://gerrit.wikimedia.org/r/215891
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I87147db34746d9f9b02cc93d97fecaec69aa6912
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