404 lines
13 KiB
Python
404 lines
13 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2016 Mirantis, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Fuel YAQL real-time console.
|
|
Allow fast and easy test your YAQL expressions on live cluster."""
|
|
from __future__ import print_function
|
|
|
|
import inspect
|
|
import json
|
|
import readline
|
|
import sys
|
|
import traceback
|
|
|
|
from nailgun import consts
|
|
from nailgun.fuyaql import completion
|
|
from nailgun.logger import logger
|
|
from nailgun import objects
|
|
from nailgun.orchestrator import deployment_serializers
|
|
from nailgun import yaql_ext
|
|
|
|
logger.disabled = True
|
|
|
|
|
|
class FuYaqlController(object):
|
|
CURRENT = 0
|
|
EXPECTED = 1
|
|
|
|
def __init__(self):
|
|
self._cluster = None
|
|
self._node_id = None
|
|
self._tasks = [None, None]
|
|
self._infos = [None, None]
|
|
self._yaql_context = yaql_ext.create_context(
|
|
add_serializers=True, add_datadiff=True
|
|
)
|
|
self._yaql_engine = yaql_ext.create_engine()
|
|
|
|
@property
|
|
def cluster(self):
|
|
return self._cluster
|
|
|
|
@property
|
|
def node_id(self):
|
|
return self._node_id
|
|
|
|
@property
|
|
def selected_tasks(self):
|
|
return self._tasks
|
|
|
|
def set_cluster(self, cluster_id=None):
|
|
"""Load the cluster object.
|
|
|
|
:param cluster_id: id of a cluster
|
|
"""
|
|
cluster = objects.Cluster.get_by_uid(
|
|
cluster_id, fail_if_not_found=False
|
|
)
|
|
if cluster:
|
|
self._cluster = cluster
|
|
self._set_task(self.EXPECTED, None)
|
|
self._set_task(
|
|
self.CURRENT,
|
|
objects.TransactionCollection.get_last_succeed_run(cluster)
|
|
)
|
|
return True
|
|
return False
|
|
|
|
def set_task(self, state, task_id=None):
|
|
"""Sets the task, which is used to get new state."""
|
|
assert self._cluster
|
|
task = self._get_task(task_id)
|
|
if task is not False:
|
|
self._set_task(state, task)
|
|
return True
|
|
return False
|
|
|
|
def set_node(self, node_id):
|
|
"""Sets the node id."""
|
|
info = self._get_info(self.EXPECTED)
|
|
if node_id in info:
|
|
self._node_id = node_id
|
|
return True
|
|
return False
|
|
|
|
def get_node(self):
|
|
"""Gets the full information about node."""
|
|
assert self._node_id is not None
|
|
return self._get_info(self.EXPECTED)[self._node_id]
|
|
|
|
@staticmethod
|
|
def get_clusters():
|
|
"""Gets list of all clusters."""
|
|
return objects.ClusterCollection.order_by(
|
|
objects.ClusterCollection.all(),
|
|
'id'
|
|
)
|
|
|
|
def get_tasks(self):
|
|
"""Gets all deployment tasks for current cluster."""
|
|
assert self.cluster
|
|
query = objects.TransactionCollection.filter_by(
|
|
None,
|
|
cluster_id=self.cluster.id, name=consts.TASK_NAMES.deployment
|
|
)
|
|
query = objects.TransactionCollection.filter_by_not(
|
|
query, deployment_info=None
|
|
)
|
|
return objects.TransactionCollection.order_by(query, 'id')
|
|
|
|
def get_nodes(self):
|
|
info = self._get_info(self.EXPECTED)
|
|
for node_id in sorted(info):
|
|
yield info[node_id]
|
|
|
|
def evaluate(self, expression):
|
|
"""Evaluate given YAQL expression
|
|
|
|
:param expression: YAQL expression which needed to be evaluated
|
|
:return: result of evaluation as a string
|
|
"""
|
|
assert self.cluster
|
|
assert self.node_id
|
|
context = self._yaql_context.create_child_context()
|
|
context['$%new'] = self._get_info(self.EXPECTED)[self._node_id]
|
|
context['$%old'] = self._get_info(self.CURRENT).get(self._node_id, {})
|
|
|
|
parsed_exp = self._yaql_engine(expression)
|
|
return parsed_exp.evaluate(data=context['$%new'], context=context)
|
|
|
|
def _get_task(self, task_id):
|
|
"""Gets task by id and checks that it belongs to cluster."""
|
|
if not task_id:
|
|
return None
|
|
task = objects.Transaction.get_by_uid(task_id, fail_if_not_found=False)
|
|
if task and task.cluster_id == self.cluster.id:
|
|
return task
|
|
return False
|
|
|
|
def _set_task(self, state, task):
|
|
self._tasks[state] = task
|
|
self._set_info(
|
|
state,
|
|
objects.Transaction.get_deployment_info(task)
|
|
)
|
|
|
|
def _set_info(self, state, info):
|
|
if state == self.EXPECTED:
|
|
self._node_id = None
|
|
if info is None:
|
|
serialized = deployment_serializers.serialize_for_lcm(
|
|
self._cluster,
|
|
objects.Cluster.get_nodes_not_for_deletion(self._cluster)
|
|
)
|
|
info = {node['uid']: node for node in serialized}
|
|
|
|
self._infos[state] = info or {}
|
|
|
|
def _get_info(self, state):
|
|
return self._infos[state]
|
|
|
|
|
|
class FuyaqlInterpreter(object):
|
|
COMMANDS = {
|
|
':show clusters': 'show_clusters',
|
|
':show cluster': 'show_cluster',
|
|
':use cluster': 'set_cluster',
|
|
':show tasks': 'show_tasks',
|
|
':use task2': 'set_task2',
|
|
':use task1': 'set_task1',
|
|
':show nodes': 'show_nodes',
|
|
':show node': 'show_node',
|
|
':use node': 'set_node',
|
|
':help': 'show_help',
|
|
':exit': 'shutdown',
|
|
':q': 'shutdown',
|
|
}
|
|
|
|
def __init__(self, cluster_id=None, node_id=None, controller=None):
|
|
self.controller = controller or FuYaqlController()
|
|
if cluster_id is not None:
|
|
self.set_cluster(cluster_id)
|
|
if node_id is not None:
|
|
self.set_node(node_id)
|
|
|
|
def show_help(self):
|
|
"""Shows this help."""
|
|
for cmd in sorted(self.COMMANDS):
|
|
doc = getattr(self, self.COMMANDS[cmd]).__doc__
|
|
print(cmd, '-', doc)
|
|
|
|
def show_clusters(self):
|
|
"""Shows all clusters which is available for choose."""
|
|
cluster_ids = [
|
|
self.controller.cluster and self.controller.cluster['id']
|
|
]
|
|
self.print_list(
|
|
('id', 'name', 'status'), self.controller.get_clusters(),
|
|
lambda x: cluster_ids.index(x['id'])
|
|
)
|
|
|
|
def show_tasks(self):
|
|
"""Shows all tasks which is available for choose."""
|
|
task_ids = [
|
|
t and t['id'] for t in self.controller.selected_tasks
|
|
]
|
|
|
|
if self._check_cluster():
|
|
self.print_list(
|
|
('id', 'status'), self.controller.get_tasks(),
|
|
lambda x: task_ids.index(x['id'])
|
|
)
|
|
|
|
def show_nodes(self):
|
|
"""Shows all tasks which is available for choose."""
|
|
node_ids = [self.controller.node_id]
|
|
|
|
if self._check_cluster():
|
|
self.print_list(
|
|
('uid', 'status', 'roles'), self.controller.get_nodes(),
|
|
lambda x: node_ids.index(x['uid'])
|
|
)
|
|
|
|
def show_cluster(self):
|
|
"""Shows details of selected cluster."""
|
|
if self.controller.cluster:
|
|
self.print_object(
|
|
'cluster', ('id', 'name', 'status'), self.controller.cluster
|
|
)
|
|
else:
|
|
print("There is no cluster.")
|
|
|
|
def show_task2(self):
|
|
"""Shows details of task, which belongs to new state of cluster."""
|
|
self._show_task(self.controller.EXPECTED)
|
|
|
|
def show_task1(self):
|
|
"""Shows details of task, which belongs to old state of cluster."""
|
|
self._show_task(self.controller.CURRENT)
|
|
|
|
def show_node(self):
|
|
"""Shows details of selected node."""
|
|
if self.controller.node_id:
|
|
self.print_object(
|
|
'node',
|
|
('uid', 'status', 'roles'),
|
|
self.controller.get_node()
|
|
)
|
|
else:
|
|
print("Please select node at first.")
|
|
|
|
def set_cluster(self, cluster_id):
|
|
"""Select the cluster."""
|
|
if not self.controller.set_cluster(cluster_id):
|
|
print("There is no cluster with id:", cluster_id)
|
|
|
|
def set_node(self, node_id):
|
|
"""Select the node."""
|
|
if self._check_cluster():
|
|
if not self.controller.set_node(node_id):
|
|
print("There is no node with id:", node_id)
|
|
|
|
def set_task2(self, task_id):
|
|
"""Select the task which will belong to state new."""
|
|
self._set_task(self.controller.EXPECTED, task_id)
|
|
|
|
def set_task1(self, task_id):
|
|
"""Select the task which will belong to state old."""
|
|
self._set_task(self.controller.CURRENT, task_id)
|
|
|
|
def evaluate_expression(self, exp):
|
|
if self._check_node():
|
|
return self.controller.evaluate(exp)
|
|
|
|
def execute_command(self, cmdline):
|
|
for cmd in self.COMMANDS:
|
|
if (cmdline.startswith(cmd) and
|
|
(len(cmdline) == len(cmd) or cmdline[len(cmd)].isspace())):
|
|
break
|
|
else:
|
|
print("Unknown command:", cmdline)
|
|
print("Please use :help to see list of available commands")
|
|
return
|
|
|
|
f = getattr(self, self.COMMANDS[cmd])
|
|
args = cmdline[len(cmd):].split()
|
|
try:
|
|
inspect.getcallargs(f, *args)
|
|
except (ValueError, TypeError):
|
|
print("Not enough arguments for a command were given.")
|
|
return
|
|
|
|
return f(*args)
|
|
|
|
@staticmethod
|
|
def shutdown():
|
|
"""Exits."""
|
|
sys.exit(0)
|
|
|
|
def run_loop(self):
|
|
"""Create a loop for user input"""
|
|
while True:
|
|
try:
|
|
command = raw_input('fuel-yaql> ').strip()
|
|
except EOFError:
|
|
return
|
|
if not command:
|
|
continue
|
|
|
|
try:
|
|
if command.startswith(':'): # Check for internal command
|
|
r = self.execute_command(command)
|
|
else:
|
|
r = self.evaluate_expression(command)
|
|
|
|
if isinstance(r, (list, dict)):
|
|
print(json.dumps(r, indent=4))
|
|
elif r is not None:
|
|
print(r)
|
|
|
|
except Exception as e:
|
|
print("Unexpected error: {0}".format(e))
|
|
traceback.print_exc(sys.stdout)
|
|
|
|
def _show_task(self, state):
|
|
task = self.controller.selected_tasks[state]
|
|
if task:
|
|
self.print_object('task', ('id', 'status'), task)
|
|
else:
|
|
print("Please select task at first.")
|
|
|
|
def _set_task(self, state, task_id):
|
|
if self._check_cluster():
|
|
next_state = (state + 1) % len(self.controller.selected_tasks)
|
|
next_task = self.controller.selected_tasks[next_state]
|
|
task_id = int(task_id or 0)
|
|
if task_id and next_task:
|
|
if next_state > state and task_id > next_task['id']:
|
|
print("The task, which belongs to state old cannot be"
|
|
" under than task which belongs to state new.")
|
|
return
|
|
elif next_state < state and task_id < next_task['id']:
|
|
print("The task, which belongs to state new cannot be"
|
|
" older than task which belongs to state old.")
|
|
return
|
|
self.controller.set_task(state, task_id)
|
|
|
|
def _check_cluster(self):
|
|
if self.controller.cluster is None:
|
|
print("Select cluster at first.")
|
|
return False
|
|
return True
|
|
|
|
def _check_node(self):
|
|
if self.controller.node_id is None:
|
|
print("Select node at first.")
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def print_list(column_names, iterable, get_selected_index=None):
|
|
template = '\t|\t'.join('{%d}' % x for x in range(len(column_names)))
|
|
print(template.format(*column_names))
|
|
print('-' * (sum(len(x) + 5 for x in column_names)))
|
|
for column in iterable:
|
|
if get_selected_index is not None:
|
|
try:
|
|
print('*' * (get_selected_index(column) + 1), end=' ')
|
|
except ValueError:
|
|
pass
|
|
print(template.format(*(column.get(x, '-') for x in column_names)))
|
|
|
|
@staticmethod
|
|
def print_object(name, properties, obj):
|
|
print(name.title() + ':')
|
|
for p in properties:
|
|
print("\t{0}:\t{1}".format(p, obj.get(p, '-')))
|
|
|
|
|
|
def main(cluster_id=None, node_id=None):
|
|
# set up command history and completion
|
|
readline.set_completer_delims(r'''`~!@#$%^&*()-=+[{]}\|;'",<>/?''')
|
|
readline.set_completer(completion.FuCompleter(
|
|
list(FuyaqlInterpreter.COMMANDS)
|
|
).complete)
|
|
readline.parse_and_bind('tab: complete')
|
|
interpret = FuyaqlInterpreter(cluster_id, node_id)
|
|
interpret.run_loop()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|