1
|
|
|
from argparse import ArgumentParser |
2
|
|
|
import concurrent.futures |
3
|
|
|
import configparser |
4
|
|
|
import logging |
5
|
|
|
from logging import config |
6
|
|
|
import multiprocessing |
7
|
|
|
import os |
8
|
|
|
import shutil |
9
|
|
|
import signal |
10
|
|
|
import tabpy.tabpy_server |
11
|
|
|
from tabpy.tabpy import __version__ |
12
|
|
|
from tabpy.tabpy_server.app.ConfigParameters import ConfigParameters |
13
|
|
|
from tabpy.tabpy_server.app.SettingsParameters import SettingsParameters |
14
|
|
|
from tabpy.tabpy_server.app.util import parse_pwd_file |
15
|
|
|
from tabpy.tabpy_server.management.state import TabPyState |
16
|
|
|
from tabpy.tabpy_server.management.util import _get_state_from_file |
17
|
|
|
from tabpy.tabpy_server.psws.callbacks\ |
18
|
|
|
import (init_model_evaluator, init_ps_server) |
19
|
|
|
from tabpy.tabpy_server.psws.python_service\ |
20
|
|
|
import (PythonService, PythonServiceHandler) |
21
|
|
|
from tabpy.tabpy_server.handlers\ |
22
|
|
|
import (EndpointHandler, EndpointsHandler, |
23
|
|
|
EvaluationPlaneHandler, QueryPlaneHandler, |
24
|
|
|
ServiceInfoHandler, StatusHandler, |
25
|
|
|
UploadDestinationHandler) |
26
|
|
|
import tornado |
27
|
|
|
|
28
|
|
|
|
29
|
|
|
logger = logging.getLogger(__name__) |
30
|
|
|
|
31
|
|
|
|
32
|
|
|
class TabPyApp: |
33
|
|
|
''' |
34
|
|
|
TabPy application class for keeping context like settings, state, etc. |
35
|
|
|
''' |
36
|
|
|
|
37
|
|
|
settings = {} |
38
|
|
|
subdirectory = "" |
39
|
|
|
tabpy_state = None |
40
|
|
|
python_service = None |
41
|
|
|
credentials = {} |
42
|
|
|
|
43
|
|
|
def __init__(self, config_file=None): |
44
|
|
|
if config_file is None: |
45
|
|
|
cli_args = self._parse_cli_arguments() |
46
|
|
|
if cli_args.config is not None: |
47
|
|
|
config_file = cli_args.config |
48
|
|
|
else: |
49
|
|
|
config_file = os.path.join(os.path.dirname(__file__), |
50
|
|
|
os.path.pardir, 'common', |
51
|
|
|
'default.conf') |
52
|
|
|
|
53
|
|
|
if os.path.isfile(config_file): |
54
|
|
|
try: |
55
|
|
|
logging.config.fileConfig( |
56
|
|
|
config_file, disable_existing_loggers=False) |
57
|
|
|
except KeyError: |
58
|
|
|
logging.basicConfig(level=logging.DEBUG) |
59
|
|
|
|
60
|
|
|
self._parse_config(config_file) |
61
|
|
|
|
62
|
|
|
def run(self): |
63
|
|
|
application = self._create_tornado_web_app() |
64
|
|
|
max_request_size =\ |
65
|
|
|
int(self.settings[SettingsParameters.MaxRequestSizeInMb]) *\ |
66
|
|
|
1024 * 1024 |
67
|
|
|
logger.info(f'Setting max request size to {max_request_size} bytes') |
68
|
|
|
|
69
|
|
|
init_model_evaluator( |
70
|
|
|
self.settings, |
71
|
|
|
self.tabpy_state, |
72
|
|
|
self.python_service) |
73
|
|
|
|
74
|
|
|
protocol = self.settings[SettingsParameters.TransferProtocol] |
75
|
|
|
ssl_options = None |
76
|
|
|
if protocol == 'https': |
77
|
|
|
ssl_options = { |
78
|
|
|
'certfile': self.settings[SettingsParameters.CertificateFile], |
79
|
|
|
'keyfile': self.settings[SettingsParameters.KeyFile] |
80
|
|
|
} |
81
|
|
|
elif protocol != 'http': |
82
|
|
|
msg = f'Unsupported transfer protocol {protocol}.' |
83
|
|
|
logger.critical(msg) |
84
|
|
|
raise RuntimeError(msg) |
85
|
|
|
|
86
|
|
|
application.listen( |
87
|
|
|
self.settings[SettingsParameters.Port], |
88
|
|
|
ssl_options=ssl_options, |
89
|
|
|
max_buffer_size=max_request_size, |
90
|
|
|
max_body_size=max_request_size) |
91
|
|
|
|
92
|
|
|
logger.info( |
93
|
|
|
'Web service listening on port ' |
94
|
|
|
f'{str(self.settings[SettingsParameters.Port])}') |
95
|
|
|
tornado.ioloop.IOLoop.instance().start() |
96
|
|
|
|
97
|
|
|
def _create_tornado_web_app(self): |
98
|
|
|
class TabPyTornadoApp(tornado.web.Application): |
99
|
|
|
is_closing = False |
100
|
|
|
|
101
|
|
|
def signal_handler(self, signal): |
102
|
|
|
logger.critical(f'Exiting on signal {signal}...') |
103
|
|
|
self.is_closing = True |
104
|
|
|
|
105
|
|
|
def try_exit(self): |
106
|
|
|
if self.is_closing: |
107
|
|
|
tornado.ioloop.IOLoop.instance().stop() |
108
|
|
|
logger.info('Shutting down TabPy...') |
109
|
|
|
|
110
|
|
|
logger.info('Initializing TabPy...') |
111
|
|
|
tornado.ioloop.IOLoop.instance().run_sync( |
112
|
|
|
lambda: init_ps_server(self.settings, self.tabpy_state)) |
113
|
|
|
logger.info('Done initializing TabPy.') |
114
|
|
|
|
115
|
|
|
executor = concurrent.futures.ThreadPoolExecutor( |
116
|
|
|
max_workers=multiprocessing.cpu_count()) |
117
|
|
|
|
118
|
|
|
# initialize Tornado application |
119
|
|
|
application = TabPyTornadoApp([ |
120
|
|
|
# skip MainHandler to use StaticFileHandler .* page requests and |
121
|
|
|
# default to index.html |
122
|
|
|
# (r"/", MainHandler), |
123
|
|
|
(self.subdirectory + r'/query/([^/]+)', QueryPlaneHandler, |
124
|
|
|
dict(app=self)), |
125
|
|
|
(self.subdirectory + r'/status', StatusHandler, |
126
|
|
|
dict(app=self)), |
127
|
|
|
(self.subdirectory + r'/info', ServiceInfoHandler, |
128
|
|
|
dict(app=self)), |
129
|
|
|
(self.subdirectory + r'/endpoints', EndpointsHandler, |
130
|
|
|
dict(app=self)), |
131
|
|
|
(self.subdirectory + r'/endpoints/([^/]+)?', EndpointHandler, |
132
|
|
|
dict(app=self)), |
133
|
|
|
(self.subdirectory + r'/evaluate', EvaluationPlaneHandler, |
134
|
|
|
dict(executor=executor, |
135
|
|
|
app=self)), |
136
|
|
|
(self.subdirectory + |
137
|
|
|
r'/configurations/endpoint_upload_destination', |
138
|
|
|
UploadDestinationHandler, |
139
|
|
|
dict(app=self)), |
140
|
|
|
(self.subdirectory + r'/(.*)', tornado.web.StaticFileHandler, |
141
|
|
|
dict(path=self.settings[SettingsParameters.StaticPath], |
142
|
|
|
default_filename="index.html")), |
143
|
|
|
], debug=False, **self.settings) |
144
|
|
|
|
145
|
|
|
signal.signal(signal.SIGINT, application.signal_handler) |
146
|
|
|
tornado.ioloop.PeriodicCallback(application.try_exit, 500).start() |
147
|
|
|
|
148
|
|
|
return application |
149
|
|
|
|
150
|
|
|
def _parse_cli_arguments(self): |
151
|
|
|
''' |
152
|
|
|
Parse command line arguments. Expected arguments: |
153
|
|
|
* --config: string |
154
|
|
|
''' |
155
|
|
|
parser = ArgumentParser(description='Run TabPy Server.') |
156
|
|
|
parser.add_argument('--config', help='Path to a config file.') |
157
|
|
|
return parser.parse_args() |
158
|
|
|
|
159
|
|
|
def _parse_config(self, config_file): |
160
|
|
|
"""Provide consistent mechanism for pulling in configuration. |
161
|
|
|
|
162
|
|
|
Attempt to retain backward compatibility for |
163
|
|
|
existing implementations by grabbing port |
164
|
|
|
setting from CLI first. |
165
|
|
|
|
166
|
|
|
Take settings in the following order: |
167
|
|
|
|
168
|
|
|
1. CLI arguments if present |
169
|
|
|
2. config file |
170
|
|
|
3. OS environment variables (for ease of |
171
|
|
|
setting defaults if not present) |
172
|
|
|
4. current defaults if a setting is not present in any location |
173
|
|
|
|
174
|
|
|
Additionally provide similar configuration capabilities in between |
175
|
|
|
config file and environment variables. |
176
|
|
|
For consistency use the same variable name in the config file as |
177
|
|
|
in the os environment. |
178
|
|
|
For naming standards use all capitals and start with 'TABPY_' |
179
|
|
|
""" |
180
|
|
|
self.settings = {} |
181
|
|
|
self.subdirectory = "" |
182
|
|
|
self.tabpy_state = None |
183
|
|
|
self.python_service = None |
184
|
|
|
self.credentials = {} |
185
|
|
|
|
186
|
|
|
parser = configparser.ConfigParser(os.environ) |
187
|
|
|
|
188
|
|
|
if os.path.isfile(config_file): |
189
|
|
|
with open(config_file) as f: |
190
|
|
|
parser.read_string(f.read()) |
191
|
|
|
else: |
192
|
|
|
logger.warning( |
193
|
|
|
f'Unable to find config file at {config_file}, ' |
194
|
|
|
'using default settings.') |
195
|
|
|
|
196
|
|
|
def set_parameter(settings_key, |
197
|
|
|
config_key, |
198
|
|
|
default_val=None): |
199
|
|
|
key_is_set = False |
200
|
|
|
|
201
|
|
|
if config_key is not None and\ |
202
|
|
|
parser.has_section('TabPy') and\ |
203
|
|
|
parser.has_option('TabPy', config_key): |
204
|
|
|
self.settings[settings_key] = parser.get('TabPy', config_key) |
205
|
|
|
key_is_set = True |
206
|
|
|
logger.debug( |
207
|
|
|
f'Parameter {settings_key} set to ' |
208
|
|
|
f'"{self.settings[settings_key]}" ' |
209
|
|
|
'from config file or environment variable') |
210
|
|
|
|
211
|
|
|
if not key_is_set and default_val is not None: |
212
|
|
|
self.settings[settings_key] = default_val |
213
|
|
|
key_is_set = True |
214
|
|
|
logger.debug( |
215
|
|
|
f'Parameter {settings_key} set to ' |
216
|
|
|
f'"{self.settings[settings_key]}" ' |
217
|
|
|
'from default value') |
218
|
|
|
|
219
|
|
|
if not key_is_set: |
220
|
|
|
logger.debug( |
221
|
|
|
f'Parameter {settings_key} is not set') |
222
|
|
|
|
223
|
|
|
set_parameter(SettingsParameters.Port, ConfigParameters.TABPY_PORT, |
224
|
|
|
default_val=9004) |
225
|
|
|
set_parameter(SettingsParameters.ServerVersion, None, |
226
|
|
|
default_val=__version__) |
227
|
|
|
|
228
|
|
|
set_parameter(SettingsParameters.EvaluateTimeout, |
229
|
|
|
ConfigParameters.TABPY_EVALUATE_TIMEOUT, |
230
|
|
|
default_val=30) |
231
|
|
|
|
232
|
|
|
try: |
233
|
|
|
self.settings[SettingsParameters.EvaluateTimeout] = float( |
234
|
|
|
self.settings[SettingsParameters.EvaluateTimeout]) |
235
|
|
|
except ValueError: |
236
|
|
|
logger.warning( |
237
|
|
|
'Evaluate timeout must be a float type. Defaulting ' |
238
|
|
|
'to evaluate timeout of 30 seconds.') |
239
|
|
|
self.settings[SettingsParameters.EvaluateTimeout] = 30 |
240
|
|
|
|
241
|
|
|
pkg_path = os.path.dirname(tabpy.__file__) |
242
|
|
|
set_parameter(SettingsParameters.UploadDir, |
243
|
|
|
ConfigParameters.TABPY_QUERY_OBJECT_PATH, |
244
|
|
|
default_val=os.path.join(pkg_path, |
245
|
|
|
'tmp', 'query_objects')) |
246
|
|
|
if not os.path.exists(self.settings[SettingsParameters.UploadDir]): |
247
|
|
|
os.makedirs(self.settings[SettingsParameters.UploadDir]) |
248
|
|
|
|
249
|
|
|
# set and validate transfer protocol |
250
|
|
|
set_parameter(SettingsParameters.TransferProtocol, |
251
|
|
|
ConfigParameters.TABPY_TRANSFER_PROTOCOL, |
252
|
|
|
default_val='http') |
253
|
|
|
self.settings[SettingsParameters.TransferProtocol] =\ |
254
|
|
|
self.settings[SettingsParameters.TransferProtocol].lower() |
255
|
|
|
|
256
|
|
|
set_parameter(SettingsParameters.CertificateFile, |
257
|
|
|
ConfigParameters.TABPY_CERTIFICATE_FILE) |
258
|
|
|
set_parameter(SettingsParameters.KeyFile, |
259
|
|
|
ConfigParameters.TABPY_KEY_FILE) |
260
|
|
|
self._validate_transfer_protocol_settings() |
261
|
|
|
|
262
|
|
|
# if state.ini does not exist try and create it - remove |
263
|
|
|
# last dependence on batch/shell script |
264
|
|
|
set_parameter(SettingsParameters.StateFilePath, |
265
|
|
|
ConfigParameters.TABPY_STATE_PATH, |
266
|
|
|
default_val=os.path.join(pkg_path, 'tabpy_server')) |
267
|
|
|
self.settings[SettingsParameters.StateFilePath] = os.path.realpath( |
268
|
|
|
os.path.normpath( |
269
|
|
|
os.path.expanduser( |
270
|
|
|
self.settings[SettingsParameters.StateFilePath]))) |
271
|
|
|
state_file_dir = self.settings[SettingsParameters.StateFilePath] |
272
|
|
|
state_file_path = os.path.join(state_file_dir, 'state.ini') |
273
|
|
|
if not os.path.isfile(state_file_path): |
274
|
|
|
state_file_template_path = os.path.join( |
275
|
|
|
pkg_path, 'tabpy_server', 'state.ini.template') |
276
|
|
|
logger.debug(f'File {state_file_path} not found, creating from ' |
277
|
|
|
f'template {state_file_template_path}...') |
278
|
|
|
shutil.copy(state_file_template_path, state_file_path) |
279
|
|
|
|
280
|
|
|
logger.info(f'Loading state from state file {state_file_path}') |
281
|
|
|
tabpy_state = _get_state_from_file(state_file_dir) |
282
|
|
|
self.tabpy_state = TabPyState( |
283
|
|
|
config=tabpy_state, settings=self.settings) |
284
|
|
|
|
285
|
|
|
self.python_service = PythonServiceHandler(PythonService()) |
286
|
|
|
self.settings['compress_response'] = True |
287
|
|
|
set_parameter(SettingsParameters.StaticPath, |
288
|
|
|
ConfigParameters.TABPY_STATIC_PATH, |
289
|
|
|
default_val='./') |
290
|
|
|
self.settings[SettingsParameters.StaticPath] =\ |
291
|
|
|
os.path.abspath(self.settings[SettingsParameters.StaticPath]) |
292
|
|
|
logger.debug(f'Static pages folder set to ' |
293
|
|
|
f'"{self.settings[SettingsParameters.StaticPath]}"') |
294
|
|
|
|
295
|
|
|
# Set subdirectory from config if applicable |
296
|
|
|
if tabpy_state.has_option("Service Info", "Subdirectory"): |
297
|
|
|
self.subdirectory = "/" + \ |
298
|
|
|
tabpy_state.get("Service Info", "Subdirectory") |
299
|
|
|
|
300
|
|
|
# If passwords file specified load credentials |
301
|
|
|
set_parameter(ConfigParameters.TABPY_PWD_FILE, |
302
|
|
|
ConfigParameters.TABPY_PWD_FILE) |
303
|
|
|
if ConfigParameters.TABPY_PWD_FILE in self.settings: |
304
|
|
|
if not self._parse_pwd_file(): |
305
|
|
|
msg = ('Failed to read passwords file ' |
306
|
|
|
f'{self.settings[ConfigParameters.TABPY_PWD_FILE]}') |
307
|
|
|
logger.critical(msg) |
308
|
|
|
raise RuntimeError(msg) |
309
|
|
|
else: |
310
|
|
|
logger.info( |
311
|
|
|
"Password file is not specified: " |
312
|
|
|
"Authentication is not enabled") |
313
|
|
|
|
314
|
|
|
features = self._get_features() |
315
|
|
|
self.settings[SettingsParameters.ApiVersions] =\ |
316
|
|
|
{'v1': {'features': features}} |
317
|
|
|
|
318
|
|
|
set_parameter(SettingsParameters.LogRequestContext, |
319
|
|
|
ConfigParameters.TABPY_LOG_DETAILS, |
320
|
|
|
default_val='false') |
321
|
|
|
self.settings[SettingsParameters.LogRequestContext] = ( |
322
|
|
|
self.settings[SettingsParameters.LogRequestContext].lower() != |
323
|
|
|
'false') |
324
|
|
|
call_context_state =\ |
325
|
|
|
'enabled' if self.settings[SettingsParameters.LogRequestContext]\ |
326
|
|
|
else 'disabled' |
327
|
|
|
logger.info(f'Call context logging is {call_context_state}') |
328
|
|
|
|
329
|
|
|
set_parameter(SettingsParameters.MaxRequestSizeInMb, |
330
|
|
|
ConfigParameters.TABPY_MAX_REQUEST_SIZE_MB, |
331
|
|
|
default_val=100) |
332
|
|
|
|
333
|
|
|
def _validate_transfer_protocol_settings(self): |
334
|
|
|
if SettingsParameters.TransferProtocol not in self.settings: |
335
|
|
|
msg = 'Missing transfer protocol information.' |
336
|
|
|
logger.critical(msg) |
337
|
|
|
raise RuntimeError(msg) |
338
|
|
|
|
339
|
|
|
protocol = self.settings[SettingsParameters.TransferProtocol] |
340
|
|
|
|
341
|
|
|
if protocol == 'http': |
342
|
|
|
return |
343
|
|
|
|
344
|
|
|
if protocol != 'https': |
345
|
|
|
msg = f'Unsupported transfer protocol: {protocol}' |
346
|
|
|
logger.critical(msg) |
347
|
|
|
raise RuntimeError(msg) |
348
|
|
|
|
349
|
|
|
self._validate_cert_key_state( |
350
|
|
|
'The parameter(s) {} must be set.', |
351
|
|
|
SettingsParameters.CertificateFile in self.settings, |
352
|
|
|
SettingsParameters.KeyFile in self.settings) |
353
|
|
|
cert = self.settings[SettingsParameters.CertificateFile] |
354
|
|
|
|
355
|
|
|
self._validate_cert_key_state( |
356
|
|
|
'The parameter(s) {} must point to ' |
357
|
|
|
'an existing file.', |
358
|
|
|
os.path.isfile(cert), |
359
|
|
|
os.path.isfile(self.settings[SettingsParameters.KeyFile])) |
360
|
|
|
tabpy.tabpy_server.app.util.validate_cert(cert) |
361
|
|
|
|
362
|
|
|
@staticmethod |
363
|
|
|
def _validate_cert_key_state(msg, cert_valid, key_valid): |
364
|
|
|
cert_and_key_param = ( |
365
|
|
|
f'{ConfigParameters.TABPY_CERTIFICATE_FILE} and ' |
366
|
|
|
f'{ConfigParameters.TABPY_KEY_FILE}') |
367
|
|
|
https_error = 'Error using HTTPS: ' |
368
|
|
|
err = None |
369
|
|
|
if not cert_valid and not key_valid: |
370
|
|
|
err = https_error + msg.format(cert_and_key_param) |
371
|
|
|
elif not cert_valid: |
372
|
|
|
err = https_error + \ |
373
|
|
|
msg.format(ConfigParameters.TABPY_CERTIFICATE_FILE) |
374
|
|
|
elif not key_valid: |
375
|
|
|
err = https_error + msg.format(ConfigParameters.TABPY_KEY_FILE) |
376
|
|
|
|
377
|
|
|
if err is not None: |
378
|
|
|
logger.critical(err) |
379
|
|
|
raise RuntimeError(err) |
380
|
|
|
|
381
|
|
|
def _parse_pwd_file(self): |
382
|
|
|
succeeded, self.credentials = parse_pwd_file( |
383
|
|
|
self.settings[ConfigParameters.TABPY_PWD_FILE]) |
384
|
|
|
|
385
|
|
|
if succeeded and len(self.credentials) == 0: |
386
|
|
|
logger.error('No credentials found') |
387
|
|
|
succeeded = False |
388
|
|
|
|
389
|
|
|
return succeeded |
390
|
|
|
|
391
|
|
|
def _get_features(self): |
392
|
|
|
features = {} |
393
|
|
|
|
394
|
|
|
# Check for auth |
395
|
|
|
if ConfigParameters.TABPY_PWD_FILE in self.settings: |
396
|
|
|
features['authentication'] = { |
397
|
|
|
'required': True, 'methods': { |
398
|
|
|
'basic-auth': {}}} |
399
|
|
|
|
400
|
|
|
return features |
401
|
|
|
|