cleepcli 1.43.10__tar.gz → 1.44.2__tar.gz
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.
- {cleepcli-1.43.10/cleepcli.egg-info → cleepcli-1.44.2}/PKG-INFO +1 -1
- {cleepcli-1.43.10 → cleepcli-1.44.2}/bin/ccli +33 -16
- {cleepcli-1.43.10 → cleepcli-1.44.2}/bin/cleep-cli +33 -16
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/ci.py +3 -1
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/cleepapi.py +158 -6
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/console.py +38 -48
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/file.py +67 -38
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/git.py +2 -2
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/module.py +1 -1
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/version.py +1 -1
- cleepcli-1.44.2/cleepcli/watch.py +573 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2/cleepcli.egg-info}/PKG-INFO +1 -1
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/SOURCES.txt +3 -1
- cleepcli-1.44.2/tests/test_console.py +51 -0
- cleepcli-1.44.2/tests/test_watch.py +314 -0
- cleepcli-1.43.10/cleepcli/watch.py +0 -375
- {cleepcli-1.43.10 → cleepcli-1.44.2}/LICENSE +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/MANIFEST.in +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/README.md +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/__init__.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/check.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/config.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/docs.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/package.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/test.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli/tools.py +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/dependency_links.txt +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/requires.txt +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/cleepcli.egg-info/top_level.txt +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/pyproject.toml +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/requirements.txt +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/setup.cfg +0 -0
- {cleepcli-1.43.10 → cleepcli-1.44.2}/setup.py +0 -0
|
@@ -90,7 +90,8 @@ def coreget():
|
|
|
90
90
|
|
|
91
91
|
# force coresync to synchronize installation with repo
|
|
92
92
|
f = File()
|
|
93
|
-
|
|
93
|
+
if not f.core_sync():
|
|
94
|
+
sys.exit(1)
|
|
94
95
|
|
|
95
96
|
# core modules
|
|
96
97
|
if not get_core_mods():
|
|
@@ -290,12 +291,24 @@ def modssync(run_scripts):
|
|
|
290
291
|
f = File()
|
|
291
292
|
|
|
292
293
|
for module in config.MODULES_REPO_URL.keys():
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
294
|
+
if run_scripts:
|
|
295
|
+
# Same order as package install: scripts + preinst, then files, then postinst
|
|
296
|
+
res = f.module_sync(module, backend=False, frontend=False, scripts=True)
|
|
297
|
+
if not res:
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
res = f.module_run_install_script(module, "preinst.sh")
|
|
300
|
+
if not res:
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
res = f.module_sync(module, backend=True, frontend=True, scripts=True)
|
|
303
|
+
if not res:
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
res = f.module_run_install_script(module, "postinst.sh")
|
|
306
|
+
if not res:
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
else:
|
|
309
|
+
res = f.module_sync(module)
|
|
310
|
+
if not res:
|
|
311
|
+
sys.exit(1)
|
|
299
312
|
|
|
300
313
|
logging.info('Done')
|
|
301
314
|
|
|
@@ -899,17 +912,21 @@ def cli():
|
|
|
899
912
|
|
|
900
913
|
|
|
901
914
|
if __name__ == '__main__':
|
|
902
|
-
def
|
|
903
|
-
if platform.system()!='Linux':
|
|
915
|
+
def is_raspberry_pi_os():
|
|
916
|
+
if platform.system() != 'Linux':
|
|
904
917
|
return False
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
918
|
+
try:
|
|
919
|
+
with open('/etc/os-release', 'r', encoding='utf-8') as handle:
|
|
920
|
+
content = handle.read().lower()
|
|
921
|
+
except OSError:
|
|
908
922
|
return False
|
|
909
|
-
return
|
|
923
|
+
return any(
|
|
924
|
+
token in content for token in ('raspbian', 'raspios', 'raspberry')
|
|
925
|
+
)
|
|
910
926
|
|
|
911
|
-
# execute only on
|
|
912
|
-
if
|
|
927
|
+
# execute only on Raspberry Pi OS
|
|
928
|
+
if is_raspberry_pi_os():
|
|
913
929
|
cli()
|
|
914
930
|
else:
|
|
915
|
-
click.echo('Cleep-cli runs only on
|
|
931
|
+
click.echo('Cleep-cli runs only on Raspberry Pi OS')
|
|
932
|
+
sys.exit(1)
|
|
@@ -90,7 +90,8 @@ def coreget():
|
|
|
90
90
|
|
|
91
91
|
# force coresync to synchronize installation with repo
|
|
92
92
|
f = File()
|
|
93
|
-
|
|
93
|
+
if not f.core_sync():
|
|
94
|
+
sys.exit(1)
|
|
94
95
|
|
|
95
96
|
# core modules
|
|
96
97
|
if not get_core_mods():
|
|
@@ -290,12 +291,24 @@ def modssync(run_scripts):
|
|
|
290
291
|
f = File()
|
|
291
292
|
|
|
292
293
|
for module in config.MODULES_REPO_URL.keys():
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
294
|
+
if run_scripts:
|
|
295
|
+
# Same order as package install: scripts + preinst, then files, then postinst
|
|
296
|
+
res = f.module_sync(module, backend=False, frontend=False, scripts=True)
|
|
297
|
+
if not res:
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
res = f.module_run_install_script(module, "preinst.sh")
|
|
300
|
+
if not res:
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
res = f.module_sync(module, backend=True, frontend=True, scripts=True)
|
|
303
|
+
if not res:
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
res = f.module_run_install_script(module, "postinst.sh")
|
|
306
|
+
if not res:
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
else:
|
|
309
|
+
res = f.module_sync(module)
|
|
310
|
+
if not res:
|
|
311
|
+
sys.exit(1)
|
|
299
312
|
|
|
300
313
|
logging.info('Done')
|
|
301
314
|
|
|
@@ -899,17 +912,21 @@ def cli():
|
|
|
899
912
|
|
|
900
913
|
|
|
901
914
|
if __name__ == '__main__':
|
|
902
|
-
def
|
|
903
|
-
if platform.system()!='Linux':
|
|
915
|
+
def is_raspberry_pi_os():
|
|
916
|
+
if platform.system() != 'Linux':
|
|
904
917
|
return False
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
918
|
+
try:
|
|
919
|
+
with open('/etc/os-release', 'r', encoding='utf-8') as handle:
|
|
920
|
+
content = handle.read().lower()
|
|
921
|
+
except OSError:
|
|
908
922
|
return False
|
|
909
|
-
return
|
|
923
|
+
return any(
|
|
924
|
+
token in content for token in ('raspbian', 'raspios', 'raspberry')
|
|
925
|
+
)
|
|
910
926
|
|
|
911
|
-
# execute only on
|
|
912
|
-
if
|
|
927
|
+
# execute only on Raspberry Pi OS
|
|
928
|
+
if is_raspberry_pi_os():
|
|
913
929
|
cli()
|
|
914
930
|
else:
|
|
915
|
-
click.echo('Cleep-cli runs only on
|
|
931
|
+
click.echo('Cleep-cli runs only on Raspberry Pi OS')
|
|
932
|
+
sys.exit(1)
|
|
@@ -12,7 +12,7 @@ import urllib.parse
|
|
|
12
12
|
from . import config
|
|
13
13
|
from .console import Console
|
|
14
14
|
from .check import Check
|
|
15
|
-
from .cleepapi import resolve_rpc_url
|
|
15
|
+
from .cleepapi import local_socket_available, request_over_local_socket, resolve_rpc_url
|
|
16
16
|
import subprocess
|
|
17
17
|
|
|
18
18
|
requests.packages.urllib3.disable_warnings()
|
|
@@ -284,6 +284,8 @@ class Ci:
|
|
|
284
284
|
"""
|
|
285
285
|
POST JSON to Cleep /command (self-signed TLS accepted).
|
|
286
286
|
"""
|
|
287
|
+
if local_socket_available():
|
|
288
|
+
return request_over_local_socket('POST', self.command_url, json_body=payload)
|
|
287
289
|
return requests.post(self.command_url, json=payload, verify=False)
|
|
288
290
|
|
|
289
291
|
def _rpc_get(self, url):
|
|
@@ -17,6 +17,9 @@ DEFAULT_RPC_URLS = (
|
|
|
17
17
|
'https://127.0.0.1:443',
|
|
18
18
|
'http://127.0.0.1:80',
|
|
19
19
|
)
|
|
20
|
+
# Local commands (frontend restart, docs, CI) use this socket so they
|
|
21
|
+
# stay available when TCP requires the Cleep password.
|
|
22
|
+
LOCAL_SOCKET_PATH = '/run/cleep/rpc.sock'
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def resolve_rpc_url(urls=None, timeout=3.0):
|
|
@@ -54,6 +57,144 @@ def resolve_rpc_url(urls=None, timeout=3.0):
|
|
|
54
57
|
raise Exception('Unable to reach Cleep RPC (tried %s): %s' % (', '.join(candidates), last_error))
|
|
55
58
|
|
|
56
59
|
|
|
60
|
+
def local_socket_available(path=None):
|
|
61
|
+
"""
|
|
62
|
+
Return True when the local Cleep RPC socket exists.
|
|
63
|
+
"""
|
|
64
|
+
return os.path.exists(path or LOCAL_SOCKET_PATH)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _http_path(url):
|
|
68
|
+
"""
|
|
69
|
+
Return the path and query of an RPC URL.
|
|
70
|
+
"""
|
|
71
|
+
parts = urllib.parse.urlsplit(url)
|
|
72
|
+
path = parts.path or '/'
|
|
73
|
+
if parts.query:
|
|
74
|
+
path += '?' + parts.query
|
|
75
|
+
return path
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_http_response(sock):
|
|
79
|
+
"""
|
|
80
|
+
Read one HTTP/1.1 response from a socket.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
tuple: status code (int), body (bytes)
|
|
84
|
+
"""
|
|
85
|
+
data = b''
|
|
86
|
+
while b'\r\n\r\n' not in data:
|
|
87
|
+
chunk = sock.recv(4096)
|
|
88
|
+
if not chunk:
|
|
89
|
+
break
|
|
90
|
+
data += chunk
|
|
91
|
+
if b'\r\n\r\n' not in data:
|
|
92
|
+
raise Exception('Incomplete HTTP response from local socket')
|
|
93
|
+
|
|
94
|
+
header_blob, rest = data.split(b'\r\n\r\n', 1)
|
|
95
|
+
lines = header_blob.split(b'\r\n')
|
|
96
|
+
status_code = int(lines[0].split()[1])
|
|
97
|
+
headers = {}
|
|
98
|
+
for line in lines[1:]:
|
|
99
|
+
if b':' not in line:
|
|
100
|
+
continue
|
|
101
|
+
name, value = line.split(b':', 1)
|
|
102
|
+
headers[name.decode('ascii').lower()] = value.strip().decode('ascii')
|
|
103
|
+
|
|
104
|
+
if headers.get('transfer-encoding', '').lower() == 'chunked':
|
|
105
|
+
body = _read_chunked(sock, rest)
|
|
106
|
+
elif 'content-length' in headers:
|
|
107
|
+
length = int(headers['content-length'])
|
|
108
|
+
while len(rest) < length:
|
|
109
|
+
chunk = sock.recv(4096)
|
|
110
|
+
if not chunk:
|
|
111
|
+
break
|
|
112
|
+
rest += chunk
|
|
113
|
+
body = rest[:length]
|
|
114
|
+
else:
|
|
115
|
+
while True:
|
|
116
|
+
chunk = sock.recv(4096)
|
|
117
|
+
if not chunk:
|
|
118
|
+
break
|
|
119
|
+
rest += chunk
|
|
120
|
+
body = rest
|
|
121
|
+
return status_code, body
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read_chunked(sock, pending):
|
|
125
|
+
"""
|
|
126
|
+
Decode a chunked HTTP body.
|
|
127
|
+
"""
|
|
128
|
+
data = pending
|
|
129
|
+
body = b''
|
|
130
|
+
while True:
|
|
131
|
+
while b'\r\n' not in data:
|
|
132
|
+
chunk = sock.recv(4096)
|
|
133
|
+
if not chunk:
|
|
134
|
+
return body
|
|
135
|
+
data += chunk
|
|
136
|
+
size_line, data = data.split(b'\r\n', 1)
|
|
137
|
+
size = int(size_line.split(b';', 1)[0], 16)
|
|
138
|
+
if size == 0:
|
|
139
|
+
return body
|
|
140
|
+
while len(data) < size + 2:
|
|
141
|
+
chunk = sock.recv(4096)
|
|
142
|
+
if not chunk:
|
|
143
|
+
return body
|
|
144
|
+
data += chunk
|
|
145
|
+
body += data[:size]
|
|
146
|
+
data = data[size + 2:]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def request_over_local_socket(method, url, json_body=None, timeout=30.0, path=None):
|
|
150
|
+
"""
|
|
151
|
+
Send one HTTP request through the local Cleep Unix socket.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
method (str): HTTP method
|
|
155
|
+
url (str): request URL, used for the path when path is omitted
|
|
156
|
+
json_body (dict): JSON body for POST
|
|
157
|
+
timeout (float): socket timeout in seconds
|
|
158
|
+
path (str): socket path
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
requests.Response
|
|
162
|
+
"""
|
|
163
|
+
import socket
|
|
164
|
+
from requests import Response
|
|
165
|
+
|
|
166
|
+
socket_path = path or LOCAL_SOCKET_PATH
|
|
167
|
+
http_path = _http_path(url)
|
|
168
|
+
body = b''
|
|
169
|
+
headers = [
|
|
170
|
+
'%s %s HTTP/1.1' % (method, http_path),
|
|
171
|
+
'Host: localhost',
|
|
172
|
+
'Accept: application/json',
|
|
173
|
+
'Connection: close',
|
|
174
|
+
]
|
|
175
|
+
if json_body is not None:
|
|
176
|
+
body = json.dumps(json_body).encode('utf-8')
|
|
177
|
+
headers.append('Content-Type: application/json')
|
|
178
|
+
headers.append('Content-Length: %s' % len(body))
|
|
179
|
+
raw = ('\r\n'.join(headers) + '\r\n\r\n').encode('ascii') + body
|
|
180
|
+
|
|
181
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
182
|
+
sock.settimeout(timeout)
|
|
183
|
+
try:
|
|
184
|
+
sock.connect(socket_path)
|
|
185
|
+
sock.sendall(raw)
|
|
186
|
+
status_code, payload = _read_http_response(sock)
|
|
187
|
+
finally:
|
|
188
|
+
sock.close()
|
|
189
|
+
|
|
190
|
+
response = Response()
|
|
191
|
+
response.status_code = status_code
|
|
192
|
+
response._content = payload
|
|
193
|
+
response.url = 'http://localhost' + http_path
|
|
194
|
+
response.encoding = 'utf-8'
|
|
195
|
+
return response
|
|
196
|
+
|
|
197
|
+
|
|
57
198
|
class CleepApi():
|
|
58
199
|
"""
|
|
59
200
|
Cleep api helper
|
|
@@ -81,9 +222,10 @@ class CleepApi():
|
|
|
81
222
|
|
|
82
223
|
cmd = '/bin/systemctl restart cleep'
|
|
83
224
|
c = Console()
|
|
84
|
-
|
|
225
|
+
# Maximum wait. systemctl returns as soon as the service has restarted.
|
|
226
|
+
resp = c.command(cmd, timeout=60)
|
|
85
227
|
self.logger.debug('Systemctl resp: %s' % resp)
|
|
86
|
-
if resp['
|
|
228
|
+
if resp['killed'] or resp['returncode'] != 0:
|
|
87
229
|
self.logger.error('Error restarting cleep backend')
|
|
88
230
|
return False
|
|
89
231
|
|
|
@@ -95,7 +237,11 @@ class CleepApi():
|
|
|
95
237
|
"""
|
|
96
238
|
self.logger.info('Restarting frontend')
|
|
97
239
|
data = {'to':'developer', 'command':'restart_frontend'}
|
|
98
|
-
self.__post(self.command_url, data)
|
|
240
|
+
result = self.__post(self.command_url, data)
|
|
241
|
+
if not result or result[0] != 200:
|
|
242
|
+
self.logger.error('Error restarting cleep frontend')
|
|
243
|
+
return False
|
|
244
|
+
return True
|
|
99
245
|
|
|
100
246
|
def get_documentation(self, module_name):
|
|
101
247
|
"""
|
|
@@ -132,7 +278,7 @@ class CleepApi():
|
|
|
132
278
|
(status_code, resp) = self.__get(url)
|
|
133
279
|
|
|
134
280
|
if status_code != 200:
|
|
135
|
-
raise Exception("Unable to call cleep %s endpoint"
|
|
281
|
+
raise Exception("Unable to call cleep %s endpoint" % url)
|
|
136
282
|
return resp
|
|
137
283
|
|
|
138
284
|
def __post(self, url, data):
|
|
@@ -152,7 +298,10 @@ class CleepApi():
|
|
|
152
298
|
"""
|
|
153
299
|
try:
|
|
154
300
|
self.logger.debug("POST url: %s", url)
|
|
155
|
-
|
|
301
|
+
if local_socket_available():
|
|
302
|
+
resp = request_over_local_socket('POST', url, json_body=data)
|
|
303
|
+
else:
|
|
304
|
+
resp = requests.post(url, json=data, verify=False)
|
|
156
305
|
resp_data = resp.json()
|
|
157
306
|
self.logger.debug('Response[%s]: %s', resp.status_code, resp_data)
|
|
158
307
|
return (resp.status_code, resp_data)
|
|
@@ -178,7 +327,10 @@ class CleepApi():
|
|
|
178
327
|
"""
|
|
179
328
|
try:
|
|
180
329
|
self.logger.debug("GET url: %s", url)
|
|
181
|
-
|
|
330
|
+
if local_socket_available():
|
|
331
|
+
resp = request_over_local_socket('GET', url)
|
|
332
|
+
else:
|
|
333
|
+
resp = requests.get(url, verify=False)
|
|
182
334
|
resp_data = resp.json()
|
|
183
335
|
self.logger.debug('Response[%s]: %s', resp.status_code, resp_data)
|
|
184
336
|
return (resp.status_code, resp_data)
|
|
@@ -14,6 +14,9 @@ import signal
|
|
|
14
14
|
import logging
|
|
15
15
|
import re
|
|
16
16
|
|
|
17
|
+
ON_POSIX = os.name == "posix"
|
|
18
|
+
|
|
19
|
+
|
|
17
20
|
class EndlessConsole(Thread):
|
|
18
21
|
"""
|
|
19
22
|
Helper class to execute long command line (system update...)
|
|
@@ -322,65 +325,52 @@ class Console():
|
|
|
322
325
|
stdout=subprocess.PIPE,
|
|
323
326
|
stderr=subprocess.PIPE,
|
|
324
327
|
close_fds=True,
|
|
325
|
-
preexec_fn=os.setsid
|
|
328
|
+
preexec_fn=os.setsid if ON_POSIX else None,
|
|
326
329
|
)
|
|
327
330
|
pid = proc.pid
|
|
328
|
-
|
|
329
|
-
# wait for end of command line
|
|
330
|
-
done = False
|
|
331
|
-
start = time.time()
|
|
332
331
|
killed = False
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
# read stds to avoid deadlocks
|
|
336
|
-
(stdout, stderr) = proc.communicate()
|
|
337
|
-
if stdout:
|
|
338
|
-
result['stdout'] += self.__process_lines(stdout.split(b'\n'))
|
|
339
|
-
if stderr:
|
|
340
|
-
result['stderr'] += self.__process_lines(stderr.split(b'\n'))
|
|
341
|
-
|
|
342
|
-
# check if command has finished
|
|
343
|
-
proc.poll()
|
|
344
|
-
if proc.returncode is not None:
|
|
345
|
-
# command terminated
|
|
346
|
-
self.logger.trace('Command terminated with returncode %s' % proc.returncode)
|
|
347
|
-
return_code = proc.returncode
|
|
348
|
-
self.last_return_code = return_code
|
|
349
|
-
done = True
|
|
350
|
-
break
|
|
351
|
-
|
|
352
|
-
# check timeout
|
|
353
|
-
if time.time() > (start + timeout):
|
|
354
|
-
# timeout is over, kill command
|
|
355
|
-
pgid = os.getpgid(pid)
|
|
356
|
-
self.logger.debug('Timeout over, kill command for PID=%s PGID=%s' % (pid, pgid))
|
|
357
|
-
try:
|
|
358
|
-
if ON_POSIX:
|
|
359
|
-
os.killpg(pgid, signal.SIGKILL)
|
|
360
|
-
else: # pragma: no cover
|
|
361
|
-
proc.kill()
|
|
362
|
-
except Exception: # pragma: no cover
|
|
363
|
-
pass
|
|
364
|
-
killed = True
|
|
365
|
-
break
|
|
332
|
+
stdout = b""
|
|
333
|
+
stderr = b""
|
|
366
334
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
335
|
+
try:
|
|
336
|
+
stdout, stderr = proc.communicate(timeout=timeout)
|
|
337
|
+
except subprocess.TimeoutExpired:
|
|
338
|
+
self.logger.debug("Timeout over, kill command for PID=%s" % pid)
|
|
339
|
+
try:
|
|
340
|
+
if ON_POSIX:
|
|
341
|
+
os.killpg(os.getpgid(pid), signal.SIGKILL)
|
|
342
|
+
else: # pragma: no cover
|
|
343
|
+
proc.kill()
|
|
344
|
+
except Exception: # pragma: no cover
|
|
345
|
+
pass
|
|
346
|
+
killed = True
|
|
347
|
+
try:
|
|
348
|
+
stdout, stderr = proc.communicate(timeout=1.0)
|
|
349
|
+
except Exception: # pragma: no cover
|
|
350
|
+
stdout, stderr = b"", b""
|
|
351
|
+
|
|
352
|
+
if stdout:
|
|
353
|
+
result["stdout"] = self.__process_lines(stdout.split(b"\n"))
|
|
354
|
+
if stderr:
|
|
355
|
+
result["stderr"] = self.__process_lines(stderr.split(b"\n"))
|
|
356
|
+
|
|
357
|
+
return_code = proc.returncode
|
|
358
|
+
self.last_return_code = return_code
|
|
359
|
+
result["returncode"] = return_code
|
|
360
|
+
result["killed"] = killed
|
|
361
|
+
# Prefer return code over stderr: tools like systemctl often write to stderr on success
|
|
362
|
+
result["error"] = killed or return_code not in (0, None)
|
|
363
|
+
self.logger.trace("Command terminated with returncode %s" % return_code)
|
|
364
|
+
self.logger.trace("Result: %s" % result)
|
|
375
365
|
|
|
376
366
|
# make sure all stds are closed
|
|
377
367
|
try:
|
|
378
368
|
proc.stdout.close()
|
|
379
|
-
except Exception:
|
|
369
|
+
except Exception: # pragma: no cover
|
|
380
370
|
pass
|
|
381
371
|
try:
|
|
382
372
|
proc.stderr.close()
|
|
383
|
-
except Exception:
|
|
373
|
+
except Exception: # pragma: no cover
|
|
384
374
|
pass
|
|
385
375
|
|
|
386
376
|
# trigger callback (used for delayed command)
|
|
@@ -31,37 +31,48 @@ class File():
|
|
|
31
31
|
'MEDIA_DST': config.MEDIA_DST,
|
|
32
32
|
'CONFIG_DIR': config.CONFIG_DIR,
|
|
33
33
|
}
|
|
34
|
+
variables['MODULES_DST'] = config.MODULES_DST
|
|
34
35
|
cmd = """
|
|
36
|
+
set -e
|
|
35
37
|
mkdir -p "%(HTML_DST)s/"
|
|
36
|
-
mkdir -p "%(CORE_DST)s
|
|
38
|
+
mkdir -p "%(CORE_DST)s"
|
|
39
|
+
mkdir -p "%(MODULES_DST)s"
|
|
37
40
|
mkdir -p "%(MEDIA_DST)s/modules"
|
|
38
41
|
mkdir -p "%(CONFIG_DIR)s"
|
|
42
|
+
# Python imports cleep.modules from CORE_DST/modules; it must point at the live apps tree
|
|
43
|
+
if [ -e "%(CORE_DST)s/modules" ] && [ ! -L "%(CORE_DST)s/modules" ]; then
|
|
44
|
+
rm -rf "%(CORE_DST)s/modules"
|
|
45
|
+
fi
|
|
46
|
+
ln -sfn "%(MODULES_DST)s" "%(CORE_DST)s/modules"
|
|
39
47
|
rsync -av "%(REPO_DIR)s/cleep/" "%(CORE_DST)s/" --exclude "/tests/" --exclude "modules" --exclude "*__pycache__*" --delete --exclude "*.pyc" --keep-dirlinks
|
|
40
48
|
rsync -av "%(REPO_DIR)s/html/" "%(HTML_DST)s/" --delete --exclude "js/modules/" --exclude "*node_modules*"
|
|
41
49
|
rsync -av "%(REPO_DIR)s/bin/cleep" "%(BIN_DST)s/cleep"
|
|
42
50
|
rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
|
|
43
51
|
""" % variables
|
|
44
52
|
self.logger.debug('Coresync cmd: %s' % cmd)
|
|
45
|
-
resp = c.command(cmd,
|
|
53
|
+
resp = c.command(cmd, 120)
|
|
46
54
|
self.logger.debug('Coresync stdout:\n%s' % '\n'.join(resp['stdout']))
|
|
47
55
|
if resp['stderr']:
|
|
48
56
|
self.logger.debug('Coresync stderr:\n%s' % '\n'.join(resp['stderr']))
|
|
49
|
-
if resp['
|
|
57
|
+
if resp['killed'] or resp['returncode'] != 0:
|
|
50
58
|
self.logger.error('Error occured while sync core content: %s' % ('killed' if resp['killed'] else resp['stderr']))
|
|
51
59
|
return False
|
|
52
60
|
|
|
53
61
|
return True
|
|
54
62
|
|
|
55
|
-
def module_sync(self, module_name):
|
|
56
|
-
"""
|
|
63
|
+
def module_sync(self, module_name, backend=True, frontend=True, scripts=True):
|
|
64
|
+
"""
|
|
57
65
|
Copy specified module content to valid execution location
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
module_name (str): module folder name under MODULES_SRC
|
|
69
|
+
backend (bool): sync backend/ when True
|
|
70
|
+
frontend (bool): sync frontend/ when True
|
|
71
|
+
scripts (bool): sync scripts/ when True
|
|
58
72
|
"""
|
|
59
73
|
if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
|
|
60
74
|
self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
|
|
61
75
|
return False
|
|
62
|
-
|
|
63
|
-
if not os.path.exists(os.path.join(config.MODULES_DST, module_name)):
|
|
64
|
-
os.makedirs(os.path.join(config.MODULES_DST, module_name))
|
|
65
76
|
|
|
66
77
|
mod_backend_src = os.path.join(config.MODULES_SRC, module_name, 'backend/')
|
|
67
78
|
mod_backend_dst = os.path.join(config.MODULES_DST, module_name)
|
|
@@ -70,61 +81,79 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
|
|
|
70
81
|
mod_scripts_src = os.path.join(config.MODULES_SRC, module_name, 'scripts/')
|
|
71
82
|
mod_scripts_dst = os.path.join(config.MODULES_SCRIPTS_DST, module_name)
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
parts = []
|
|
85
|
+
if backend:
|
|
86
|
+
parts.append("""
|
|
75
87
|
if [ -d "%(BACKEND_SRC)s" ]; then
|
|
76
88
|
/bin/mkdir -p "%(BACKEND_DST)s"
|
|
77
89
|
rsync -av "%(BACKEND_SRC)s" "%(BACKEND_DST)s" --delete --exclude "*.pyc" --exclude "*__pycache__*"
|
|
78
90
|
fi
|
|
91
|
+
""" % {'BACKEND_SRC': mod_backend_src, 'BACKEND_DST': mod_backend_dst})
|
|
92
|
+
if frontend:
|
|
93
|
+
parts.append("""
|
|
79
94
|
if [ -d "%(FRONTEND_SRC)s" ]; then
|
|
80
95
|
/bin/mkdir -p "%(FRONTEND_DST)s"
|
|
81
96
|
rsync -av "%(FRONTEND_SRC)s" "%(FRONTEND_DST)s" --delete --exclude "*node_modules*"
|
|
82
97
|
fi
|
|
98
|
+
""" % {'FRONTEND_SRC': mod_frontend_src, 'FRONTEND_DST': mod_frontend_dst})
|
|
99
|
+
if scripts:
|
|
100
|
+
parts.append("""
|
|
83
101
|
if [ -d "%(SCRIPTS_SRC)s" ]; then
|
|
84
102
|
/bin/mkdir -p "%(SCRIPTS_DST)s"
|
|
85
103
|
rsync -av "%(SCRIPTS_SRC)s" "%(SCRIPTS_DST)s" --delete --exclude "*__pycache__*"
|
|
86
104
|
fi
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
""" % {'SCRIPTS_SRC': mod_scripts_src, 'SCRIPTS_DST': mod_scripts_dst})
|
|
106
|
+
|
|
107
|
+
if not parts:
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
c = Console()
|
|
111
|
+
cmd = "set -e\n" + "\n".join(parts)
|
|
92
112
|
self.logger.debug('Modsync cmd: %s' % cmd)
|
|
93
|
-
resp = c.command(cmd,
|
|
113
|
+
resp = c.command(cmd, 120)
|
|
94
114
|
|
|
95
115
|
self.logger.debug('Modsync stdout:\n%s' % '\n'.join(resp['stdout']))
|
|
96
116
|
if resp['stderr']:
|
|
97
117
|
self.logger.debug('Modsync stderr:\n%s' % '\n'.join(resp['stderr']))
|
|
98
|
-
if resp['
|
|
99
|
-
self.logger.error(
|
|
118
|
+
if resp['killed'] or resp['returncode'] != 0:
|
|
119
|
+
self.logger.error('Error occured while sync module content: %s' % ('killed' if resp['killed'] else resp['stderr']))
|
|
100
120
|
return False
|
|
101
121
|
|
|
102
122
|
return True
|
|
103
123
|
|
|
104
|
-
def
|
|
124
|
+
def module_run_install_script(self, module_name, script_name):
|
|
105
125
|
"""
|
|
106
|
-
Run module installation scripts
|
|
126
|
+
Run one module installation script from the synced scripts directory
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
module_name (str): module name
|
|
130
|
+
script_name (str): script filename (preinst.sh, postinst.sh, ...)
|
|
107
131
|
"""
|
|
108
132
|
if not os.path.exists(os.path.join(config.MODULES_SRC, module_name)):
|
|
109
133
|
self.logger.error('Module "%s" doesn\'t exist [%s]' % (module_name, os.path.join(config.MODULES_SRC, module_name)))
|
|
110
134
|
return False
|
|
111
135
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if resp["returncode"] != 0:
|
|
127
|
-
self.logger.error('Error running %s [%s]: %s', postinst_script, resp["returncode"], ''.join(resp["stderr"]))
|
|
128
|
-
return False
|
|
129
|
-
|
|
136
|
+
script_path = os.path.join(config.MODULES_SCRIPTS_DST, module_name, script_name)
|
|
137
|
+
if not os.path.exists(script_path):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
self.logger.info('Running %s', script_path)
|
|
141
|
+
resp = Console().command(script_path, timeout=300)
|
|
142
|
+
if resp["killed"] or resp["returncode"] != 0:
|
|
143
|
+
self.logger.error(
|
|
144
|
+
'Error running %s [%s]: %s',
|
|
145
|
+
script_path,
|
|
146
|
+
resp["returncode"],
|
|
147
|
+
''.join(resp["stderr"]),
|
|
148
|
+
)
|
|
149
|
+
return False
|
|
130
150
|
return True
|
|
151
|
+
|
|
152
|
+
def module_run_install_scripts(self, module_name):
|
|
153
|
+
"""
|
|
154
|
+
Run module installation scripts (preinst then postinst)
|
|
155
|
+
"""
|
|
156
|
+
return (
|
|
157
|
+
self.module_run_install_script(module_name, "preinst.sh")
|
|
158
|
+
and self.module_run_install_script(module_name, "postinst.sh")
|
|
159
|
+
)
|