From dbb82c1d058de73af1f731311dd177b6b7838c18 Mon Sep 17 00:00:00 2001 From: knikure Date: Tue, 5 Mar 2024 06:26:52 +0000 Subject: [PATCH 1/3] fix: Create custom tarfile extractall util to fix backward compatibility issue --- src/sagemaker/local/image.py | 5 +- .../serve/model_server/djl_serving/prepare.py | 5 +- .../serve/model_server/tgi/prepare.py | 5 +- src/sagemaker/utils.py | 105 ++++++++++++++---- src/sagemaker/workflow/_repack_model.py | 93 +++++++++++++++- src/sagemaker/workflow/_utils.py | 5 +- tests/integ/s3_utils.py | 5 +- tests/unit/test_fw_utils.py | 5 +- tests/unit/test_utils.py | 56 ++++++++-- 9 files changed, 233 insertions(+), 51 deletions(-) diff --git a/src/sagemaker/local/image.py b/src/sagemaker/local/image.py index 39c879ef6d..377bdcac85 100644 --- a/src/sagemaker/local/image.py +++ b/src/sagemaker/local/image.py @@ -40,7 +40,7 @@ import sagemaker.local.data import sagemaker.local.utils import sagemaker.utils -from sagemaker.utils import check_tarfile_data_filter_attribute +from sagemaker.utils import custom_extractall_tarfile CONTAINER_PREFIX = "algo" STUDIO_HOST_NAME = "sagemaker-local" @@ -687,8 +687,7 @@ def _prepare_serving_volumes(self, model_location): for filename in model_data_source.get_file_list(): if tarfile.is_tarfile(filename): with tarfile.open(filename) as tar: - check_tarfile_data_filter_attribute() - tar.extractall(path=model_data_source.get_root_dir(), filter="data") + custom_extractall_tarfile(tar, model_data_source.get_root_dir()) volumes.append(_Volume(model_data_source.get_root_dir(), "/opt/ml/model")) diff --git a/src/sagemaker/serve/model_server/djl_serving/prepare.py b/src/sagemaker/serve/model_server/djl_serving/prepare.py index 6bdada0b6c..810acc8aff 100644 --- a/src/sagemaker/serve/model_server/djl_serving/prepare.py +++ b/src/sagemaker/serve/model_server/djl_serving/prepare.py @@ -20,7 +20,7 @@ from typing import List from pathlib import Path -from sagemaker.utils import _tmpdir, check_tarfile_data_filter_attribute +from sagemaker.utils import _tmpdir, custom_extractall_tarfile from sagemaker.s3 import S3Downloader from sagemaker.djl_inference import DJLModel from sagemaker.djl_inference.model import _read_existing_serving_properties @@ -53,8 +53,7 @@ def _extract_js_resource(js_model_dir: str, js_id: str): """Uncompress the jumpstart resource""" tmp_sourcedir = Path(js_model_dir).joinpath(f"infer-prepack-{js_id}.tar.gz") with tarfile.open(str(tmp_sourcedir)) as resources: - check_tarfile_data_filter_attribute() - resources.extractall(path=js_model_dir, filter="data") + custom_extractall_tarfile(resources, js_model_dir) def _copy_jumpstart_artifacts(model_data: str, js_id: str, code_dir: Path): diff --git a/src/sagemaker/serve/model_server/tgi/prepare.py b/src/sagemaker/serve/model_server/tgi/prepare.py index 9b187dd2ed..af09515da9 100644 --- a/src/sagemaker/serve/model_server/tgi/prepare.py +++ b/src/sagemaker/serve/model_server/tgi/prepare.py @@ -19,7 +19,7 @@ from pathlib import Path from sagemaker.serve.utils.local_hardware import _check_disk_space, _check_docker_disk_usage -from sagemaker.utils import _tmpdir, check_tarfile_data_filter_attribute +from sagemaker.utils import _tmpdir, custom_extractall_tarfile from sagemaker.s3 import S3Downloader logger = logging.getLogger(__name__) @@ -29,8 +29,7 @@ def _extract_js_resource(js_model_dir: str, code_dir: Path, js_id: str): """Uncompress the jumpstart resource""" tmp_sourcedir = Path(js_model_dir).joinpath(f"infer-prepack-{js_id}.tar.gz") with tarfile.open(str(tmp_sourcedir)) as resources: - check_tarfile_data_filter_attribute() - resources.extractall(path=code_dir, filter="data") + custom_extractall_tarfile(resources, code_dir) def _copy_jumpstart_artifacts(model_data: str, js_id: str, code_dir: Path) -> bool: diff --git a/src/sagemaker/utils.py b/src/sagemaker/utils.py index a6d26db48b..3706e9d8a1 100644 --- a/src/sagemaker/utils.py +++ b/src/sagemaker/utils.py @@ -22,7 +22,6 @@ import random import re import shutil -import sys import tarfile import tempfile import time @@ -31,6 +30,8 @@ import abc import uuid from datetime import datetime +from os.path import abspath, realpath, dirname, normpath, join as joinpath +from sys import stderr from importlib import import_module import botocore @@ -592,8 +593,7 @@ def _create_or_update_code_dir( download_file_from_url(source_directory, local_code_path, sagemaker_session) with tarfile.open(name=local_code_path, mode="r:gz") as t: - check_tarfile_data_filter_attribute() - t.extractall(path=code_dir, filter="data") + custom_extractall_tarfile(t, code_dir) elif source_directory: if os.path.exists(code_dir): @@ -630,8 +630,7 @@ def _extract_model(model_uri, sagemaker_session, tmp): else: local_model_path = model_uri.replace("file://", "") with tarfile.open(name=local_model_path, mode="r:gz") as t: - check_tarfile_data_filter_attribute() - t.extractall(path=tmp_model_dir, filter="data") + custom_extractall_tarfile(t, tmp_model_dir) return tmp_model_dir @@ -1494,23 +1493,89 @@ def format_tags(tags: Tags) -> List[TagsDict]: return tags -class PythonVersionError(Exception): - """Raise when a secure [/patched] version of Python is not used.""" +def _get_resolved_path(path): + """Return the normalized absolute path of a given path. + abspath - returns the absolute path without resolving symlinks + realpath - resolves the symlinks and gets the actual path + normpath - normalizes paths (e.g. remove redudant separators) + and handles platform-specific differences + """ + return normpath(realpath(abspath(path))) -def check_tarfile_data_filter_attribute(): - """Check if tarfile has data_filter utility. - Tarfile-data_filter utility has guardrails against untrusted de-serialisation. +def _is_bad_path(path, base): + """Checks if the joined path (base directory + file path) is rooted under the base directory - Raises: - PythonVersionError: if `tarfile.data_filter` is not available. + Ensuring that the file does not attempt to access paths + outside the expected directory structure. + + Args: + path (str): The file path. + base (str): The base directory. + + Returns: + bool: True if the path is not rooted under the base directory, False otherwise. """ - # The function and it's usages can be deprecated post support of python >= 3.12 - if not hasattr(tarfile, "data_filter"): - raise PythonVersionError( - f"Since tarfile extraction is unsafe the operation is prohibited " - f"per PEP-721. Please update your Python [{sys.version}] " - f"to latest patch [refer to https://www.python.org/downloads/] " - f"to consume the security patch" - ) + # joinpath will ignore base if path is absolute + return not _get_resolved_path(joinpath(base, path)).startswith(base) + + +def _is_bad_link(info, base): + """Checks if the link is rooted under the base directory. + + Ensuring that the link does not attempt to access paths outside the expected directory structure + + Args: + info (tarfile.TarInfo): The tar file info. + base (str): The base directory. + + Returns: + bool: True if the link is not rooted under the base directory, False otherwise. + """ + # Links are interpreted relative to the directory containing the link + tip = _get_resolved_path(joinpath(base, dirname(info.name))) + return _is_bad_path(info.linkname, base=tip) + + +def safe_members(members): + """A generator that yields members that are safe to extract. + + It checks for bad paths and bad links. + + Args: + members (list): A list of members to check. + + Yields: + tarfile.TarInfo: The tar file info. + """ + base = _get_resolved_path(".") + + for file_info in members: + if _is_bad_path(file_info.name, base): + print(stderr, file_info.name, "is blocked (illegal path)") + elif file_info.issym() and _is_bad_link(file_info, base): + print(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) + elif file_info.islnk() and _is_bad_link(file_info, base): + print(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) + else: + yield file_info + + +def custom_extractall_tarfile(tar, extract_path): + """Extract a tarfile, optionally using data_filter if available. + + If the tarfile has a data_filter attribute, it will be used to extract the contents of the file. + Otherwise, the safe_members function will be used to check for bad paths and bad links. + + Args: + tar (tarfile.TarFile): The opened tarfile object. + extract_path (str): The path to extract the contents of the tarfile. + + Returns: + None + """ + if hasattr(tar, "data_filter"): + tar.extractall(path=extract_path, filter="data") + else: + tar.extractall(path=extract_path, members=safe_members(tar)) diff --git a/src/sagemaker/workflow/_repack_model.py b/src/sagemaker/workflow/_repack_model.py index 3cfa6760b3..8a3eebd452 100644 --- a/src/sagemaker/workflow/_repack_model.py +++ b/src/sagemaker/workflow/_repack_model.py @@ -33,6 +33,97 @@ # repacking is some short-lived hackery, right?? from distutils.dir_util import copy_tree +from os.path import abspath, realpath, dirname, normpath, join as joinpath +from sys import stderr + + +def _get_resolved_path(path): + """Return the normalized absolute path of a given path. + + abspath - returns the absolute path without resolving symlinks + realpath - resolves the symlinks and gets the actual path + normpath - normalizes paths (e.g. remove redudant separators) + and handles platform-specific differences + """ + return normpath(realpath(abspath(path))) + + +def _is_bad_path(path, base): + """Checks if the joined path (base directory + file path) is rooted under the base directory + + Ensuring that the file does not attempt to access paths + outside the expected directory structure. + + Args: + path (str): The file path. + base (str): The base directory. + + Returns: + bool: True if the path is not rooted under the base directory, False otherwise. + """ + # joinpath will ignore base if path is absolute + return not _get_resolved_path(joinpath(base, path)).startswith(base) + + +def _is_bad_link(info, base): + """Checks if the link is rooted under the base directory. + + Ensuring that the link does not attempt to access paths outside the expected directory structure + + Args: + info (tarfile.TarInfo): The tar file info. + base (str): The base directory. + + Returns: + bool: True if the link is not rooted under the base directory, False otherwise. + """ + # Links are interpreted relative to the directory containing the link + tip = _get_resolved_path(joinpath(base, dirname(info.name))) + return _is_bad_path(info.linkname, base=tip) + + +def safe_members(members): + """A generator that yields members that are safe to extract. + + It checks for bad paths and bad links. + + Args: + members (list): A list of members to check. + + Yields: + tarfile.TarInfo: The tar file info. + """ + base = _get_resolved_path(".") + + for file_info in members: + if _is_bad_path(file_info.name, base): + print(stderr, file_info.name, "is blocked (illegal path)") + elif file_info.issym() and _is_bad_link(file_info, base): + print(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) + elif file_info.islnk() and _is_bad_link(file_info, base): + print(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) + else: + yield file_info + + +def custom_extractall_tarfile(tar, extract_path): + """Extract a tarfile, optionally using data_filter if available. + + If the tarfile has a data_filter attribute, it will be used to extract the contents of the file. + Otherwise, the safe_members function will be used to check for bad paths and bad links. + + Args: + tar (tarfile.TarFile): The opened tarfile object. + extract_path (str): The path to extract the contents of the tarfile. + + Returns: + None + """ + if hasattr(tar, "data_filter"): + tar.extractall(path=extract_path, filter="data") + else: + tar.extractall(path=extract_path, members=safe_members(tar)) + def repack(inference_script, model_archive, dependencies=None, source_dir=None): # pragma: no cover """Repack custom dependencies and code into an existing model TAR archive @@ -60,7 +151,7 @@ def repack(inference_script, model_archive, dependencies=None, source_dir=None): # extract the contents of the previous training job's model archive to the "src" # directory of this training job with tarfile.open(name=local_path, mode="r:gz") as tf: - tf.extractall(path=src_dir) + custom_extractall_tarfile(tf, src_dir) if source_dir: # copy /opt/ml/code to code/ diff --git a/src/sagemaker/workflow/_utils.py b/src/sagemaker/workflow/_utils.py index 1b88bfd924..1fafa646bf 100644 --- a/src/sagemaker/workflow/_utils.py +++ b/src/sagemaker/workflow/_utils.py @@ -36,7 +36,7 @@ _save_model, download_file_from_url, format_tags, - check_tarfile_data_filter_attribute, + custom_extractall_tarfile, ) from sagemaker.workflow.retry import RetryPolicy from sagemaker.workflow.utilities import trim_request_dict @@ -262,8 +262,7 @@ def _inject_repack_script_and_launcher(self): download_file_from_url(self._source_dir, old_targz_path, self.sagemaker_session) with tarfile.open(name=old_targz_path, mode="r:gz") as t: - check_tarfile_data_filter_attribute() - t.extractall(path=targz_contents_dir, filter="data") + custom_extractall_tarfile(t, targz_contents_dir) shutil.copy2(fname, os.path.join(targz_contents_dir, REPACK_SCRIPT)) with open( diff --git a/tests/integ/s3_utils.py b/tests/integ/s3_utils.py index 500dc4a33a..d5839c8409 100644 --- a/tests/integ/s3_utils.py +++ b/tests/integ/s3_utils.py @@ -19,7 +19,7 @@ import boto3 from six.moves.urllib.parse import urlparse -from sagemaker.utils import check_tarfile_data_filter_attribute +from sagemaker.utils import custom_extractall_tarfile def assert_s3_files_exist(sagemaker_session, s3_url, files): @@ -57,5 +57,4 @@ def extract_files_from_s3(s3_url, tmpdir, sagemaker_session): s3.Bucket(parsed_url.netloc).download_file(parsed_url.path.lstrip("/"), model) with tarfile.open(model, "r") as tar_file: - check_tarfile_data_filter_attribute() - tar_file.extractall(tmpdir, filter="data") + custom_extractall_tarfile(tar_file, tmpdir) diff --git a/tests/unit/test_fw_utils.py b/tests/unit/test_fw_utils.py index 4600785159..ddc2d27151 100644 --- a/tests/unit/test_fw_utils.py +++ b/tests/unit/test_fw_utils.py @@ -24,7 +24,7 @@ from mock import Mock, patch from sagemaker import fw_utils -from sagemaker.utils import name_from_image, check_tarfile_data_filter_attribute +from sagemaker.utils import name_from_image, custom_extractall_tarfile from sagemaker.session_settings import SessionSettings from sagemaker.instance_group import InstanceGroup @@ -424,8 +424,7 @@ def list_tar_files(folder, tar_ball, tmpdir): startpath = str(tmpdir.ensure(folder, dir=True)) with tarfile.open(name=tar_ball, mode="r:gz") as t: - check_tarfile_data_filter_attribute() - t.extractall(path=startpath, filter="data") + custom_extractall_tarfile(t, startpath) def walk(): for root, dirs, files in os.walk(startpath): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8488a8308e..0c399f52f6 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -42,8 +42,10 @@ resolve_nested_dict_value_from_config, update_list_of_dicts_with_values_from_config, volume_size_supported, - PythonVersionError, - check_tarfile_data_filter_attribute, + _get_resolved_path, + _is_bad_path, + _is_bad_link, + custom_extractall_tarfile, ) from tests.unit.sagemaker.workflow.helpers import CustomStep from sagemaker.workflow.parameters import ParameterString, ParameterInteger @@ -1076,7 +1078,7 @@ def list_tar_files(tar_ball, tmp): os.mkdir(startpath) with tarfile.open(name=tar_ball, mode="r:gz") as t: - t.extractall(path=startpath) + custom_extractall_tarfile(t, startpath) def walk(): for root, dirs, files in os.walk(startpath): @@ -1752,13 +1754,43 @@ def test_instance_family_from_full_instance_type(self): self.assertEqual(family, get_instance_type_family(instance_type)) -class TestCheckTarfileDataFilterAttribute(TestCase): - def test_check_tarfile_data_filter_attribute_unhappy_case(self): - with pytest.raises(PythonVersionError): - with patch("tarfile.data_filter", None): - delattr(tarfile, "data_filter") - check_tarfile_data_filter_attribute() +@pytest.fixture +def mock_custom_tarfile(): + class MockTarfile: + def __init__(self, data_filter=False): + self.data_filter = data_filter - def test_check_tarfile_data_filter_attribute_happy_case(self): - with patch("tarfile.data_filter", "some_value"): - check_tarfile_data_filter_attribute() + def extractall(self, path, members=None, filter=None): + assert path == "/extract/path" + if members is not None: + assert next(members).name == "file.txt" + + return MockTarfile + + +def test_get_resolved_path(): + assert _get_resolved_path("path/to/file") == os.path.normpath( + os.path.realpath(os.path.abspath("path/to/file")) + ) + + +@pytest.mark.parametrize("file_path, base, expected", [("file.txt", "/path/to/base", False)]) +def test_is_bad_path(file_path, base, expected): + assert _is_bad_path(file_path, base) == expected + + +@pytest.mark.parametrize( + "link_name, base, expected", [("link_to_file.txt", "/path/to/base", False)] +) +def test_is_bad_link(link_name, base, expected): + dummy_info = tarfile.TarInfo(name="dummy.txt") + dummy_info.linkname = link_name + assert _is_bad_link(dummy_info, base) == expected + + +@pytest.mark.parametrize( + "data_filter, expected_extract_path", [(True, "/extract/path"), (False, "/extract/path")] +) +def test_custom_extractall_tarfile(mock_custom_tarfile, data_filter, expected_extract_path): + tar = mock_custom_tarfile(data_filter) + custom_extractall_tarfile(tar, "/extract/path") From fa3dfc2dfcf759b1edeec4afe55775bea99faaed Mon Sep 17 00:00:00 2001 From: knikure Date: Tue, 5 Mar 2024 22:43:07 +0000 Subject: [PATCH 2/3] Address review comments --- src/sagemaker/utils.py | 19 +++++++++++-------- src/sagemaker/workflow/_repack_model.py | 22 ++++++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/sagemaker/utils.py b/src/sagemaker/utils.py index 3706e9d8a1..f78e273859 100644 --- a/src/sagemaker/utils.py +++ b/src/sagemaker/utils.py @@ -1538,10 +1538,10 @@ def _is_bad_link(info, base): return _is_bad_path(info.linkname, base=tip) -def safe_members(members): +def _get_safe_members(members): """A generator that yields members that are safe to extract. - It checks for bad paths and bad links. + It filters out bad paths and bad links. Args: members (list): A list of members to check. @@ -1553,11 +1553,11 @@ def safe_members(members): for file_info in members: if _is_bad_path(file_info.name, base): - print(stderr, file_info.name, "is blocked (illegal path)") + logger.error(stderr, file_info.name, "is blocked (illegal path)") elif file_info.issym() and _is_bad_link(file_info, base): - print(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) + logger.error(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) elif file_info.islnk() and _is_bad_link(file_info, base): - print(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) + logger.error(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) else: yield file_info @@ -1565,8 +1565,11 @@ def safe_members(members): def custom_extractall_tarfile(tar, extract_path): """Extract a tarfile, optionally using data_filter if available. + # TODO: The function and it's usages can be deprecated once SageMaker Python SDK + is upgraded to use Python 3.12+ + If the tarfile has a data_filter attribute, it will be used to extract the contents of the file. - Otherwise, the safe_members function will be used to check for bad paths and bad links. + Otherwise, the _get_safe_members function will be used to filter bad paths and bad links. Args: tar (tarfile.TarFile): The opened tarfile object. @@ -1575,7 +1578,7 @@ def custom_extractall_tarfile(tar, extract_path): Returns: None """ - if hasattr(tar, "data_filter"): + if hasattr(tarfile, "data_filter"): tar.extractall(path=extract_path, filter="data") else: - tar.extractall(path=extract_path, members=safe_members(tar)) + tar.extractall(path=extract_path, members=_get_safe_members(tar)) diff --git a/src/sagemaker/workflow/_repack_model.py b/src/sagemaker/workflow/_repack_model.py index 8a3eebd452..bdf6c15e52 100644 --- a/src/sagemaker/workflow/_repack_model.py +++ b/src/sagemaker/workflow/_repack_model.py @@ -14,6 +14,7 @@ from __future__ import absolute_import import argparse +import logging import os import shutil import tarfile @@ -36,6 +37,8 @@ from os.path import abspath, realpath, dirname, normpath, join as joinpath from sys import stderr +logger = logging.getLogger(__name__) + def _get_resolved_path(path): """Return the normalized absolute path of a given path. @@ -82,10 +85,10 @@ def _is_bad_link(info, base): return _is_bad_path(info.linkname, base=tip) -def safe_members(members): +def _get_safe_members(members): """A generator that yields members that are safe to extract. - It checks for bad paths and bad links. + It filters out bad paths and bad links. Args: members (list): A list of members to check. @@ -97,11 +100,11 @@ def safe_members(members): for file_info in members: if _is_bad_path(file_info.name, base): - print(stderr, file_info.name, "is blocked (illegal path)") + logger.error(stderr, file_info.name, "is blocked (illegal path)") elif file_info.issym() and _is_bad_link(file_info, base): - print(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) + logger.error(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) elif file_info.islnk() and _is_bad_link(file_info, base): - print(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) + logger.error(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) else: yield file_info @@ -109,8 +112,11 @@ def safe_members(members): def custom_extractall_tarfile(tar, extract_path): """Extract a tarfile, optionally using data_filter if available. + # TODO: The function and it's usages can be deprecated once SageMaker Python SDK + is upgraded to use Python 3.12+ + If the tarfile has a data_filter attribute, it will be used to extract the contents of the file. - Otherwise, the safe_members function will be used to check for bad paths and bad links. + Otherwise, the _get_safe_members function will be used to filter bad paths and bad links. Args: tar (tarfile.TarFile): The opened tarfile object. @@ -119,10 +125,10 @@ def custom_extractall_tarfile(tar, extract_path): Returns: None """ - if hasattr(tar, "data_filter"): + if hasattr(tarfile, "data_filter"): tar.extractall(path=extract_path, filter="data") else: - tar.extractall(path=extract_path, members=safe_members(tar)) + tar.extractall(path=extract_path, members=_get_safe_members(tar)) def repack(inference_script, model_archive, dependencies=None, source_dir=None): # pragma: no cover From 9db053968c0b4beb3b720acec50f97d60b7e0573 Mon Sep 17 00:00:00 2001 From: knikure Date: Wed, 6 Mar 2024 06:32:17 +0000 Subject: [PATCH 3/3] fix logger.error statements --- src/sagemaker/utils.py | 7 +++---- src/sagemaker/workflow/_repack_model.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/sagemaker/utils.py b/src/sagemaker/utils.py index f78e273859..115b8b258d 100644 --- a/src/sagemaker/utils.py +++ b/src/sagemaker/utils.py @@ -31,7 +31,6 @@ import uuid from datetime import datetime from os.path import abspath, realpath, dirname, normpath, join as joinpath -from sys import stderr from importlib import import_module import botocore @@ -1553,11 +1552,11 @@ def _get_safe_members(members): for file_info in members: if _is_bad_path(file_info.name, base): - logger.error(stderr, file_info.name, "is blocked (illegal path)") + logger.error("%s is blocked (illegal path)", file_info.name) elif file_info.issym() and _is_bad_link(file_info, base): - logger.error(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) + logger.error("%s is blocked: Symlink to %s", file_info.name, file_info.linkname) elif file_info.islnk() and _is_bad_link(file_info, base): - logger.error(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) + logger.error("%s is blocked: Hard link to %s", file_info.name, file_info.linkname) else: yield file_info diff --git a/src/sagemaker/workflow/_repack_model.py b/src/sagemaker/workflow/_repack_model.py index bdf6c15e52..84b3a426f6 100644 --- a/src/sagemaker/workflow/_repack_model.py +++ b/src/sagemaker/workflow/_repack_model.py @@ -35,7 +35,6 @@ from distutils.dir_util import copy_tree from os.path import abspath, realpath, dirname, normpath, join as joinpath -from sys import stderr logger = logging.getLogger(__name__) @@ -100,11 +99,11 @@ def _get_safe_members(members): for file_info in members: if _is_bad_path(file_info.name, base): - logger.error(stderr, file_info.name, "is blocked (illegal path)") + logger.error("%s is blocked (illegal path)", file_info.name) elif file_info.issym() and _is_bad_link(file_info, base): - logger.error(stderr, file_info.name, "is blocked: Symlink to", file_info.linkname) + logger.error("%s is blocked: Symlink to %s", file_info.name, file_info.linkname) elif file_info.islnk() and _is_bad_link(file_info, base): - logger.error(stderr, file_info.name, "is blocked: Hard link to", file_info.linkname) + logger.error("%s is blocked: Hard link to %s", file_info.name, file_info.linkname) else: yield file_info