multivisor 6.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. multivisor/__init__.py +0 -0
  2. multivisor/client/__init__.py +0 -0
  3. multivisor/client/cli.py +29 -0
  4. multivisor/client/http.py +93 -0
  5. multivisor/client/repl.py +244 -0
  6. multivisor/client/util.py +58 -0
  7. multivisor/multivisor.py +470 -0
  8. multivisor/rpc.py +232 -0
  9. multivisor/server/__init__.py +0 -0
  10. multivisor/server/dist/index.html +1 -0
  11. multivisor/server/dist/static/css/app.2aff3580a128440bba89b94112292cb1.css +6 -0
  12. multivisor/server/dist/static/css/app.2aff3580a128440bba89b94112292cb1.css.map +1 -0
  13. multivisor/server/dist/static/js/app.52791f915c2f060b9cb1.js +2 -0
  14. multivisor/server/dist/static/js/app.52791f915c2f060b9cb1.js.map +1 -0
  15. multivisor/server/dist/static/js/manifest.2ae2e69a05c33dfc65f8.js +2 -0
  16. multivisor/server/dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map +1 -0
  17. multivisor/server/dist/static/js/vendor.1d02877727062a41e9fb.js +1319 -0
  18. multivisor/server/dist/static/js/vendor.1d02877727062a41e9fb.js.map +1 -0
  19. multivisor/server/rpc.py +156 -0
  20. multivisor/server/tests/__init__.py +0 -0
  21. multivisor/server/tests/conftest.py +1 -0
  22. multivisor/server/tests/test_web.py +194 -0
  23. multivisor/server/util.py +70 -0
  24. multivisor/server/web.py +327 -0
  25. multivisor/signals.py +5 -0
  26. multivisor/tests/__init__.py +0 -0
  27. multivisor/tests/test_multivisor.py +179 -0
  28. multivisor/util.py +75 -0
  29. multivisor-6.0.2.dist-info/METADATA +375 -0
  30. multivisor-6.0.2.dist-info/RECORD +34 -0
  31. multivisor-6.0.2.dist-info/WHEEL +5 -0
  32. multivisor-6.0.2.dist-info/entry_points.txt +4 -0
  33. multivisor-6.0.2.dist-info/licenses/LICENSE +674 -0
  34. multivisor-6.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,156 @@
1
+ from __future__ import print_function
2
+
3
+ import sys
4
+ import logging
5
+ import xmlrpc.client
6
+
7
+ from os import environ
8
+ from functools import partial
9
+
10
+ from gevent import spawn
11
+ from gevent.lock import RLock
12
+ from gevent.queue import Queue
13
+ from gevent.fileobject import FileObject
14
+ from zerorpc import stream, Server, LostRemote
15
+ from supervisor.childutils import getRPCInterface
16
+
17
+
18
+ READY = "READY\n"
19
+ ACKNOWLEDGED = "RESULT 2\nOK"
20
+ DEFAULT_BIND = "tcp://*:9002"
21
+
22
+
23
+ def signal(stream, msg):
24
+ stream.write(msg)
25
+ stream.flush()
26
+
27
+
28
+ def wait_for_event(stream):
29
+ header_line = stream.readline()
30
+ event = dict((x.split(":") for x in header_line.split()))
31
+ payload_str = stream.read(int(event["len"]))
32
+ event["payload"] = dict((x.split(":") for x in payload_str.split()))
33
+ return event
34
+
35
+
36
+ def event_producer_loop(dispatch):
37
+ istream = FileObject(sys.stdin)
38
+ ostream = FileObject(sys.stdout, mode='w')
39
+ while True:
40
+ signal(ostream, READY)
41
+ event = wait_for_event(istream)
42
+ dispatch(event)
43
+ signal(ostream, ACKNOWLEDGED)
44
+
45
+
46
+ def event_consumer_loop(queue, handler):
47
+ for event in queue:
48
+ try:
49
+ handler(event)
50
+ except:
51
+ logging.exception("Error processing %s", event)
52
+
53
+
54
+ get_rpc = partial(getRPCInterface, environ)
55
+
56
+
57
+ def build_method(supervisor, name):
58
+ subsystem_name, func_name = name.split(".", 1)
59
+ def method(*args):
60
+ subsystem = getattr(supervisor.rpc, subsystem_name)
61
+ with supervisor.lock:
62
+ return getattr(subsystem, func_name)(*args)
63
+ method.__name__ = func_name
64
+ return func_name, method
65
+
66
+
67
+ class Supervisor(object):
68
+
69
+ def __init__(self, xml_rpc):
70
+ self.event_channels = set()
71
+ self.lock = RLock()
72
+ self.rpc = xml_rpc
73
+ for name in self.rpc.system.listMethods():
74
+ setattr(self, *build_method(self, name))
75
+
76
+ @stream
77
+ def event_stream(self):
78
+ logging.info("client connected to stream")
79
+ channel = Queue()
80
+ self.event_channels.add(channel)
81
+ try:
82
+ yield "First event to trigger connection. Please ignore me!"
83
+ for event in channel:
84
+ yield event
85
+ except LostRemote as e:
86
+ logging.info("remote end of stream disconnected")
87
+ finally:
88
+ self.event_channels.remove(channel)
89
+
90
+ def publish_event(self, event):
91
+ name = event["eventname"]
92
+ if name.startswith("TICK"):
93
+ return
94
+ event = dict(event)
95
+ if name.startswith("PROCESS_STATE"):
96
+ payload = event["payload"]
97
+ pname = "{}:{}".format(payload["groupname"], payload["processname"])
98
+ logging.info("handling %s of %s", name, pname)
99
+ try:
100
+ payload["process"] = self.getProcessInfo(pname)
101
+ except xmlrpc.client.Fault:
102
+ # probably supervisor is shutting down
103
+ logging.warn("probably shutting down...")
104
+ return
105
+ elif not name.startswith("SUPERVISOR_STATE"):
106
+ logging.warning("ignored %s", name)
107
+ return
108
+ for channel in self.event_channels:
109
+ channel.put(event)
110
+
111
+
112
+ def run(xml_rpc, bind=DEFAULT_BIND):
113
+ channel = Queue()
114
+ supervisor = Supervisor(xml_rpc)
115
+ t1 = spawn(event_consumer_loop, channel, supervisor.publish_event)
116
+ t2 = spawn(event_producer_loop, channel.put)
117
+ server = Server(supervisor)
118
+ server.bind(bind)
119
+ server.run()
120
+
121
+
122
+ def main(args=None):
123
+ import gevent.monkey
124
+
125
+ gevent.monkey.patch_all(thread=False, sys=True)
126
+
127
+ import argparse
128
+
129
+ parser = argparse.ArgumentParser()
130
+ parser.add_argument("--bind", help="bind address", default=DEFAULT_BIND)
131
+ parser.add_argument(
132
+ "--log-level",
133
+ help="log level",
134
+ type=str,
135
+ default="INFO",
136
+ choices=["DEBUG", "INFO", "WARN", "ERROR"],
137
+ )
138
+ options = parser.parse_args(args)
139
+
140
+ log_level = getattr(logging, options.log_level.upper())
141
+ log_fmt = "%(levelname)s %(asctime)-15s %(name)s: %(message)s"
142
+ logging.basicConfig(level=log_level, format=log_fmt)
143
+
144
+ bind = options.bind
145
+ if "://" not in bind:
146
+ bind = "tcp://" + bind
147
+ try:
148
+ rpc = get_rpc()
149
+ except KeyError:
150
+ print("multivisor-rpc can only run as supervisor eventlistener", file=sys.stderr)
151
+ exit(1)
152
+ run(rpc, bind)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
File without changes
@@ -0,0 +1 @@
1
+ import pytest
@@ -0,0 +1,194 @@
1
+ import requests
2
+
3
+ from tests.functions import assert_fields_in_object
4
+ from tests.conftest import *
5
+
6
+
7
+ @pytest.mark.usefixtures("api_base_url")
8
+ def test_data_view(api_base_url):
9
+ url = "{}/data".format(api_base_url)
10
+ response = requests.get(url)
11
+ assert response.status_code == 200
12
+ data = response.json()
13
+ assert_fields_in_object(["name", "supervisors"], data)
14
+ assert "test001" in data["supervisors"]
15
+ supervisor = data["supervisors"]["test001"]
16
+ assert_fields_in_object(
17
+ [
18
+ "processes",
19
+ "name",
20
+ "url",
21
+ "pid",
22
+ "running",
23
+ "host",
24
+ "version",
25
+ "identification",
26
+ "supervisor_version",
27
+ "api_version",
28
+ ],
29
+ supervisor,
30
+ )
31
+
32
+ assert supervisor["running"]
33
+ processes = supervisor["processes"]
34
+ for name, process in processes.items():
35
+ assert_fields_in_object(
36
+ [
37
+ "stderr_logfile",
38
+ "description",
39
+ "statename",
40
+ "pid",
41
+ "stdout_logfile",
42
+ "full_name",
43
+ "supervisor",
44
+ "logfile",
45
+ "exitstatus",
46
+ ],
47
+ process,
48
+ )
49
+
50
+
51
+ @pytest.mark.usefixtures("api_base_url")
52
+ def test_config_view(api_base_url):
53
+ url = "{}/config/file".format(api_base_url)
54
+ response = requests.get(url)
55
+ assert response.status_code == 200
56
+ data = response.json()
57
+ assert "content" in data
58
+
59
+
60
+ @pytest.mark.usefixtures("api_base_url")
61
+ def test_list_processes_view(api_base_url):
62
+ url = "{}/process/list".format(api_base_url)
63
+ response = requests.get(url)
64
+ assert response.status_code == 200
65
+ data = response.json()
66
+ assert isinstance(data, list)
67
+ assert len(data) == 10
68
+
69
+
70
+ @pytest.mark.usefixtures("api_base_url")
71
+ def test_process_info_view(api_base_url):
72
+ uid = "test001:PLC:wcid00d"
73
+ url = "{}/process/info/{}".format(api_base_url, uid)
74
+ response = requests.get(url)
75
+ assert response.status_code == 200
76
+ process_data = response.json()
77
+ keys = [
78
+ u"logfile",
79
+ u"statename",
80
+ u"group",
81
+ u"description",
82
+ u"pid",
83
+ u"stderr_logfile",
84
+ u"stop",
85
+ u"running",
86
+ u"name",
87
+ u"start",
88
+ u"state",
89
+ u"spawnerr",
90
+ u"full_name",
91
+ u"host",
92
+ u"supervisor",
93
+ u"now",
94
+ u"exitstatus",
95
+ u"stdout_logfile",
96
+ u"uid",
97
+ ]
98
+
99
+ assert_fields_in_object(keys, process_data)
100
+ assert process_data["uid"] == uid
101
+ assert process_data["supervisor"] == "test001"
102
+ assert process_data["group"] == "PLC"
103
+ assert process_data["name"] == "wcid00d"
104
+
105
+
106
+ @pytest.mark.usefixtures("api_base_url")
107
+ def test_supervisor_info_view(api_base_url):
108
+ uid = "test001"
109
+ url = "{}/supervisor/info/{}".format(api_base_url, uid)
110
+ response = requests.get(url)
111
+ assert response.status_code == 200
112
+ supervisor_data = response.json()
113
+ keys = [
114
+ u"processes",
115
+ u"name",
116
+ u"url",
117
+ u"pid",
118
+ u"running",
119
+ u"host",
120
+ u"version",
121
+ u"identification",
122
+ u"supervisor_version",
123
+ u"api_version",
124
+ ]
125
+
126
+ assert_fields_in_object(keys, supervisor_data)
127
+ assert supervisor_data["name"] == uid
128
+ assert len(supervisor_data["processes"]) == 10
129
+
130
+
131
+ @pytest.mark.usefixtures("api_base_url")
132
+ def test_reload_view(api_base_url):
133
+ url = "{}/admin/reload".format(api_base_url)
134
+ response = requests.get(url)
135
+ assert response.status_code == 200
136
+
137
+
138
+ @pytest.mark.usefixtures("api_base_url")
139
+ def test_refresh_view(api_base_url):
140
+ url = "{}/refresh".format(api_base_url)
141
+ response = requests.get(url)
142
+ assert response.status_code == 200
143
+
144
+
145
+ @pytest.mark.usefixtures("api_base_url")
146
+ def test_stop_process_view(api_base_url, multivisor_instance):
147
+ multivisor_instance.refresh() # processes are empty before calling this
148
+ uid = "test001:PLC:wcid00d"
149
+ process = multivisor_instance.get_process(uid)
150
+ # assert process is currently running
151
+ index, max_retries = 0, 10
152
+ while not process["running"]:
153
+ multivisor_instance.refresh()
154
+ process = multivisor_instance.get_process(uid)
155
+ sleep(0.5)
156
+ if index == max_retries:
157
+ raise AssertionError("Process {} is not running".format(uid))
158
+ index += 1
159
+
160
+ # stop the process
161
+ url = "{}/process/stop".format(api_base_url)
162
+ response = requests.post(url, {"uid": uid})
163
+ assert response.status_code == 200
164
+
165
+ # assert process is stopped
166
+ index, max_retries = 0, 10
167
+ while process["running"]:
168
+ multivisor_instance.refresh()
169
+ process = multivisor_instance.get_process(uid)
170
+ sleep(0.5)
171
+ if index == max_retries:
172
+ raise AssertionError("Process {} is not stopped".format(uid))
173
+ index += 1
174
+
175
+
176
+ @pytest.mark.usefixtures("api_base_url")
177
+ def test_restart_process_view(api_base_url, multivisor_instance):
178
+ multivisor_instance.refresh() # processes are empty before calling this
179
+ uid = "test001:PLC:wcid00d"
180
+ process = multivisor_instance.get_process(uid)
181
+ # stop process if it's currently running
182
+ if process["running"]:
183
+ multivisor_instance.stop_processes(uid)
184
+ assert not process["running"]
185
+
186
+ # restart the process
187
+ url = "{}/process/restart".format(api_base_url)
188
+ response = requests.post(url, {"uid": uid})
189
+ assert response.status_code == 200
190
+
191
+ # assert process is restarted
192
+ multivisor_instance.refresh()
193
+ process = multivisor_instance.get_process(uid)
194
+ assert process["running"]
@@ -0,0 +1,70 @@
1
+ import hashlib
2
+ import functools
3
+
4
+ from flask import session, abort
5
+
6
+
7
+ def is_login_valid(app, username, password):
8
+ username = username.strip()
9
+ password = password.strip()
10
+
11
+ correct_username = app.multivisor.config["username"]
12
+ correct_password = app.multivisor.config["password"]
13
+ return constant_time_compare(username, correct_username) and constant_time_compare(
14
+ password, correct_password
15
+ )
16
+
17
+
18
+ def constant_time_compare(val1, val2):
19
+ """
20
+ Returns True if the two strings are equal, False otherwise.
21
+
22
+ The time taken is independent of the number of characters that match.
23
+
24
+ For the sake of simplicity, this function executes in constant time only
25
+ when the two strings have the same length. It short-circuits when they
26
+ have different lengths.
27
+
28
+ Taken from Django Source Code
29
+ """
30
+ val1 = hashlib.sha1(_safe_encode(val1)).hexdigest()
31
+ if val2.startswith("{SHA}"): # password can be specified as SHA-1 hash in config
32
+ val2 = val2.split("{SHA}")[1]
33
+ else:
34
+ val2 = hashlib.sha1(_safe_encode(val2)).hexdigest()
35
+ if len(val1) != len(val2):
36
+ return False
37
+ result = 0
38
+ for x, y in zip(val1, val2):
39
+ result |= ord(x) ^ ord(y)
40
+ return result == 0
41
+
42
+
43
+ def _safe_encode(data):
44
+ """Safely encode @data string to utf-8"""
45
+ try:
46
+ result = data.encode("utf-8")
47
+ except (UnicodeDecodeError, UnicodeEncodeError, AttributeError):
48
+ result = data
49
+ return result
50
+
51
+
52
+ def login_required(app):
53
+ """
54
+ Decorator to mark view as requiring being logged in
55
+ """
56
+
57
+ def decorator(func):
58
+ @functools.wraps(func)
59
+ def wrapper_login_required(*args, **kwargs):
60
+ auth_on = app.multivisor.use_authentication
61
+
62
+ if not auth_on or "username" in session:
63
+ return func(*args, **kwargs)
64
+
65
+ # user not authenticated, return 401
66
+ abort(401)
67
+
68
+ return wrapper_login_required
69
+
70
+ return decorator