From d672d309e80ed2f0b2924138c42ca8351177caa7 Mon Sep 17 00:00:00 2001 From: Eric Herrera Date: Sun, 1 Aug 2021 09:22:31 -0500 Subject: [PATCH] fix: Improve and fix codejail service integration Address recommendations and fixes discussed in PR. Use similar structure to notes app. Fix errors detected in tests. --- common/lib/capa/capa/safe_exec/exceptions.py | 24 +++++ common/lib/capa/capa/safe_exec/helpers.py | 97 ++++++++++++++++++++ common/lib/capa/capa/safe_exec/safe_exec.py | 50 +++------- lms/envs/common.py | 11 ++- 4 files changed, 141 insertions(+), 41 deletions(-) create mode 100644 common/lib/capa/capa/safe_exec/exceptions.py create mode 100644 common/lib/capa/capa/safe_exec/helpers.py diff --git a/common/lib/capa/capa/safe_exec/exceptions.py b/common/lib/capa/capa/safe_exec/exceptions.py new file mode 100644 index 0000000000..c6cb78c8f8 --- /dev/null +++ b/common/lib/capa/capa/safe_exec/exceptions.py @@ -0,0 +1,24 @@ +""" +Exceptions related to safe exec. +""" + + +class CodejailServiceParseError(Exception): + """ + An exception that is raised whenever we have issues with data parsing. + """ + pass # lint-amnesty, pylint: disable=unnecessary-pass + + +class CodejailServiceStatusError(Exception): + """ + An exception that is raised whenever Codejail service response status is different to 200. + """ + pass # lint-amnesty, pylint: disable=unnecessary-pass + + +class CodejailServiceUnavailable(Exception): + """ + An exception that is raised whenever Codejail service is unavailable. + """ + pass # lint-amnesty, pylint: disable=unnecessary-pass diff --git a/common/lib/capa/capa/safe_exec/helpers.py b/common/lib/capa/capa/safe_exec/helpers.py new file mode 100644 index 0000000000..76c62e4259 --- /dev/null +++ b/common/lib/capa/capa/safe_exec/helpers.py @@ -0,0 +1,97 @@ +""" +Helper methods related to safe exec. +""" + +import requests +import json +import logging + +from codejail.safe_exec import SafeExecException +from django.conf import settings +from django.utils.translation import ugettext as _ +from edx_toggles.toggles import SettingDictToggle +from requests.exceptions import RequestException, HTTPError +from simplejson import JSONDecodeError + +from .exceptions import CodejailServiceParseError, CodejailServiceStatusError, CodejailServiceUnavailable + +log = logging.getLogger(__name__) + +# .. toggle_name: ENABLE_CODEJAIL_REST_SERVICE +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Set this to True if you want to run Codejail code using +# a separate VM or container and communicate with edx-platform using REST API. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2021-08-19 +# .. toggle_target_removal_date: None +# .. toggle_warnings: +# .. toggle_tickets: +ENABLE_CODEJAIL_REST_SERVICE = SettingDictToggle( + "FEATURES", "ENABLE_CODEJAIL_REST_SERVICE", default=False, module_name=__name__ +) + + +def is_codejail_rest_service_enabled(): + return ENABLE_CODEJAIL_REST_SERVICE.is_enabled() + + +def get_codejail_rest_service_endpoint(): + return "".join([ + settings.CODE_JAIL_REST_SERVICE_HOST, + "/api/v0/code-exec"]) + + +def send_safe_exec_request(data, extra_files): + """ + Sends a request to a codejail api service forwarding required code and files. + Arguments: + data: Dict containing code and othe parameters + required for jailed code execution. + extra_files: python_lib.zip file containing extra files + required by the codejail execution. + Returns: + Response received from codejsail api service + """ + globals_dict = data["globals_dict"] + + codejail_service_endpoint = get_codejail_rest_service_endpoint() + payload = json.dumps(data) + + try: + response = requests.post( + codejail_service_endpoint, + files=extra_files, + data={'payload': payload}, + timeout=(settings.CODE_JAIL_REST_SERVICE_CONNECT_TIMEOUT, settings.CODE_JAIL_REST_SERVICE_READ_TIMEOUT) + ) + + except RequestException: + log.error("Failed to connect to codejail api service: url=%s, params=%s", + codejail_service_endpoint, str(payload)) + raise CodejailServiceUnavailable(_("Codejail API Service is unavailable. Please try again in a few minutes.")) # lint-amnesty, pylint: disable=raise-missing-from + + try: + response.raise_for_status() + except HTTPError: + raise CodejailServiceStatusError(_("Codejail API Service invalid response.")) # lint-amnesty, pylint: disable=raise-missing-from + + try: + response_json = response.json() + except JSONDecodeError: + log.error("Invalid JSON response received from codejail api service: Response_Content=%s", response.content) + raise CodejailServiceParseError(_("Invalid JSON response received from codejail api service.")) # lint-amnesty, pylint: disable=raise-missing-from + + emsg = response_json.get("emsg") + exception = None + + if emsg: + exception_msg = ". ".join([ + emsg, + "For more information check Codejail Service logs."]) + + exception = SafeExecException(exception_msg) + + globals_dict.update(response_json.get("globals_dict")) + + return emsg, exception diff --git a/common/lib/capa/capa/safe_exec/safe_exec.py b/common/lib/capa/capa/safe_exec/safe_exec.py index 2012d8544f..4b5d36c74d 100644 --- a/common/lib/capa/capa/safe_exec/safe_exec.py +++ b/common/lib/capa/capa/safe_exec/safe_exec.py @@ -2,18 +2,16 @@ import hashlib -import requests -import json from codejail.safe_exec import SafeExecException, json_safe from codejail.safe_exec import not_safe_exec as codejail_not_safe_exec from codejail.safe_exec import safe_exec as codejail_safe_exec -from django.conf import settings from edx_django_utils.monitoring import function_trace import six from six import text_type from . import lazymod +from .helpers import is_codejail_rest_service_enabled, send_safe_exec_request # Establish the Python environment for Capa. # Capa assumes float-friendly division always. @@ -146,40 +144,18 @@ def safe_exec( # Create the complete code we'll run. code_prolog = CODE_PROLOG % random_seed - if settings.FEATURES.get('ENABLE_CODEJAIL_REST_SERVICE', False): - try: - codejail_service_endpoint = "".join([ - settings.CODE_JAIL_REST_SERVICE_HOST, - "/api/v0/code-exec" - ]) - data = { - "code": code_prolog + LAZY_IMPORTS + code, - "globals_dict": globals_dict, - "python_path": python_path, - "limit_overrides_context": limit_overrides_context, - "slug": slug, - "unsafely": unsafely - } - datajson = json.dumps(data) - response = requests.request( - "POST", - codejail_service_endpoint, - files=extra_files, - data={'payload': datajson} - ) - response_json = response.json() - emsg = response_json["emsg"] - if emsg: - exception_msg = ". ".join([ - emsg, - "For more information check Codejail Service logs." - ]) - - exception = SafeExecException(emsg) - globals_dict.update(response_json["globals_dict"]) - except Exception as e: - raise e - emsg = None + if is_codejail_rest_service_enabled(): + data = { + "code": code_prolog + LAZY_IMPORTS + code, + "globals_dict": globals_dict, + "python_path": python_path, + "limit_overrides_context": limit_overrides_context, + "slug": slug, + "unsafely": unsafely + } + + emsg, exception = send_safe_exec_request(data, extra_files) + else: # Decide which code executor to use. if unsafely: diff --git a/lms/envs/common.py b/lms/envs/common.py index 12ad10d314..15c81ea58c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -948,17 +948,17 @@ FEATURES = { # .. toggle_target_removal_date: 2021-10-01 # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MICROBA-1405' 'ENABLE_V2_CERT_DISPLAY_SETTINGS': False, - + # .. toggle_name: ENABLE_CODEJAIL_REST_SERVICE # .. toggle_implementation: DjangoSetting # .. toggle_default: False # .. toggle_description: Set this to True if you want to run Codejail code using # a separate VM or container and communicate with edx-platform using REST API. - # .. toggle_use_cases: tutor + # .. toggle_use_cases: open_edx # .. toggle_creation_date: 2021-08-19 # .. toggle_target_removal_date: None - # .. toggle_warnings: - # .. toggle_tickets: + # .. toggle_warnings: + # .. toggle_tickets: 'ENABLE_CODEJAIL_REST_SERVICE': False, } @@ -1643,6 +1643,9 @@ COURSES_WITH_UNSAFE_CODE = [] # Cojail REST service CODE_JAIL_REST_SERVICE_HOST = 'http://127.0.0.1:8550' +CODE_JAIL_REST_SERVICE_CONNECT_TIMEOUT = 0.5 # time in seconds +CODE_JAIL_REST_SERVICE_READ_TIMEOUT = 3.5 # time in seconds + ############################### DJANGO BUILT-INS ############################### # Change DEBUG in your environment settings files, not here