Passed
Pull Request — master (#347)
by
unknown
02:51
created

TabPyApp._validate_transfer_protocol_settings()   A

Complexity

Conditions 4

Size

Total Lines 28
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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