Merge branch 'ocicat', the unrealized dream. Ocicat will return...

This commit is contained in:
Kristóf Tóth 2019-05-15 11:19:24 +02:00
commit 07cd1264f5
55 changed files with 715 additions and 189 deletions

View File

@ -10,4 +10,4 @@ pipeline:
- docker push eu.gcr.io/avatao-challengestore/tutorial-framework:${DRONE_TAG} - docker push eu.gcr.io/avatao-challengestore/tutorial-framework:${DRONE_TAG}
when: when:
event: 'tag' event: 'tag'
branch: refs/tags/mainecoon-20* branch: refs/tags/ocicat-20*

22
.git-hooks/apply_hooks.sh Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eu
set -o pipefail
set -o errtrace
shopt -s expand_aliases
[ "$(uname)" == "Darwin" ] && alias readlink="greadlink" || :
GREEN='\033[0;32m'
NC='\033[0m'
here="$(dirname "$(readlink -f "$0")")"
cd "${here}/../.git/hooks"
rm -f pre-push pre-commit || :
prepush_script="../../.git-hooks/pre-push.sh"
precommit_script="../../.git-hooks/pre-commit.sh"
[ -f "${prepush_script}" ] && ln -s "${prepush_script}" pre-push
[ -f "${precommit_script}" ] && ln -s "${precommit_script}" pre-commit
echo -e "\n${GREEN}Done! Hooks applied, you can start committing and pushing!${NC}\n"

18
.git-hooks/pre-push.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -eu
set -o pipefail
set -o errtrace
shopt -s expand_aliases
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo -e "Running pylint...\n"
if pylint lib; then
echo -e "\n${GREEN}Pylint found no errors!${NC}\n"
else
echo -e "\n${RED}Pylint failed with errors${NC}\n"
exit 1
fi

View File

@ -3,3 +3,9 @@
ignored-modules = zmq ignored-modules = zmq
max-line-length = 120 max-line-length = 120
disable = missing-docstring, too-few-public-methods, invalid-name disable = missing-docstring, too-few-public-methods, invalid-name
[SIMILARITIES]
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes

View File

@ -36,9 +36,10 @@ ENV PYTHONPATH="/usr/local/lib" \
TFW_NGINX_DEFAULT="/etc/nginx/sites-enabled/default" \ TFW_NGINX_DEFAULT="/etc/nginx/sites-enabled/default" \
TFW_NGINX_COMPONENTS="/etc/nginx/components" \ TFW_NGINX_COMPONENTS="/etc/nginx/components" \
TFW_LIB_DIR="/usr/local/lib" \ TFW_LIB_DIR="/usr/local/lib" \
TFW_TERMINADO_DIR="/tmp/terminado_server" \
TFW_FRONTEND_DIR="/srv/frontend" \ TFW_FRONTEND_DIR="/srv/frontend" \
TFW_SERVER_DIR="/srv/.tfw" \ TFW_DIR="/.tfw" \
TFW_SERVER_DIR="/.tfw/tfw_server" \
TFW_SNAPSHOTS_DIR="/.tfw/snapshots" \
TFW_AUTH_KEY="/tmp/tfw-auth.key" \ TFW_AUTH_KEY="/tmp/tfw-auth.key" \
TFW_HISTFILE="/home/${AVATAO_USER}/.bash_history" \ TFW_HISTFILE="/home/${AVATAO_USER}/.bash_history" \
PROMPT_COMMAND="history -a" PROMPT_COMMAND="history -a"
@ -75,4 +76,4 @@ ONBUILD RUN test -z "${NOFRONTEND}" && cd /data && yarn install --frozen-lockfil
ONBUILD RUN test -z "${NOFRONTEND}" && cd /data && yarn build --no-progress || : ONBUILD RUN test -z "${NOFRONTEND}" && cd /data && yarn build --no-progress || :
ONBUILD RUN test -z "${NOFRONTEND}" && mv /data/dist ${TFW_FRONTEND_DIR} && rm -rf /data || : ONBUILD RUN test -z "${NOFRONTEND}" && mv /data/dist ${TFW_FRONTEND_DIR} && rm -rf /data || :
CMD exec supervisord --nodaemon CMD exec supervisord --nodaemon --configuration ${TFW_SUPERVISORD_CONF}

View File

@ -47,6 +47,8 @@ Our pre-made event handlers are written in Python3, but you can write event hand
This makes the framework really flexible: you can demonstrate the concepts you want to in any language while using the same set of tools provided by TFW. This makes the framework really flexible: you can demonstrate the concepts you want to in any language while using the same set of tools provided by TFW.
Inside Avatao this means that any of the content teams can use the framework with ease. Inside Avatao this means that any of the content teams can use the framework with ease.
To implement an event handler in Python3 you should subclass the `EventHandlerBase` or `FSMAwareEventHandler` class in `tfw.event_handler_base` (the first provides a minimal working `EventHandler`, the second allows you to execute code on FSM events).
### FSM ### FSM
Another unique feature of the framework is the FSM finite state machine representing the state of your challenge. Another unique feature of the framework is the FSM finite state machine representing the state of your challenge.
@ -74,20 +76,24 @@ The TFW message format:
```text ```text
{ {
"key: "some identifier used for addressing", "key: ...some identifier used for addressing...,
"data": "data":
{ {
... ...
JSON object carrying anything, preferably cats JSON object carrying anything, preferably cats
... ...
}, },
"trigger": "FSM action" "trigger": ...FSM action...,
"signature": ...HMAC signature for authenticated messages...,
"seq": ...sequence number...
} }
``` ```
- The `key` field is used by TFW for addressing and every message must have one (it can be an empty string though) - The `key` field is used by TFW for addressing and every message must have one (it can be an empty string though)
- The `data` object can contain anything you might want to send - The `data` object can contain anything you might want to send
- The `trigger` key is an optional field that triggers an FSM action with that name from the current state (whatever that might be) - The `trigger` key is an optional field that triggers an FSM action with that name from the current state (whatever that might be)
- The `signature` field is present on authenticated messages (such as `fsm_update`s)
- The `seq` key is a counter incremented with each proxied message in the TFW server
To mirror messages back to their sources you can use a special messaging format, in which the message to be mirrored is enveloped inside the `data` field of the outer message: To mirror messages back to their sources you can use a special messaging format, in which the message to be mirrored is enveloped inside the `data` field of the outer message:
@ -117,6 +123,8 @@ APIs exposed by our pre-witten event handlers are documented here.
### IdeEventHandler ### IdeEventHandler
This event handler is responsible for reading and writing files shown in the frontend code editor.
You can read the content of the currently selected file like so: You can read the content of the currently selected file like so:
``` ```
{ {
@ -178,6 +186,15 @@ Overwriting the current list of excluded file patterns is possible with this mes
### TerminalEventHandler ### TerminalEventHandler
Event handler responsible for running a backend for `xterm.js` to connect to (frontend terminal backend).
By default callbacks on terminal history are invoked *as soon as* a command starts to execute in the terminal (they do not wait for the started command to finish, the callback may even run in paralell with the command).
If you want to wait for them and invoke your callbacks *after* the command has finished, please set the `TFW_DELAY_HISTAPPEND` envvar to `1`.
Practically this can be done by appending an `export` to the user's `.bashrc` file from your `Dockerfile`, like so:
`RUN echo "export TFW_DELAY_HISTAPPEND=1" >> /home/${AVATAO_USER}/.bashrc`
Writing to the terminal: Writing to the terminal:
``` ```
{ {
@ -204,6 +221,8 @@ You can read terminal command history like so:
### ProcessManagingEventHandler ### ProcessManagingEventHandler
This event handler is responsible for managing processes controlled by supervisord.
Starting, stopping and restarting supervisor processes can be done using similar messages (where `command` is `start`, `stop` or `restart`): Starting, stopping and restarting supervisor processes can be done using similar messages (where `command` is `start`, `stop` or `restart`):
``` ```
{ {
@ -218,6 +237,8 @@ Starting, stopping and restarting supervisor processes can be done using similar
### LogMonitoringEventHandler ### LogMonitoringEventHandler
Event handler emitting real time logs (`stdout` and `stderr`) from supervisord processes.
To change which supervisor process is monitored use this message: To change which supervisor process is monitored use this message:
``` ```
{ {
@ -244,6 +265,8 @@ To set the tail length of logs (the monitor will send back the last `value` char
### FSMManagingEventHandler ### FSMManagingEventHandler
This event handler controls the TFW finite state machine (FSM).
To attempt executing a trigger on the FSM use (this will also generate an FSM update message): To attempt executing a trigger on the FSM use (this will also generate an FSM update message):
``` ```
{ {
@ -279,3 +302,41 @@ This event handler broadcasts FSM update messages after handling commands in the
} }
``` ```
### DirectorySnapshottingEventHandler
Event handler capable of taking and restoring snapshots of directories (saving and restoring directory contens).
You can take a snapshot of the directories with the following message:
```
{
"key": "snapshot",
"data" :
{
"command": "take_snapshot"
}
}
```
To restore the state of the files in the directories use:
```
{
"key": "snapshot",
"data" :
{
"command": "restore_snapshot",
"value": ...date string (can parse ISO 8601, unix timestamp, etc.)...
}
}
```
It is also possible to exclude files that match given patterns (formatted like lines in `.gitignore` files):
```
{
"key": "snapshot",
"data" :
{
"command": "exclude",
"value": ...list of patterns to exclude from snapshots...
}
}
```

View File

@ -1 +1 @@
mainecoon ocicat

View File

@ -23,3 +23,10 @@ Components
.. autoclass:: BashMonitor .. autoclass:: BashMonitor
:members: :members:
.. autoclass:: FSMManagingEventHandler
:members:
.. autoclass:: CommandsEqual
:members:

View File

@ -3,10 +3,13 @@ Event handler base classes
Subclass these to create your cusom event handlers. Subclass these to create your cusom event handlers.
.. automodule:: tfw .. automodule:: tfw.event_handler_base
.. autoclass:: EventHandlerBase .. autoclass:: EventHandlerBase
:members: :members:
.. autoclass:: TriggeredEventHandler .. autoclass:: FSMAwareEventHandler
:members:
.. autoclass:: BroadcastingEventHandler
:members: :members:

View File

@ -3,10 +3,13 @@ FSM base classes
Subclass these to create an FSM that fits your tutorial/challenge. Subclass these to create an FSM that fits your tutorial/challenge.
.. automodule:: tfw .. automodule:: tfw.fsm
.. autoclass:: FSMBase .. autoclass:: FSMBase
:members: :members:
.. autoclass:: LinearFSM .. autoclass:: LinearFSM
:members: :members:
.. autoclass:: YamlFSM
:members:

View File

@ -36,6 +36,16 @@ These are pre-written components for you to use, such as our IDE, terminal or co
components/* components/*
Utility
-------
These are useful decorators, mixins and helpers to make common dev tasks easier.
.. toctree::
:glob:
utility/*
Indices and tables Indices and tables
================== ==================

View File

@ -6,7 +6,7 @@ Networking
.. autoclass:: TFWServerConnector .. autoclass:: TFWServerConnector
:members: :members:
.. automodule:: tfw.networking.event_handlers .. automodule:: tfw.networking.event_handlers.server_connector
.. autoclass:: ServerUplinkConnector .. autoclass:: ServerUplinkConnector
:members: :members:
@ -15,3 +15,8 @@ Networking
.. autoclass:: MessageSender .. autoclass:: MessageSender
:members: :members:
.. automodule:: tfw.networking.fsm_aware
.. autoclass:: FSMAware
:members:

View File

@ -0,0 +1,10 @@
TFW decorators
--------------
.. automodule:: tfw.decorators.rate_limiter
.. autoclass:: RateLimiter
:members:
.. autoclass:: AsyncRateLimiter
:members:

View File

@ -4,7 +4,7 @@
from collections import namedtuple from collections import namedtuple
from os import environ from os import environ
from tfw.decorators import lazy_property from tfw.decorators.lazy_property import lazy_property
class LazyEnvironment: class LazyEnvironment:

View File

@ -1,7 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .event_handler_base import EventHandlerBase, FSMAwareEventHandler, BroadcastingEventHandler
from .fsm_base import FSMBase
from .linear_fsm import LinearFSM
from .yaml_fsm import YamlFSM

View File

@ -12,3 +12,5 @@ from .fsm_managing_event_handler import FSMManagingEventHandler
from .snapshot_provider import SnapshotProvider from .snapshot_provider import SnapshotProvider
from .pipe_io_event_handler import PipeIOEventHandlerBase, PipeIOEventHandler, PipeIOServer from .pipe_io_event_handler import PipeIOEventHandlerBase, PipeIOEventHandler, PipeIOServer
from .pipe_io_event_handler import TransformerPipeIOEventHandler, CommandEventHandler from .pipe_io_event_handler import TransformerPipeIOEventHandler, CommandEventHandler
from .directory_snapshotting_event_handler import DirectorySnapshottingEventHandler
from .commands_equal import CommandsEqual

View File

@ -0,0 +1,110 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from shlex import split
from re import search
from tfw.decorators.lazy_property import lazy_property
class CommandsEqual:
# pylint: disable=too-many-arguments
"""
This class is useful for comparing executed commands with
excepted commands (i.e. when triggering a state change when
the correct command is executed).
Note that in most cases you should test the changes
caused by the commands instead of just checking command history
(stuff can be done in countless ways and preparing for every
single case is impossible). This should only be used when
testing the changes would be very difficult, like when
explaining stuff with cli tools and such.
This class implicitly converts to bool, use it like
if CommandsEqual(...): ...
It tries detecting differing command parameter orders with similar
semantics and provides fuzzy logic options.
The rationale behind this is that a few false positives
are better than only accepting a single version of a command
(i.e. using ==).
"""
def __init__(
self, command_1, command_2,
fuzzyness=1, begin_similarly=True,
include_patterns=None, exclude_patterns=None
):
"""
:param command_1: Compared command 1
:param command_2: Compared command 2
:param fuzzyness: float between 0 and 1.
the percentage of arguments required to
match between commands to result in True.
i.e 1 means 100% - all arguments need to be
present in both commands, while 0.75
would mean 75% - in case of 4 arguments
1 could differ between the commands.
:param begin_similarly: bool, the first word of the commands
must match
:param include_patterns: list of regex patterns the commands
must include
:param exclude_patterns: list of regex patterns the commands
must exclude
"""
self.command_1 = split(command_1)
self.command_2 = split(command_2)
self.fuzzyness = fuzzyness
self.begin_similarly = begin_similarly
self.include_patterns = include_patterns
self.exclude_patterns = exclude_patterns
def __bool__(self):
if self.begin_similarly:
if not self.beginnings_are_equal:
return False
if self.include_patterns is not None:
if not self.commands_contain_include_patterns:
return False
if self.exclude_patterns is not None:
if not self.commands_contain_no_exclude_patterns:
return False
return self.similarity >= self.fuzzyness
@lazy_property
def beginnings_are_equal(self):
return self.command_1[0] == self.command_2[0]
@lazy_property
def commands_contain_include_patterns(self):
return all((
self.contains_regex_patterns(self.command_1, self.include_patterns),
self.contains_regex_patterns(self.command_2, self.include_patterns)
))
@lazy_property
def commands_contain_no_exclude_patterns(self):
return all((
not self.contains_regex_patterns(self.command_1, self.exclude_patterns),
not self.contains_regex_patterns(self.command_2, self.exclude_patterns)
))
@staticmethod
def contains_regex_patterns(command, regex_parts):
command = ' '.join(command)
for pattern in regex_parts:
if not search(pattern, command):
return False
return True
@lazy_property
def similarity(self):
parts_1 = set(self.command_1)
parts_2 = set(self.command_2)
difference = parts_1 - parts_2
deviance = len(difference) / len(max(parts_1, parts_2))
return 1 - deviance

View File

@ -5,9 +5,9 @@ from functools import wraps
from watchdog.events import FileSystemEventHandler as FileSystemWatchdogEventHandler from watchdog.events import FileSystemEventHandler as FileSystemWatchdogEventHandler
from tfw.networking.event_handlers import ServerUplinkConnector from tfw.networking.event_handlers.server_connector import ServerUplinkConnector
from tfw.decorators import RateLimiter from tfw.decorators.rate_limiter import RateLimiter
from tfw.mixins import ObserverMixin from tfw.mixins.observer_mixin import ObserverMixin
from tfw.config.logs import logging from tfw.config.logs import logging

View File

@ -3,10 +3,10 @@
from os.path import isdir, exists from os.path import isdir, exists
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.mixins.monitor_manager_mixin import MonitorManagerMixin
from tfw.components.directory_monitor import DirectoryMonitor
from tfw.config.logs import logging from tfw.config.logs import logging
from tfw.mixins import MonitorManagerMixin
from .directory_monitor import DirectoryMonitor
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -0,0 +1,87 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from os.path import join as joinpath
from os.path import basename
from os import makedirs
from datetime import datetime
from dateutil import parser as dateparser
from tfw.event_handler_base import EventHandlerBase
from tfw.components.snapshot_provider import SnapshotProvider
from tfw.config import TFWENV
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class DirectorySnapshottingEventHandler(EventHandlerBase):
def __init__(self, key, directories, exclude_unix_patterns=None):
super().__init__(key)
self.snapshot_providers = {}
self._exclude_unix_patterns = exclude_unix_patterns
self.init_snapshot_providers(directories)
self.command_handlers = {
'take_snapshot': self.handle_take_snapshot,
'restore_snapshot': self.handle_restore_snapshot,
'exclude': self.handle_exclude
}
def init_snapshot_providers(self, directories):
for index, directory in enumerate(directories):
git_dir = self.init_git_dir(index, directory)
self.snapshot_providers[directory] = SnapshotProvider(
directory,
git_dir,
self._exclude_unix_patterns
)
@staticmethod
def init_git_dir(index, directory):
git_dir = joinpath(
TFWENV.SNAPSHOTS_DIR,
f'{basename(directory)}-{index}'
)
makedirs(git_dir, exist_ok=True)
return git_dir
def handle_event(self, message):
try:
data = message['data']
message['data'] = self.command_handlers[data['command']](data)
return message
except KeyError:
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def handle_take_snapshot(self, data):
LOG.debug('Taking snapshots of directories %s', self.snapshot_providers.keys())
for provider in self.snapshot_providers.values():
provider.take_snapshot()
return data
def handle_restore_snapshot(self, data):
date = dateparser.parse(
data.get(
'value',
datetime.now().isoformat()
)
)
LOG.debug(
'Restoring snapshots (@ %s) of directories %s',
date,
self.snapshot_providers.keys()
)
for provider in self.snapshot_providers.values():
provider.restore_snapshot(date)
return data
def handle_exclude(self, data):
exclude_unix_patterns = data['value']
if not isinstance(exclude_unix_patterns, list):
raise KeyError
for provider in self.snapshot_providers.values():
provider.exclude = exclude_unix_patterns
return data

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.crypto import KeyManager, sign_message, verify_message from tfw.crypto import KeyManager, sign_message, verify_message
from tfw.config.logs import logging from tfw.config.logs import logging
@ -9,6 +9,20 @@ LOG = logging.getLogger(__name__)
class FSMManagingEventHandler(EventHandlerBase): class FSMManagingEventHandler(EventHandlerBase):
"""
EventHandler responsible for managing the state machine of
the framework (TFW FSM).
tfw.networking.TFWServer instances automatically send 'trigger'
commands to the event handler listening on the 'fsm' key,
which should be an instance of this event handler.
This event handler accepts messages that have a
data['command'] key specifying a command to be executed.
An 'fsm_update' message is broadcasted after every successful
command.
"""
def __init__(self, key, fsm_type, require_signature=False): def __init__(self, key, fsm_type, require_signature=False):
super().__init__(key) super().__init__(key)
self.fsm = fsm_type() self.fsm = fsm_type()
@ -25,7 +39,7 @@ class FSMManagingEventHandler(EventHandlerBase):
try: try:
message = self.command_handlers[message['data']['command']](message) message = self.command_handlers[message['data']['command']](message)
if message: if message:
fsm_update_message = self._fsm_updater.generate_fsm_update() fsm_update_message = self._fsm_updater.fsm_update
sign_message(self.auth_key, message) sign_message(self.auth_key, message)
sign_message(self.auth_key, fsm_update_message) sign_message(self.auth_key, fsm_update_message)
self.server_connector.broadcast(fsm_update_message) self.server_connector.broadcast(fsm_update_message)
@ -34,6 +48,12 @@ class FSMManagingEventHandler(EventHandlerBase):
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message) LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
def handle_trigger(self, message): def handle_trigger(self, message):
"""
Attempts to step the FSM with the supplied trigger.
:param message: TFW message with a data field containing
the action to try triggering in data['value']
"""
trigger = message['data']['value'] trigger = message['data']['value']
if self._require_signature: if self._require_signature:
if not verify_message(self.auth_key, message): if not verify_message(self.auth_key, message):
@ -44,6 +64,9 @@ class FSMManagingEventHandler(EventHandlerBase):
return None return None
def handle_update(self, message): def handle_update(self, message):
"""
Does nothing, but triggers an 'fsm_update' message.
"""
# pylint: disable=no-self-use # pylint: disable=no-self-use
return message return message
@ -52,23 +75,24 @@ class FSMUpdater:
def __init__(self, fsm): def __init__(self, fsm):
self.fsm = fsm self.fsm = fsm
def generate_fsm_update(self): @property
def fsm_update(self):
return { return {
'key': 'fsm_update', 'key': 'fsm_update',
'data': self.get_fsm_state_and_transitions() 'data': self.fsm_update_data
} }
def get_fsm_state_and_transitions(self): @property
state = self.fsm.state def fsm_update_data(self):
valid_transitions = [ valid_transitions = [
{'trigger': trigger} {'trigger': trigger}
for trigger in self.fsm.get_triggers(self.fsm.state) for trigger in self.fsm.get_triggers(self.fsm.state)
] ]
last_trigger = self.fsm.trigger_history[-1] if self.fsm.trigger_history else None last_fsm_event = self.fsm.event_log[-1]
in_accepted_state = state in self.fsm.accepted_states last_fsm_event['timestamp'] = last_fsm_event['timestamp'].isoformat()
return { return {
'current_state': state, 'current_state': self.fsm.state,
'valid_transitions': valid_transitions, 'valid_transitions': valid_transitions,
'last_trigger': last_trigger, 'in_accepted_state': self.fsm.in_accepted_state,
'in_accepted_state': in_accepted_state 'last_event': last_fsm_event
} }

View File

@ -8,8 +8,9 @@ from abc import ABC, abstractmethod
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
from tfw.mixins import CallbackMixin, ObserverMixin from tfw.mixins.callback_mixin import CallbackMixin
from tfw.decorators import RateLimiter from tfw.mixins.observer_mixin import ObserverMixin
from tfw.decorators.rate_limiter import RateLimiter
class CallbackEventHandler(PatternMatchingEventHandler, ABC): class CallbackEventHandler(PatternMatchingEventHandler, ABC):

View File

@ -6,10 +6,10 @@ from glob import glob
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from typing import Iterable from typing import Iterable
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.mixins import MonitorManagerMixin from tfw.mixins.monitor_manager_mixin import MonitorManagerMixin
from tfw.components.directory_monitor import DirectoryMonitor
from tfw.config.logs import logging from tfw.config.logs import logging
from .directory_monitor import DirectoryMonitor
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -157,7 +157,8 @@ class IdeEventHandler(EventHandlerBase, MonitorManagerMixin):
""" """
Read the currently selected file. Read the currently selected file.
:return dict: message with the contents of the file in data['content'] :return dict: TFW message data containing key 'content'
(contents of the selected file)
""" """
try: try:
data['content'] = self.filemanager.file_contents data['content'] = self.filemanager.file_contents

View File

@ -6,9 +6,10 @@ from os.path import dirname
from watchdog.events import PatternMatchingEventHandler as PatternMatchingWatchdogEventHandler from watchdog.events import PatternMatchingEventHandler as PatternMatchingWatchdogEventHandler
from tfw.networking.event_handlers import ServerUplinkConnector from tfw.networking.event_handlers.server_connector import ServerUplinkConnector
from tfw.decorators import RateLimiter from tfw.decorators.rate_limiter import RateLimiter
from tfw.mixins import ObserverMixin, SupervisorLogMixin from tfw.mixins.observer_mixin import ObserverMixin
from tfw.mixins.supervisor_mixin import SupervisorLogMixin
class LogMonitor(ObserverMixin): class LogMonitor(ObserverMixin):

View File

@ -1,10 +1,10 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.mixins import MonitorManagerMixin from tfw.mixins.monitor_manager_mixin import MonitorManagerMixin
from tfw.components.log_monitor import LogMonitor
from tfw.config.logs import logging from tfw.config.logs import logging
from .log_monitor import LogMonitor
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -9,7 +9,7 @@ from secrets import token_urlsafe
from threading import Thread from threading import Thread
from contextlib import suppress from contextlib import suppress
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.config.logs import logging from tfw.config.logs import logging
from .pipe_io_server import PipeIOServer, terminate_process_on_failure from .pipe_io_server import PipeIOServer, terminate_process_on_failure

View File

@ -3,10 +3,10 @@
from xmlrpc.client import Fault as SupervisorFault from xmlrpc.client import Fault as SupervisorFault
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.mixins import SupervisorMixin, SupervisorLogMixin from tfw.mixins.supervisor_mixin import SupervisorMixin, SupervisorLogMixin
from tfw.components.directory_monitor import with_monitor_paused
from tfw.config.logs import logging from tfw.config.logs import logging
from .directory_monitor import with_monitor_paused
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)

View File

@ -2,15 +2,17 @@
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
import re import re
from subprocess import run, CalledProcessError from subprocess import run, CalledProcessError, PIPE
from getpass import getuser from getpass import getuser
from os.path import isdir from os.path import isdir
from datetime import datetime from os.path import join as joinpath
from uuid import uuid4 from uuid import uuid4
from dateutil import parser as dateparser
class SnapshotProvider: class SnapshotProvider:
def __init__(self, directory, git_dir): def __init__(self, directory, git_dir, exclude_unix_patterns=None):
self._classname = self.__class__.__name__ self._classname = self.__class__.__name__
author = f'{getuser()} via TFW {self._classname}' author = f'{getuser()} via TFW {self._classname}'
self.gitenv = { self.gitenv = {
@ -25,6 +27,8 @@ class SnapshotProvider:
self._init_repo() self._init_repo()
self.__last_valid_branch = self._branch self.__last_valid_branch = self._branch
if exclude_unix_patterns:
self.exclude = exclude_unix_patterns
def _init_repo(self): def _init_repo(self):
self._check_environment() self._check_environment()
@ -66,10 +70,14 @@ class SnapshotProvider:
'git', 'add', 'git', 'add',
'-A' '-A'
)) ))
self._run(( try:
'git', 'commit', self._get_stdout((
'-m', 'Snapshot' 'git', 'commit',
)) '-m', 'Snapshot'
))
except CalledProcessError as err:
if b'nothing to commit, working tree clean' not in err.output:
raise
def _check_head_not_detached(self): def _check_head_not_detached(self):
if self._head_detached: if self._head_detached:
@ -87,7 +95,8 @@ class SnapshotProvider:
)) ))
def _get_stdout(self, *args, **kwargs): def _get_stdout(self, *args, **kwargs):
kwargs['capture_output'] = True kwargs['stdout'] = PIPE
kwargs['stderr'] = PIPE
stdout_bytes = self._run(*args, **kwargs).stdout stdout_bytes = self._run(*args, **kwargs).stdout
return stdout_bytes.decode().rstrip('\n') return stdout_bytes.decode().rstrip('\n')
@ -98,13 +107,31 @@ class SnapshotProvider:
kwargs['env'] = self.gitenv kwargs['env'] = self.gitenv
return run(*args, **kwargs) return run(*args, **kwargs)
@property
def exclude(self):
with open(self._exclude_path, 'r') as ofile:
return ofile.read()
@exclude.setter
def exclude(self, exclude_patterns):
with open(self._exclude_path, 'w') as ifile:
ifile.write('\n'.join(exclude_patterns))
@property
def _exclude_path(self):
return joinpath(
self.gitenv['GIT_DIR'],
'info',
'exclude'
)
def take_snapshot(self): def take_snapshot(self):
if self._head_detached: if self._head_detached:
self._checkout_new_branch_from_head() self._checkout_new_branch_from_head()
self._snapshot() self._snapshot()
def _checkout_new_branch_from_head(self): def _checkout_new_branch_from_head(self):
branch_name = uuid4() branch_name = str(uuid4())
self._run(( self._run((
'git', 'branch', 'git', 'branch',
branch_name branch_name
@ -119,16 +146,30 @@ class SnapshotProvider:
def restore_snapshot(self, date): def restore_snapshot(self, date):
commit = self._get_commit_from_timestamp(date) commit = self._get_commit_from_timestamp(date)
branch = self._last_valid_branch
if commit == self._latest_commit_on_branch(branch):
commit = branch
self._checkout(commit) self._checkout(commit)
def _get_commit_from_timestamp(self, date): def _get_commit_from_timestamp(self, date):
return self._get_stdout(( commit = self._get_stdout((
'git', 'rev-list', 'git', 'rev-list',
'--date=iso', '--date=iso',
'-n', '1', '-n', '1',
f'--before="{date.isoformat()}"', f'--before="{date.isoformat()}"',
self._last_valid_branch self._last_valid_branch
)) ))
if not commit:
commit = self._get_oldest_parent_of_head()
return commit
def _get_oldest_parent_of_head(self):
return self._get_stdout((
'git',
'rev-list',
'--max-parents=0',
'HEAD'
))
@property @property
def _last_valid_branch(self): def _last_valid_branch(self):
@ -136,6 +177,14 @@ class SnapshotProvider:
self.__last_valid_branch = self._branch self.__last_valid_branch = self._branch
return self.__last_valid_branch return self.__last_valid_branch
def _latest_commit_on_branch(self, branch):
return self._get_stdout((
'git', 'log',
'-n', '1',
'--pretty=format:%H',
branch
))
@property @property
def all_timelines(self): def all_timelines(self):
return self._branches return self._branches
@ -169,7 +218,7 @@ class SnapshotProvider:
commit_hash, timestamp = line.split('@') commit_hash, timestamp = line.split('@')
commits.append({ commits.append({
'hash': commit_hash, 'hash': commit_hash,
'timestamp': datetime.fromisoformat(timestamp) 'timestamp': dateparser.parse(timestamp)
}) })
return commits return commits

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from tfw import EventHandlerBase from tfw.event_handler_base import EventHandlerBase
from tfw.components.terminado_mini_server import TerminadoMiniServer
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.config.logs import logging from tfw.config.logs import logging
from tao.config import TAOENV from tao.config import TAOENV
from .terminado_mini_server import TerminadoMiniServer
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -26,7 +26,6 @@ class TerminalEventHandler(EventHandlerBase):
:param monitor: tfw.components.HistoryMonitor instance to read command history from :param monitor: tfw.components.HistoryMonitor instance to read command history from
""" """
super().__init__(key) super().__init__(key)
self.working_directory = TFWENV.TERMINADO_DIR
self._historymonitor = monitor self._historymonitor = monitor
bash_as_user_cmd = ['sudo', '-u', TAOENV.USER, 'bash'] bash_as_user_cmd = ['sudo', '-u', TAOENV.USER, 'bash']

View File

@ -14,8 +14,8 @@ from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.hmac import HMAC as _HMAC from cryptography.hazmat.primitives.hmac import HMAC as _HMAC
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from tfw.networking import message_bytes from tfw.networking.serialization import message_bytes
from tfw.decorators import lazy_property from tfw.decorators.lazy_property import lazy_property
from tfw.config import TFWENV from tfw.config import TFWENV

View File

@ -1,5 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .rate_limiter import RateLimiter
from .lazy_property import lazy_property

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from functools import update_wrapper from functools import update_wrapper, wraps
class lazy_property: class lazy_property:
@ -19,3 +19,12 @@ class lazy_property:
value = self.func(instance) value = self.func(instance)
setattr(instance, self.func.__name__, value) setattr(instance, self.func.__name__, value)
return value return value
def lazy_factory(fun):
class wrapper:
@wraps(fun)
@lazy_property
def instance(self): # pylint: disable=no-self-use
return fun()
return wrapper()

View File

@ -87,6 +87,7 @@ class AsyncRateLimiter(RateLimiter):
return self._ioloop_factory() return self._ioloop_factory()
def action(self, seconds_to_next_call): def action(self, seconds_to_next_call):
# pylint: disable=method-hidden
if self._last_callback: if self._last_callback:
self.ioloop.remove_timeout(self._last_callback) self.ioloop.remove_timeout(self._last_callback)

View File

@ -0,0 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .event_handler_base import EventHandlerBase
from .boradcasting_event_handler import BroadcastingEventHandler
from .fsm_aware_event_handler import FSMAwareEventHandler

View File

@ -0,0 +1,30 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from abc import ABC
from tfw.event_handler_base.event_handler_base import EventHandlerBase
from tfw.crypto import message_checksum
class BroadcastingEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which broadcast responses
and intelligently ignore their own broadcasted messages they receive.
"""
def __init__(self, key):
super().__init__(key)
self.own_message_hashes = []
def event_handler_callback(self, message):
message_hash = message_checksum(message)
if message_hash in self.own_message_hashes:
self.own_message_hashes.remove(message_hash)
return
response = self.dispatch_handling(message)
if response:
self.own_message_hashes.append(message_checksum(response))
self.server_connector.broadcast(response)

View File

@ -5,8 +5,7 @@ from abc import ABC, abstractmethod
from inspect import currentframe from inspect import currentframe
from typing import Iterable from typing import Iterable
from tfw.networking.event_handlers import ServerConnector from tfw.networking.event_handlers.server_connector import ServerConnector
from tfw.crypto import message_checksum, KeyManager, verify_message
from tfw.config.logs import logging from tfw.config.logs import logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -125,64 +124,3 @@ class EventHandlerBase(ABC):
instance for instance in locals_values instance for instance in locals_values
if isinstance(instance, cls) if isinstance(instance, cls)
} }
class FSMAwareEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which automatically
keep track of the state of the TFW FSM.
"""
def __init__(self, key):
super().__init__(key)
self.subscribe('fsm_update')
self.fsm_state = None
self.in_accepted_state = False
self._auth_key = KeyManager().auth_key
def dispatch_handling(self, message):
if message['key'] == 'fsm_update':
if verify_message(self._auth_key, message):
self._handle_fsm_update(message)
return None
return super().dispatch_handling(message)
def _handle_fsm_update(self, message):
try:
new_state = message['data']['current_state']
trigger = message['data']['last_trigger']
if self.fsm_state != new_state:
self.handle_fsm_step(self.fsm_state, new_state, trigger)
self.fsm_state = new_state
self.in_accepted_state = message['data']['in_accepted_state']
except KeyError:
LOG.error('Invalid fsm_update message received!')
def handle_fsm_step(self, from_state, to_state, trigger):
"""
Called in case the TFW FSM has stepped.
"""
pass
class BroadcastingEventHandler(EventHandlerBase, ABC):
# pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which broadcast responses
and intelligently ignore their own broadcasted messages they receive.
"""
def __init__(self, key):
super().__init__(key)
self.own_message_hashes = []
def event_handler_callback(self, message):
message_hash = message_checksum(message)
if message_hash in self.own_message_hashes:
self.own_message_hashes.remove(message_hash)
return
response = self.dispatch_handling(message)
if response:
self.own_message_hashes.append(message_checksum(response))
self.server_connector.broadcast(response)

View File

@ -0,0 +1,24 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from abc import ABC
from tfw.event_handler_base.event_handler_base import EventHandlerBase
from tfw.networking.fsm_aware import FSMAware
class FSMAwareEventHandler(EventHandlerBase, FSMAware, ABC):
# pylint: disable=abstract-method
"""
Abstract base class for EventHandlers which automatically
keep track of the state of the TFW FSM.
"""
def __init__(self, key):
EventHandlerBase.__init__(self, key)
FSMAware.__init__(self)
self.subscribe('fsm_update')
def dispatch_handling(self, message):
if self.update_fsm_data(message):
return None
return super().dispatch_handling(message)

6
lib/tfw/fsm/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from .fsm_base import FSMBase
from .linear_fsm import LinearFSM
from .yaml_fsm import YamlFSM

View File

@ -2,10 +2,11 @@
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from collections import defaultdict from collections import defaultdict
from datetime import datetime
from transitions import Machine, MachineError from transitions import Machine, MachineError
from tfw.mixins import CallbackMixin from tfw.mixins.callback_mixin import CallbackMixin
from tfw.config.logs import logging from tfw.config.logs import logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -22,9 +23,14 @@ class FSMBase(Machine, CallbackMixin):
states, transitions = [], [] states, transitions = [], []
def __init__(self, initial=None, accepted_states=None): def __init__(self, initial=None, accepted_states=None):
"""
:param initial: which state to begin with, defaults to the last one
:param accepted_states: list of states in which the challenge should be
considered successfully completed
"""
self.accepted_states = accepted_states or [self.states[-1].name] self.accepted_states = accepted_states or [self.states[-1].name]
self.trigger_predicates = defaultdict(list) self.trigger_predicates = defaultdict(list)
self.trigger_history = [] self.event_log = []
Machine.__init__( Machine.__init__(
self, self,
@ -60,9 +66,22 @@ class FSMBase(Machine, CallbackMixin):
if all(predicate_results): if all(predicate_results):
try: try:
from_state = self.state
self.trigger(trigger) self.trigger(trigger)
self.trigger_history.append(trigger) self.update_event_log(from_state, trigger)
return True return True
except (AttributeError, MachineError): except (AttributeError, MachineError):
LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger) LOG.debug('FSM failed to execute nonexistent trigger: "%s"', trigger)
return False return False
def update_event_log(self, from_state, trigger):
self.event_log.append({
'from_state': from_state,
'to_state': self.state,
'trigger': trigger,
'timestamp': datetime.utcnow()
})
@property
def in_accepted_state(self):
return self.state in self.accepted_states

View File

@ -3,7 +3,7 @@
from transitions import State from transitions import State
from .fsm_base import FSMBase from tfw.fsm.fsm_base import FSMBase
class LinearFSM(FSMBase): class LinearFSM(FSMBase):
@ -12,9 +12,13 @@ class LinearFSM(FSMBase):
This is a state machine for challenges with linear progression, consisting of This is a state machine for challenges with linear progression, consisting of
a number of steps specified in the constructor. It automatically sets up 2 a number of steps specified in the constructor. It automatically sets up 2
actions (triggers) between states as such: actions (triggers) between states as such:
(0) -- step_1 --> (1) -- step_2 --> (2) -- step_3 --> (3) ... and so on (0) -- step_1 --> (1) -- step_2 --> (2) -- step_3 --> (3) ...
(0) -- step_next --> (1) -- step_next --> (2) -- step_next --> (3) ...
""" """
def __init__(self, number_of_steps): def __init__(self, number_of_steps):
"""
:param number_of_steps: how many states this FSM should have
"""
self.states = [State(name=str(index)) for index in range(number_of_steps)] self.states = [State(name=str(index)) for index in range(number_of_steps)]
self.transitions = [] self.transitions = []
for state in self.states[:-1]: for state in self.states[:-1]:

View File

@ -1,3 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from subprocess import Popen, run from subprocess import Popen, run
from functools import partial, singledispatch from functools import partial, singledispatch
from contextlib import suppress from contextlib import suppress
@ -6,11 +9,21 @@ import yaml
import jinja2 import jinja2
from transitions import State from transitions import State
from tfw import FSMBase from tfw.fsm.fsm_base import FSMBase
class YamlFSM(FSMBase): class YamlFSM(FSMBase):
"""
This is a state machine capable of building itself from a YAML config file.
"""
def __init__(self, config_file, jinja2_variables=None): def __init__(self, config_file, jinja2_variables=None):
"""
:param config_file: path of the YAML file
:param jinja2_variables: dict containing jinja2 variables
or str with filename of YAML file to
parse and use as dict.
jinja2 support is disabled if this is None
"""
self.config = ConfigParser(config_file, jinja2_variables).config self.config = ConfigParser(config_file, jinja2_variables).config
self.setup_states() self.setup_states()
super().__init__() # FSMBase.__init__() requires states super().__init__() # FSMBase.__init__() requires states
@ -45,7 +58,7 @@ class YamlFSM(FSMBase):
partial( partial(
command_statuscode_is_zero, command_statuscode_is_zero,
predicate predicate
) )
) )
with suppress(KeyError): with suppress(KeyError):

View File

@ -1,7 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .supervisor_mixin import SupervisorMixin, SupervisorLogMixin
from .callback_mixin import CallbackMixin
from .observer_mixin import ObserverMixin
from .monitor_manager_mixin import MonitorManagerMixin

View File

@ -3,7 +3,7 @@
from functools import partial from functools import partial
from tfw.decorators import lazy_property from tfw.decorators.lazy_property import lazy_property
class CallbackMixin: class CallbackMixin:

View File

@ -3,7 +3,7 @@
from watchdog.observers import Observer from watchdog.observers import Observer
from tfw.decorators import lazy_property from tfw.decorators.lazy_property import lazy_property
class ObserverMixin: class ObserverMixin:

View File

@ -6,7 +6,7 @@ from xmlrpc.client import Fault as SupervisorFault
from contextlib import suppress from contextlib import suppress
from os import remove from os import remove
from tfw.decorators import lazy_property from tfw.decorators.lazy_property import lazy_property
from tfw.config import TFWENV from tfw.config import TFWENV

View File

@ -1,9 +1,6 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .serialization import serialize_tfw_msg, deserialize_tfw_msg
from .serialization import with_deserialize_tfw_msg, message_bytes
from .zmq_connector_base import ZMQConnectorBase
from .message_sender import MessageSender from .message_sender import MessageSender
from .event_handlers.server_connector import ServerUplinkConnector as TFWServerConnector from .event_handlers.server_connector import ServerUplinkConnector as TFWServerConnector
from .server.tfw_server import TFWServer from .server.tfw_server import TFWServer

View File

@ -1,4 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .server_connector import ServerConnector, ServerUplinkConnector, ServerDownlinkConnector

View File

@ -6,8 +6,8 @@ from functools import partial
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking import serialize_tfw_msg, with_deserialize_tfw_msg from tfw.networking.zmq_connector_base import ZMQConnectorBase
from tfw.networking import ZMQConnectorBase from tfw.networking.serialization import serialize_tfw_msg, with_deserialize_tfw_msg
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.config.logs import logging from tfw.config.logs import logging

View File

@ -0,0 +1,46 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details.
from tfw.crypto import KeyManager, verify_message
from tfw.config.logs import logging
LOG = logging.getLogger(__name__)
class FSMAware:
"""
Base class for stuff that has to be aware of the framework FSM.
This is done by processing 'fsm_update' messages.
"""
def __init__(self):
self.fsm_state = None
self.fsm_in_accepted_state = False
self.fsm_event_log = []
self._auth_key = KeyManager().auth_key
def update_fsm_data(self, message):
if message['key'] == 'fsm_update' and verify_message(self._auth_key, message):
self._handle_fsm_update(message)
return True
return False
def _handle_fsm_update(self, message):
try:
update_data = message['data']
new_state = update_data['current_state']
if self.fsm_state != new_state:
self.handle_fsm_step(**update_data)
self.fsm_state = new_state
self.fsm_in_accepted_state = update_data['in_accepted_state']
self.fsm_event_log.append(update_data)
except KeyError:
LOG.error('Invalid fsm_update message received!')
def handle_fsm_step(self, **kwargs):
"""
Called in case the TFW FSM has stepped.
:param kwargs: fsm_update 'data' field
"""
pass

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from tfw.networking.event_handlers import ServerUplinkConnector from tfw.networking.event_handlers.server_connector import ServerUplinkConnector
class MessageSender: class MessageSender:

View File

@ -1,5 +1,2 @@
# Copyright (C) 2018 Avatao.com Innovative Learning Kft. # Copyright (C) 2018 Avatao.com Innovative Learning Kft.
# All Rights Reserved. See LICENSE file for details. # All Rights Reserved. See LICENSE file for details.
from .event_handler_connector import EventHandlerConnector, EventHandlerUplinkConnector, EventHandlerDownlinkConnector
from .tfw_server import TFWServer

View File

@ -4,7 +4,8 @@
import zmq import zmq
from zmq.eventloop.zmqstream import ZMQStream from zmq.eventloop.zmqstream import ZMQStream
from tfw.networking import ZMQConnectorBase, serialize_tfw_msg, with_deserialize_tfw_msg from tfw.networking.zmq_connector_base import ZMQConnectorBase
from tfw.networking.serialization import serialize_tfw_msg, with_deserialize_tfw_msg
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.config.logs import logging from tfw.config.logs import logging

View File

@ -6,23 +6,25 @@ from contextlib import suppress
from tornado.web import Application from tornado.web import Application
from tfw.networking.event_handlers import ServerUplinkConnector from tfw.networking.server.zmq_websocket_proxy import ZMQWebSocketProxy
from tfw.networking.server import EventHandlerConnector from tfw.networking.event_handlers.server_connector import ServerUplinkConnector
from tfw.networking import MessageSender from tfw.networking.server.event_handler_connector import EventHandlerConnector
from tfw.networking.message_sender import MessageSender
from tfw.networking.fsm_aware import FSMAware
from tfw.crypto import KeyManager, verify_message, sign_message from tfw.crypto import KeyManager, verify_message, sign_message
from tfw.config.logs import logging from tfw.config.logs import logging
from .zmq_websocket_proxy import ZMQWebSocketProxy
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class TFWServer: class TFWServer(FSMAware):
""" """
This class handles the proxying of messages between the frontend and event handers. This class handles the proxying of messages between the frontend and event handers.
It proxies messages from the "/ws" route to all event handlers subscribed to a ZMQ It proxies messages from the "/ws" route to all event handlers subscribed to a ZMQ
SUB socket. SUB socket.
""" """
def __init__(self): def __init__(self):
super().__init__()
self._event_handler_connector = EventHandlerConnector() self._event_handler_connector = EventHandlerConnector()
self._uplink_connector = ServerUplinkConnector() self._uplink_connector = ServerUplinkConnector()
self._auth_key = KeyManager().auth_key self._auth_key = KeyManager().auth_key
@ -30,9 +32,16 @@ class TFWServer:
self.application = Application([( self.application = Application([(
r'/ws', ZMQWebSocketProxy, { r'/ws', ZMQWebSocketProxy, {
'event_handler_connector': self._event_handler_connector, 'event_handler_connector': self._event_handler_connector,
'message_handlers': [self.handle_trigger, self.handle_recover], 'proxy_filters_and_callbacks': {
'frontend_message_handlers': [self.save_frontend_messages] 'message_handlers': [
})]) self.handle_trigger,
self.handle_recover,
self.handle_fsm_update
],
'frontend_message_handlers': [self.save_frontend_messages]
}
}
)])
self._frontend_messages = FrontendMessageStorage() self._frontend_messages = FrontendMessageStorage()
@ -55,6 +64,9 @@ class TFWServer:
self._frontend_messages.replay_messages(self._uplink_connector) self._frontend_messages.replay_messages(self._uplink_connector)
self._frontend_messages.clear() self._frontend_messages.clear()
def handle_fsm_update(self, message):
self.update_fsm_data(message)
def save_frontend_messages(self, message): def save_frontend_messages(self, message):
self._frontend_messages.save_message(message) self._frontend_messages.save_message(message)

View File

@ -5,7 +5,7 @@ import json
from tornado.websocket import WebSocketHandler from tornado.websocket import WebSocketHandler
from tfw.mixins import CallbackMixin from tfw.mixins.callback_mixin import CallbackMixin
from tfw.config.logs import logging from tfw.config.logs import logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -14,14 +14,11 @@ LOG = logging.getLogger(__name__)
class ZMQWebSocketProxy(WebSocketHandler): class ZMQWebSocketProxy(WebSocketHandler):
# pylint: disable=abstract-method # pylint: disable=abstract-method
instances = set() instances = set()
sequence_number = 0
def initialize(self, **kwargs): # pylint: disable=arguments-differ def initialize(self, **kwargs): # pylint: disable=arguments-differ
self._event_handler_connector = kwargs['event_handler_connector'] self._event_handler_connector = kwargs['event_handler_connector']
self._proxy_filters_and_callbacks = kwargs.get('proxy_filters_and_callbacks', {})
self._message_handlers = kwargs.get('message_handlers', [])
self._frontend_message_handlers = kwargs.get('frontend_message_handlers', [])
self._eventhandler_message_handlers = kwargs.get('eventhandler_message_handlers', [])
self._proxy_filters = kwargs.get('proxy_filters', [])
self.proxy_eventhandler_to_websocket = TFWProxy( self.proxy_eventhandler_to_websocket = TFWProxy(
self.send_eventhandler_message, self.send_eventhandler_message,
@ -35,14 +32,19 @@ class ZMQWebSocketProxy(WebSocketHandler):
self.subscribe_proxy_callbacks() self.subscribe_proxy_callbacks()
def subscribe_proxy_callbacks(self): def subscribe_proxy_callbacks(self):
eventhandler_message_handlers = self._proxy_filters_and_callbacks.get('eventhandler_message_handlers', [])
frontend_message_handlers = self._proxy_filters_and_callbacks.get('frontend_message_handlers', [])
message_handlers = self._proxy_filters_and_callbacks.get('message_handlers', [])
proxy_filters = self._proxy_filters_and_callbacks.get('proxy_filters', [])
self.proxy_websocket_to_eventhandler.subscribe_proxy_callbacks_and_filters( self.proxy_websocket_to_eventhandler.subscribe_proxy_callbacks_and_filters(
self._eventhandler_message_handlers + self._message_handlers, eventhandler_message_handlers + message_handlers,
self._proxy_filters proxy_filters
) )
self.proxy_eventhandler_to_websocket.subscribe_proxy_callbacks_and_filters( self.proxy_eventhandler_to_websocket.subscribe_proxy_callbacks_and_filters(
self._frontend_message_handlers + self._message_handlers, frontend_message_handlers + message_handlers,
self._proxy_filters proxy_filters
) )
def prepare(self): def prepare(self):
@ -59,14 +61,21 @@ class ZMQWebSocketProxy(WebSocketHandler):
""" """
Invoked on ZMQ messages from event handlers. Invoked on ZMQ messages from event handlers.
""" """
self.sequence_message(message)
LOG.debug('Received on pull socket: %s', message) LOG.debug('Received on pull socket: %s', message)
self.proxy_eventhandler_to_websocket(message) self.proxy_eventhandler_to_websocket(message)
@classmethod
def sequence_message(cls, message):
cls.sequence_number += 1
message['seq'] = cls.sequence_number
def on_message(self, message): def on_message(self, message):
""" """
Invoked on WS messages from frontend. Invoked on WS messages from frontend.
""" """
message = json.loads(message) message = json.loads(message)
self.sequence_message(message)
LOG.debug('Received on WebSocket: %s', message) LOG.debug('Received on WebSocket: %s', message)
self.proxy_websocket_to_eventhandler(message) self.proxy_websocket_to_eventhandler(message)
@ -105,14 +114,9 @@ class TFWProxy:
raise ValueError('Invalid TFW message format!') raise ValueError('Invalid TFW message format!')
def __call__(self, message): def __call__(self, message):
try: if not self.filter_and_execute_callbacks(message):
self.proxy_filters._execute_callbacks(message)
except ValueError:
LOG.exception('Invalid TFW message received!')
return return
self.proxy_callbacks._execute_callbacks(message)
if message['key'] not in self.keyhandlers: if message['key'] not in self.keyhandlers:
self.to_destination(message) self.to_destination(message)
else: else:
@ -122,13 +126,26 @@ class TFWProxy:
except KeyError: except KeyError:
LOG.error('Invalid "%s" message format! Ignoring.', handler.__name__) LOG.error('Invalid "%s" message format! Ignoring.', handler.__name__)
def filter_and_execute_callbacks(self, message):
try:
self.proxy_filters._execute_callbacks(message)
self.proxy_callbacks._execute_callbacks(message)
return True
except ValueError:
LOG.exception('Invalid TFW message received!')
return False
def mirror(self, message): def mirror(self, message):
message = message['data'] message = message['data']
if not self.filter_and_execute_callbacks(message):
return
LOG.debug('Mirroring message: %s', message) LOG.debug('Mirroring message: %s', message)
self.to_source(message) self.to_source(message)
def broadcast(self, message): def broadcast(self, message):
message = message['data'] message = message['data']
if not self.filter_and_execute_callbacks(message):
return
LOG.debug('Broadcasting message: %s', message) LOG.debug('Broadcasting message: %s', message)
self.to_source(message) self.to_source(message)
self.to_destination(message) self.to_destination(message)

View File

@ -1,8 +1,9 @@
tornado==5.0 tornado==5.1
pyzmq==17.0.0 pyzmq==17.1.2
transitions==0.6.4 transitions==0.6.6
terminado==0.8.1 terminado==0.8.1
watchdog==0.8.3 watchdog==0.8.3
PyYAML==3.12 PyYAML==3.13
Jinja2==2.10 Jinja2==2.10
cryptography==2.2.2 cryptography==2.3.1
python-dateutil==2.7.3