diff --git a/README.md b/README.md new file mode 100644 index 0000000..ede3449 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# kubesql + +kubesql is a tool to use sql to query the resources of kubernetes. + +The resources of kubernetes such as nodes, pods and so on are handled as the + +For example, all pods are easily to list from apiserver. But the number of pods on each node is not easy to caculate. + +With kubesql, a sql statement can achieve it like this. + +``` +[root@localhost kubesql]# kubesql "select hostIp, count(*) from pods group by hostIp" ++----------+----------------+ +| count(*) | hostIP | ++----------+----------------+ +| 9 | None | +| 4 | 22.2.22.222 | +| 14 | 11.1.111.11 | ++----------+----------------+ +``` + +How many pod are pending + +``` +[root@localhost kubesql]# kubesql "select count(*) from pods where phase = 'Pending'" ++----------+ +| count(*) | ++----------+ +| 29 | ++----------+ +``` + + +# compoments + +kubesql has three compoments. + +- kubesql-watch: Watch the events from kube-apiserver, and write it to sqlite3. +- kubesql-server: Provide a http api for query. Accepts the sql query , execute the query in sqlite3 and return the query result. +- kubesql-client: Send the query sql to kubesql-server and get the result, then print the result in table format. + +``` ++----------------+ watch +---------------+ +---------+ +| kube-apiserver | -------> | kubesql-watch | --> | sqlite3 | ++----------------+ +---------------+ +---------+ + ^ + | + | ++----------------+ http +---------------+ | +| kubesql-client | -------> | kubsql-server | ------+ ++----------------+ +---------------+ +``` + +# install and deploy + +## manualy install and deploy + +install + +``` +//check out the code +pip install requirements.txt +python setup.py install +cp -r etc/kubesql /etc +``` + +check the config of `/etc/kubesql/config`, and modify the kubeconfig. kubeconfig is for kubesql-watch to connect to the apiserver. + +``` +nohup kubesql-watch & +nohup kubesql-server & +``` + + +# Usage + +kubesql command is short for kubesql-client. It is used to send the query and show the result in table. + +``` +[root@localhost kubesql]# kubesql -h +usage: kubesql [-h] [-t TABLE] [-a] [sql] + +positional arguments: + sql execte the sql. + +optional arguments: + -h, --help show this help message and exit + -t TABLE, --table TABLE + increase output verbosity + -a, --all show all tables +``` + +`kubesql -a` can list the tables currently supported. + +``` +[root@localhost kubesql]# kubesql -a ++------------+ +| table_name | ++------------+ +| pods | +| nodes | ++------------+ +``` + +And `kubesql -t {table_name}` can list the columns for `table_name` currently supported. + +``` +[root@localhost kubesql]# kubesql -t nodes ++-------------------------+-----+------------+---------+----+-----------+ +| name | cid | dflt_value | notnull | pk | type | ++-------------------------+-----+------------+---------+----+-----------+ +| name | 0 | None | 0 | 0 | char(200) | +| uid | 1 | None | 0 | 0 | char(200) | +| creationTimestamp | 2 | None | 0 | 0 | datetime | +| deletionTimestamp | 3 | None | 0 | 0 | datetime | +| zone | 4 | None | 0 | 0 | char(200) | +| allocatable_cpu | 5 | None | 0 | 0 | char(200) | +| allocatable_memory | 6 | None | 0 | 0 | char(200) | +| allocatable_pods | 7 | None | 0 | 0 | char(200) | +| capacity_cpu | 8 | None | 0 | 0 | char(200) | +| capacity_memory | 9 | None | 0 | 0 | char(200) | +| capacity_pods | 10 | None | 0 | 0 | char(200) | +| architecture | 11 | None | 0 | 0 | char(200) | +| containerRuntimeVersion | 12 | None | 0 | 0 | char(200) | +| kubeProxyVersion | 13 | None | 0 | 0 | char(200) | +| kubeletVersion | 14 | None | 0 | 0 | char(200) | +| operatingSystem | 15 | None | 0 | 0 | char(200) | +| osImage | 16 | None | 0 | 0 | char(200) | ++-------------------------+-----+------------+---------+----+-----------+ +``` diff --git a/etc/kubesql/config b/etc/kubesql/config new file mode 100644 index 0000000..ae84b64 --- /dev/null +++ b/etc/kubesql/config @@ -0,0 +1,6 @@ +[kubesql] +port=415 +ip=127.0.0.1 +db_path=/dev/shm/kubesql.db +param_path=/etc/kubesql/params +kubeconfig_path=/etc/kubeconfig diff --git a/etc/kubesql/params b/etc/kubesql/params new file mode 100644 index 0000000..f46f46c --- /dev/null +++ b/etc/kubesql/params @@ -0,0 +1,32 @@ +--- +pod +metadata.name +metadata.uid +metadata.namespace +metadata.creationTimestamp +metadata.deletionTimestamp +spec.nodeName +spec.schedulerName +status.hostIP +status.phase +status.podIP +status.reason +status.startTime +--- +node +metadata.name +metadata.uid +metadata.creationTimestamp +metadata.deletionTimestamp +status.allocatable.cpu allocatable_cpu +status.allocatable.memory allocatable_memory +status.allocatable.pods allocatable_pods +status.capacity.cpu capacity_cpu +status.capacity.memory capacity_memory +status.capacity.pods capacity_pods +status.nodeInfo.architecture +status.nodeInfo.containerRuntimeVersion +status.nodeInfo.kubeProxyVersion +status.nodeInfo.kubeletVersion +status.nodeInfo.operatingSystem +status.nodeInfo.osImage \ No newline at end of file diff --git a/kubesql/__init__.py b/kubesql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubesql/client.py b/kubesql/client.py new file mode 100644 index 0000000..79b117f --- /dev/null +++ b/kubesql/client.py @@ -0,0 +1,56 @@ +import prettytable as pt +import argparse +import httplib +import json +from kubesql import utils + +cfg = utils.load_config() + +def get_kube_sql(sql): + conn = httplib.HTTPConnection("%s:%s" % (cfg.get("ip"), cfg.get("port"))) + params = {'sql': sql} + headers = {'Content-type': 'application/json'} + conn.request("POST", "/sql", headers=headers, body=json.dumps(params)) + try: + response = conn.getresponse() + # print params, response.status, response.reason + data = response.read() + return json.loads(data) + except Exception, e: + print e, params + conn.close() + + +def print_json_as_table(result): + if result: + tb = pt.PrettyTable() + row = result[0] + tb.field_names = row.keys() + for row in result: + row_value = [] + for field_name in tb.field_names: + row_value.append(row.get(field_name)) + tb.add_row(row_value) + tb.align = "l" + print(tb) + + +parser = argparse.ArgumentParser() +parser.add_argument("sql", nargs="?", type=str, help="execte the sql.") +parser.add_argument("-t", "--table", help="increase output verbosity") +parser.add_argument("-a", "--all", action='store_true', help="show all tables") +args = parser.parse_args() + + +def main(): + if args.sql: + result = get_kube_sql(args.sql) + elif args.table: + result = get_kube_sql('PRAGMA table_info(%s)' % args.table) + elif args.all: + result = get_kube_sql('SELECT name as table_name FROM sqlite_master WHERE type="table"') + print_json_as_table(result) + + +if __name__ == '__main__': + main() diff --git a/kubesql/kubewatch.py b/kubesql/kubewatch.py new file mode 100644 index 0000000..ae5d1ca --- /dev/null +++ b/kubesql/kubewatch.py @@ -0,0 +1,172 @@ +import sqlite3 +from kubernetes import client, config, watch +from multiprocessing import Process +from kubesql import utils +import re + +cfg = utils.load_config() + +conn = sqlite3.connect(cfg.get("db_path")) + + +def get_db_type(col_type): + col_type = col_type.strip() + if col_type == "str": + return "char(200)" + elif col_type == "int": + return "int" + elif col_type == "datetime": + return "datetime" + else: + print "%s can not be handled." % col_type + return None + +dict_reg = re.compile("dict\((.*),(.*)\)") +base_type_set = set(["str","int","float"]) + + +def get_col_lists(param_list, base_class_name): + col_lists = [] + for item in param_list: + class_type = base_class_name + property_name_list = [] + item_list = item.split(" ") + if item_list: + param = item_list[0] + col_name = None + if len(item_list) > 1: + col_name = item_list[1] + for s in param.split("."): + if s == "labels" or s == "annotations": + property_name_list.append({"type_name": s, "type": "property"}) + property_name_list.append({"type_name": param.split(s+".")[1], "type": "dict"}) + class_type = "str" + if col_name is None: + col_name = param.split(s+".")[1] + break + base_class = getattr(client, class_type, None) + if base_class: + for k, v in base_class.attribute_map.iteritems(): + if v == s: + class_property_name = k + property_name_list.append({"type_name":class_property_name,"type":"property"}) + class_type = base_class.swagger_types.get(class_property_name) + elif class_type in base_type_set: + pass + else: + group = dict_reg.match(class_type) + if group: + property_name_list.append({"type_name":s,"type":"dict"}) + class_type = group.groups()[1] + else: + print "error", param, class_type + if col_name is None: + col_name = s + col_lists.append({ + "col_path": property_name_list, + "col_type": class_type, + "col_dbtype": get_db_type(class_type), + "col_name": col_name, + "col_param": param + }) + return col_lists + + +def create_sql(table_name, col_lists): + col_sql = [] + for col in col_lists: + col_sql.append("%s %s" % (col.get("col_name"), col.get("col_dbtype"))) + return "create table %s (" % table_name + ",".join(col_sql) + ");" + + +def transfer_object_to_db(object, col_lists): + db_object = {} + for col in col_lists: + col_path = col.get("col_path") + col_name = col.get("col_name") + value = object + for p in col_path: + if value: + path_type = p.get("type") + type_name = p.get("type_name") + if path_type == "property": + value = getattr(value, type_name) + elif path_type == "dict": + value = value.get(type_name) + db_object[col_name] = value + return db_object + + +def insert_object_to_db(table_name, object, col_lists): + db_object = transfer_object_to_db(object, col_lists) + cols = [] + values = [] + mark = [] + for col, v in db_object.iteritems(): + cols.append(col) + mark.append("?") + values.append(v) + sql = "insert into %s (" % table_name + ",".join(cols) + ") values (" + ",".join(mark) + ")" + return sql, tuple(values) + + +def update_object_to_db(table_name, object, col_lists): + db_object = transfer_object_to_db(object, col_lists) + cols = [] + values = [] + for col, v in db_object.iteritems(): + cols.append("%s = ?" % col) + values.append(v) + values.append(object.metadata.uid) + sql = "update %s set " % table_name+ ",".join(cols) + " where uid = ?" + return sql, tuple(values) + + +def delete_object_in_db(table_name, object, col_lists): + sql = "delete from %s where uid = ?" % table_name + return sql, tuple([object.metadata.uid]) + + +def wait_for_change(list_func, col_lists, table_name): + cursor = conn.cursor() + cursor.execute("DROP TABLE IF EXISTS %s" % table_name) + cursor.execute(create_sql(table_name, col_lists)) + w = watch.Watch() + stream = w.stream(list_func) + for event in stream: + eventType = event['type'] + pod = event['object'] + if eventType == "ADDED": + sql = insert_object_to_db(table_name, pod, col_lists) + elif eventType == "MODIFIED": + sql = update_object_to_db(table_name, pod, col_lists) + elif eventType == "DELETED": + sql = delete_object_in_db(table_name, pod, col_lists) + # print sql + try: + cursor.execute(*sql) + conn.commit() + except Exception, e: + print sql + print e + cursor.close() + + +def main(): + config.load_kube_config(config_file=cfg.get("kubeconfig_path")) + param_lists = utils.load_params(cfg.get("param_path")) + process_list = [] + for p in param_lists: + resource = p.get("resource") + param_list = p.get("param_list") + watch_args = utils.get_watch_args(resource) + resource_col_lists = get_col_lists(param_list, watch_args.get("base_class_name")) + resource_process = Process(target=wait_for_change, args=(watch_args.get("list_func"), resource_col_lists, watch_args.get("table_name"))) + resource_process.start() + process_list.append(resource_process) + for p in process_list: + p.join() + + +if __name__ == '__main__': + main() diff --git a/kubesql/server.py b/kubesql/server.py new file mode 100644 index 0000000..b0960cb --- /dev/null +++ b/kubesql/server.py @@ -0,0 +1,39 @@ +import sys +from bottle import run, request, route +import json +import sqlite3 +from kubesql import utils + +cfg = utils.load_config() + +conn = sqlite3.connect(cfg.get("db_path")) + + +@route('/sql', method='POST') +def do_sql(): + data = request.json + sql = data.get('sql') + if not (sql.lower().find("select") > -1 or sql.lower().find("pragma") > -1): + return json.dumps([{"error":"sql must contains `select`."}]) + cursor = conn.cursor() + cursor.execute(sql) + values = cursor.fetchall() + result = [] + for v in values: + v_dict = {} + i = 0 + for tuple in cursor.description: + col_nam = tuple[0] + v_dict[col_nam] = v[i] + i += 1 + result.append(v_dict) + cursor.close() + return json.dumps(result) + + +def main(): + run(host='0.0.0.0', port=cfg.get("port")) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/kubesql/utils.py b/kubesql/utils.py new file mode 100644 index 0000000..82a4ec3 --- /dev/null +++ b/kubesql/utils.py @@ -0,0 +1,53 @@ +import ConfigParser +from kubernetes import client + + +def load_config(): + config = ConfigParser.ConfigParser() + with open('/etc/kubesql/config', 'r') as cfgfile: + config.readfp(cfgfile) + port = config.getint('kubesql', 'port') + ip = config.get('kubesql', 'ip') + db_path = config.get('kubesql', 'db_path') + param_path = config.get('kubesql', 'param_path') + kubeconfig_path = config.get('kubesql', 'kubeconfig_path') + return {"port": port, + "ip": ip, + "db_path": db_path, + "param_path": param_path, + "kubeconfig_path": kubeconfig_path} + + +def load_params(param_path="/etc/kubesql/params"): + with open(param_path, 'r') as param_file: + contents = param_file.read() + parts = contents.strip().split("---") + param_lists = [] + for part in parts: + lines = part.strip().split("\n") + if len(lines) > 2: + resource = lines[0] + param_lists.append({ + "resource": resource, + "param_list": lines[1:] + }) + return param_lists + + +def get_watch_args(resource): + v1 = client.CoreV1Api() + if resource == "pod": + list_func = v1.list_pod_for_all_namespaces + base_class_name = "V1Pod" + table_name = "pods" + elif resource == "node": + list_func = v1.list_node + base_class_name = "V1Node" + table_name = "nodes" + elif resource == "configmap": + list_func = v1.list_config_map_for_all_namespaces + base_class_name = "V1Pod" + table_name = "configmaps" + return {"list_func": list_func, + "base_class_name": base_class_name, + "table_name": table_name} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eb32024 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +kubernetes==8.0.1 +bottle==0.12.16 +prettytable==0.7.2 +eventlet==0.24.1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cc0a744 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages + +setup( + name='kubesql', + version='0.0.1', + author='xuxinkun', + author_email='xuxinkun@gmail.com', + classifiers=[ + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 2.6'], + packages=find_packages(), + entry_points={'console_scripts': + ['kubesql-server = kubesql.server:main', + 'kubesql = kubesql.client:main', + 'kubesql-watch = kubesql.kubewatch:main'], + } +)