Skip to content

Commit ef066dd

Browse files
committed
License info from environment variables given higher preference.
Introducing module to house all environment variables used by app.
1 parent 14686e1 commit ef066dd

File tree

10 files changed

+257
-70
lines changed

10 files changed

+257
-70
lines changed

jupyter_matlab_proxy/app.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,14 +409,17 @@ def main():
409409
app = create_app()
410410

411411
loop = asyncio.get_event_loop()
412-
runner = web.AppRunner(app)
412+
413+
# Override default loggers
414+
runner = web.AppRunner(app, logger=logger, access_log=logger)
415+
413416
loop.run_until_complete(runner.setup())
414417
site = web.TCPSite(
415418
runner, host=app["settings"]["host_interface"], port=app["settings"]["app_port"]
416419
)
417420
loop.run_until_complete(site.start())
418421

419-
# Register handlers to trap termination signals
422+
# Register handlers to trap termination signals
420423
for signal in get_supported_termination_signals():
421424
logger.info(f"Installing handler for signal: {signal} ")
422425
loop.add_signal_handler(signal, lambda: loop.stop())

jupyter_matlab_proxy/app_state.py

Lines changed: 81 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
import socket
1313
import errno
1414
from collections import deque
15-
from .util import mw, mw_logger
15+
from .util import mw, mw_logger, validators
1616
from .util.exceptions import (
1717
LicensingError,
1818
InternalError,
1919
OnlineLicensingError,
2020
EntitlementError,
2121
MatlabInstallError,
22+
NetworkLicensingError,
2223
log_error,
2324
)
2425

26+
2527
logger = mw_logger.get()
2628

2729

@@ -42,21 +44,59 @@ def __init__(self, settings):
4244
logger.error("'matlab' executable not found in PATH")
4345
return
4446

47+
def __get_cached_licensing_file(self):
48+
"""Get the cached licensing file
49+
50+
Returns:
51+
Path : Path object to cached licensing file
52+
"""
53+
return self.settings["matlab_config_file"]
54+
55+
def __delete_cached_licensing_file(self):
56+
try:
57+
logger.info(f"Deleting any cached licensing files!")
58+
os.remove(self.__get_cached_licensing_file())
59+
except FileNotFoundError:
60+
# The file being absent is acceptable.
61+
pass
62+
63+
64+
def __reset_and_delete_cached_licensing(self):
65+
logger.info(f"Resetting cached licensing information...")
66+
self.licensing = None
67+
self.__delete_cached_licensing_file()
68+
4569
async def init_licensing(self):
46-
"""Initialise licensing from persisted details or environment variable."""
70+
"""Initialize licensing from environment variable or cached file.
71+
72+
Greater precedence is given to value specified in environment variable MLM_LICENSE_FILE
73+
If specified, this function will delete previously cached licensing information.
74+
This enforces a clear understanding of what was used to initialize licensing.
75+
The contents of the environment variable are NEVER cached.
76+
"""
4777

48-
# Persisted licensing details present
49-
if self.settings["matlab_config_file"].exists():
50-
with open(self.settings["matlab_config_file"], "r") as f:
51-
config = json.loads(f.read())
78+
# Default value
79+
self.licensing = None
5280

53-
if "licensing" in config:
54-
# TODO Refactoring of config file reading/writing
55-
licensing = config["licensing"]
81+
# NLM Connection String set in environment
82+
if self.settings["nlm_conn_str"] is not None:
83+
nlm_licensing_str = self.settings["nlm_conn_str"]
84+
logger.info(f"Found NLM:[{nlm_licensing_str}] set in environment")
85+
logger.info(f"Using NLM string to connect ... ")
86+
self.licensing = {
87+
"type": "nlm",
88+
"conn_str": nlm_licensing_str,
89+
}
90+
self.__delete_cached_licensing_file()
5691

57-
# If there is any problem loading config, remove it and persist
92+
# If NLM connection string is not present, then look for persistent LNU info
93+
elif self.__get_cached_licensing_file().exists():
94+
with open(self.__get_cached_licensing_file(), "r") as f:
95+
licensing = json.loads(f.read())
96+
logger.info("Found cached licensing information...")
5897
try:
5998
if licensing["type"] == "nlm":
99+
# Note: Only NLM settings entered in browser were cached.
60100
self.licensing = {
61101
"type": "nlm",
62102
"conn_str": licensing["conn_str"],
@@ -82,26 +122,19 @@ async def init_licensing(self):
82122
) - timedelta(hours=1)
83123

84124
if expiry_window > datetime.now(timezone.utc):
85-
await self.update_entitlements()
125+
successful_update = await self.update_entitlements()
126+
if successful_update:
127+
logger.info("Successful re-use of cached information.")
128+
self.persist_licensing()
129+
else:
130+
self.__reset_and_delete_cached_licensing()
86131
else:
87-
# Reset licensing and persist
88-
self.licensing["identity_token"] = None
89-
self.licensing["source_id"] = None
90-
self.licensing["expiry"] = None
91-
self.licensing["entitlements"] = []
92-
self.persist_licensing()
132+
self.__reset_and_delete_cached_licensing()
133+
else:
134+
# Somethings wrong, licensing is neither NLM or MHLM
135+
self.__reset_and_delete_cached_licensing()
93136
except Exception as e:
94-
logger.error("Error parsing config, resetting.")
95-
self.licensing = None
96-
self.persist_licensing()
97-
98-
# NLM Connection String set in environment
99-
# TODO Validate connection string
100-
elif self.settings["nlm_conn_str"] is not None:
101-
self.licensing = {
102-
"type": "nlm",
103-
"conn_str": self.settings["nlm_conn_str"],
104-
}
137+
self.__reset_and_delete_cached_licensing()
105138

106139
def get_matlab_state(self):
107140
"""Determine the state of MATLAB to be down/starting/up."""
@@ -208,6 +241,12 @@ def is_matlab_present(self):
208241
return self.settings["matlab_path"] is not None
209242

210243
async def update_entitlements(self):
244+
"""Speaks to MW and updates MHLM entitlements
245+
246+
Returns: True if update was successful
247+
Raises:
248+
InternalError: OnlineLicensingError, EntitlementError
249+
"""
211250
if self.licensing is None or self.licensing["type"] != "mhlm":
212251
raise InternalError(
213252
"MHLM licensing must be configured to update entitlements!"
@@ -230,7 +269,7 @@ async def update_entitlements(self):
230269
except OnlineLicensingError as e:
231270
self.error = e
232271
log_error(logger, e)
233-
return
272+
return False
234273
except EntitlementError as e:
235274
self.error = e
236275
log_error(logger, e)
@@ -244,48 +283,28 @@ async def update_entitlements(self):
244283
self.licensing["profile_id"] = None
245284
self.licensing["entitlements"] = []
246285
self.licensing["entitlement_id"] = None
247-
return
286+
return False
248287

249288
self.licensing["entitlements"] = entitlements
250289

251290
# If there is only one non-expired entitlement, set it as active
252291
# TODO Also, for now, set the first entitlement as active if there are multiple
253292
self.licensing["entitlement_id"] = entitlements[0]["id"]
254293

255-
def persist_licensing(self):
256-
config_file = self.settings["matlab_config_file"]
257-
if config_file.exists():
258-
with open(config_file, "r") as f:
259-
config = json.loads(f.read())
260-
else:
261-
config = {}
294+
# Successful update
295+
return True
262296

297+
def persist_licensing(self):
298+
"""Saves licensing information to file"""
263299
if self.licensing is None:
264-
if "licensing" in config:
265-
del config["licensing"]
266-
elif self.licensing["type"] == "mhlm":
267-
config["licensing"] = {
268-
"type": "mhlm",
269-
"identity_token": self.licensing["identity_token"],
270-
"source_id": self.licensing["source_id"],
271-
"expiry": self.licensing["expiry"],
272-
"email_addr": self.licensing["email_addr"],
273-
"first_name": self.licensing["first_name"],
274-
"last_name": self.licensing["last_name"],
275-
"display_name": self.licensing["display_name"],
276-
"user_id": self.licensing["user_id"],
277-
"profile_id": self.licensing["profile_id"],
278-
"entitlement_id": self.licensing["entitlement_id"],
279-
}
280-
elif self.licensing["type"] == "nlm":
281-
config["licensing"] = {
282-
"type": "nlm",
283-
"conn_str": self.licensing["conn_str"],
284-
}
285-
286-
config_file.parent.mkdir(parents=True, exist_ok=True)
287-
with open(config_file, "w") as f:
288-
f.write(json.dumps(config))
300+
self.__delete_cached_licensing_file()
301+
302+
elif self.licensing["type"] in ["mhlm", "nlm"]:
303+
logger.info("Saving licensing information...")
304+
cached_licensing_file = self.__get_cached_licensing_file()
305+
cached_licensing_file.parent.mkdir(parents=True, exist_ok=True)
306+
with open(cached_licensing_file, "w") as f:
307+
f.write(json.dumps(self.licensing))
289308

290309
def reserve_matlab_port(self):
291310
"""Reserve a free port for MATLAB Embedded Connector in the allowed range."""
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2021 The MathWorks, Inc.
2+
"""This file lists and exposes the environment variables which are used by the integration."""
3+
4+
5+
def get_env_name_network_license_manager():
6+
"""Specifies the path to valid license file or address of a network license server"""
7+
return "MLM_LICENSE_FILE"
8+
9+
10+
def get_env_name_logging_level():
11+
"""Specifies the logging level used by app's loggers"""
12+
return "LOG_LEVEL"
13+
14+
15+
def get_env_name_log_file():
16+
"""Specifies a file into which logging content is directed"""
17+
return "LOG_FILE"
18+
19+
20+
def get_env_name_base_url():
21+
"""Specifies the base url on which the website should run.
22+
Eg: www.127.0.0.1:8888/base_url/index.html
23+
24+
Note: The website runs on a URL of the form:
25+
www.<SERVER ADDRESS>:<PORT NUMBER>/<BASE_URL>/index.html
26+
"""
27+
return "BASE_URL"
28+
29+
30+
def get_env_name_app_port():
31+
"""Specifies the port on which the website is running on the server.
32+
Eg: www.127.0.0.1:PORT/index.html
33+
34+
Note: The website runs on a URL of the form:
35+
www.<SERVER ADDRESS>:<PORT NUMBER>/<BASE_URL>/index.html
36+
"""
37+
return "APP_PORT"
38+
39+
40+
def get_env_name_custom_http_headers():
41+
"""Specifies HTTP headers as JSON content, to be injected into responses sent to the browser"""
42+
return "CUSTOM_HTTP_HEADERS"
43+
44+
45+
def get_env_name_app_host():
46+
"""Specifies the host on which the TCP site (aiohttp server) is being run."""
47+
return "APP_HOST"
48+
49+
50+
def get_env_name_testing():
51+
return "TEST"
52+
53+
54+
def get_env_name_development():
55+
return "DEV"

jupyter_matlab_proxy/settings.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from pathlib import Path
55
import tempfile
66
import xml.etree.ElementTree as ET
7-
import uuid, socket
7+
import uuid
8+
import socket
89
import shutil
9-
from .util import custom_http_headers
10+
from .util import custom_http_headers, validators
1011

1112

1213
def get_matlab_path():
@@ -131,7 +132,9 @@ def get(dev=False):
131132
"host_interface": os.environ.get("APP_HOST"),
132133
"mwapikey": str(uuid.uuid4()),
133134
"matlab_protocol": "https",
134-
"nlm_conn_str": os.environ.get("MLM_LICENSE_FILE"),
135+
"nlm_conn_str": validators.validate_mlm_license_file(
136+
os.environ.get("MLM_LICENSE_FILE")
137+
),
135138
"matlab_config_file": Path.home() / ".matlab" / "proxy_app_config.json",
136139
"ws_env": ws_env,
137140
"mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",

jupyter_matlab_proxy/util/mw_logger.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def __set_logging_configuration():
5252
# log_level is either set by environment or is the default value.
5353
logger.info(f"Initializing logger with log_level: {log_level}")
5454
logger.setLevel(log_level)
55+
56+
# Allow other libraries used by this integration to
57+
# also print their logs at the specified level
5558
logging.basicConfig(level=log_level)
5659

5760
return logger
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright 2020-2021 The MathWorks, Inc.
2+
"""This file contains validators for various runtime artefacts.
3+
A validator is defined as a function which verifies the input and
4+
returns it unchanged if validation passes.
5+
Returning inputs allows validators to be used inline with the input.
6+
7+
Example:
8+
Original code: if( input ):
9+
With validator: if (valid(input)):
10+
11+
Exceptions are thrown to signal failure.
12+
"""
13+
14+
15+
def validate_mlm_license_file(nlm_conn_str):
16+
"""Validates and returns input if it passes validation.
17+
Throws exception when validation fails.
18+
The connection string should be in the form of port@hostname
19+
OR path to valid license file
20+
"""
21+
import re
22+
import os
23+
from . import mw_logger
24+
from .exceptions import NetworkLicensingError
25+
26+
logger = mw_logger.get()
27+
28+
if nlm_conn_str is None:
29+
return None
30+
31+
# TODO: The JS validation of this setting does not allow file path locations
32+
# The JS validation occurs before reaching the set_licensing_info endpoint.
33+
34+
# Regular expression to match port@hostname,
35+
# where port is any number and hostname is alphanumeric
36+
# regex = Start of Line, Any number of 0-9 digits , @, any number of nonwhite space characters with "- _ ." allowed
37+
regex = "^[0-9]+[@](\w|\_|\-|\.)+$"
38+
if not re.search(regex, nlm_conn_str):
39+
logger.debug("NLM info is not in the form of port@hostname")
40+
if not os.path.isfile(nlm_conn_str):
41+
logger.debug("NLM info is not a valid path to a license file")
42+
error_message = (
43+
f"MLM_LICENSE_FILE validation failed for {nlm_conn_str}. "
44+
f"If set, the MLM_LICENSE_FILE environment variable must be a string which is either of the form port@hostname"
45+
f" OR path to a valid license file."
46+
)
47+
logger.error(error_message)
48+
raise NetworkLicensingError(error_message)
49+
else:
50+
logger.info(
51+
f"MLM_LICENSE_FILE with value: {nlm_conn_str} is a path to a file. MATLAB will attempt to use it."
52+
)
53+
else:
54+
logger.info(
55+
f"MLM_LICENSE_FILE with value: {nlm_conn_str} is a license server, MATLAB will attempt to connect to it."
56+
)
57+
58+
# Validation passed
59+
return nlm_conn_str

tests/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def delete_matlab_test_dir():
3737
# Delete matlab_config_file & its owning directory
3838
matlab_config_file = __get_matlab_config_file()
3939
matlab_config_dir = os.path.dirname(matlab_config_file)
40-
shutil.rmtree(matlab_config_dir)
40+
try:
41+
shutil.rmtree(matlab_config_dir)
42+
except FileNotFoundError:
43+
pass
4144

4245
request.addfinalizer(delete_matlab_test_dir)

0 commit comments

Comments
 (0)