Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion amazon-efs-utils.spec
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
%{?!include_vendor_tarball:%define include_vendor_tarball true}

Name : amazon-efs-utils
Version : 2.4.1
Version : 2.4.2
Release : 1%{platform}
Summary : This package provides utilities for simplifying the use of EFS file systems

Expand Down Expand Up @@ -196,6 +196,13 @@ fi
%clean

%changelog
* Tue Dec 23 2025 Samuel Hale <samuhale@amazon.com> - 2.4.2
- Skip stunnel binary invocation when efs-proxy mode is enabled
- Retry "access denied" only for access point mounting
- Fix issue for missing PATH in env when check stunnel lib
- Fix EFS_FQDN_RE to support ADC DNS suffixes with hyphens
- Fix IPv6-only mount target FQDN resolution in match_device

* Thu Nov 20 2025 Anthony Tse <anthotse@amazon.com> - 2.4.1
- Add cafile override for eusc-de-east-1 in efs-utils.conf

Expand Down
2 changes: 1 addition & 1 deletion build-deb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ set -ex

BASE_DIR=$(pwd)
BUILD_ROOT=${BASE_DIR}/build/debbuild
VERSION=2.4.1
VERSION=2.4.2
RELEASE=1
ARCH=$(dpkg --print-architecture)
DEB_SYSTEM_RELEASE_PATH=/etc/os-release
Expand Down
2 changes: 1 addition & 1 deletion config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
#

[global]
version=2.4.1
version=2.4.2
release=1
33 changes: 27 additions & 6 deletions src/mount_efs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
BOTOCORE_PRESENT = False


VERSION = "2.4.1"
VERSION = "2.4.2"
SERVICE = "elasticfilesystem"

AMAZON_LINUX_2_RELEASE_ID = "Amazon Linux release 2 (Karoo)"
Expand Down Expand Up @@ -1425,10 +1425,15 @@ def find_command_path(command, install_method):
# For more information, see https://brew.sh/2021/02/05/homebrew-3.0.0/
else:
env_path = "/opt/homebrew/bin:/usr/local/bin"
os.putenv("PATH", env_path)

existing_path = os.environ.get("PATH", "")
search_path = env_path + ":" + existing_path if existing_path else env_path

env = os.environ.copy()
env["PATH"] = search_path

try:
path = subprocess.check_output(["which", command])
path = subprocess.check_output(["which", command], env=env)
return path.strip().decode()
except subprocess.CalledProcessError as e:
fatal_error(
Expand Down Expand Up @@ -1479,7 +1484,7 @@ def write_stunnel_config_file(
hand-serialize it.
"""

stunnel_options = get_stunnel_options()
stunnel_options = [] if efs_proxy_enabled else get_stunnel_options()
mount_filename = get_mount_specific_filename(fs_id, mountpoint, tls_port)

system_release_version = get_system_release_version()
Expand Down Expand Up @@ -2223,9 +2228,15 @@ def backoff_function(i):
out, err = proc.communicate(timeout=retry_nfs_mount_command_timeout_sec)
rc = proc.poll()
if rc != 0:
is_access_point_mount = "accesspoint" in options
continue_retry = any(
error_string in str(err) for error_string in RETRYABLE_ERRORS
)

# Only retry "access denied" for access point mounts, handles race condition that can occur during AP backend provisioning
if not continue_retry and "access denied by server" in str(err):
continue_retry = is_access_point_mount

if continue_retry:
logging.error(
'Mounting %s to %s failed, return code=%s, stdout="%s", stderr="%s", mount attempt %d/%d, '
Expand Down Expand Up @@ -3223,8 +3234,18 @@ def match_device(config, device, options):
return remote, path, None

try:
primary, secondaries, _ = socket.gethostbyname_ex(remote)
hostnames = list(filter(lambda e: e is not None, [primary] + secondaries))
addrinfo = socket.getaddrinfo(
remote, None, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_CANONNAME
)
hostnames = list(
set(
filter(
lambda e: e is not None and e != "", [info[3] for info in addrinfo]
)
)
)
if not hostnames:
hostnames = [remote]
except socket.gaierror:
create_default_cloudwatchlog_agent_if_not_exist(config, options)
fatal_error(
Expand Down
2 changes: 1 addition & 1 deletion src/proxy/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "efs-proxy"
edition = "2021"
build = "build.rs"
# The version of efs-proxy is tied to efs-utils.
version = "2.4.1"
version = "2.4.2"
publish = false
license = "MIT"

Expand Down Expand Up @@ -34,6 +34,7 @@ xdr-codec = { path = "rust-xdr/xdr-codec"}
test-case = "*"
tokio = { version = "1.29.0", features = ["test-util"] }
tempfile = "3.10.1"
regex = "1.10.2"

[build-dependencies]
xdrgen = { path = "rust-xdr/xdrgen" }
Expand Down
2 changes: 0 additions & 2 deletions src/proxy/build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use xdrgen;

fn main() {
xdrgen::compile("src/efs_prot.x").expect("xdrgen efs_prot.x failed");
}
6 changes: 3 additions & 3 deletions src/proxy/rust-xdr/xdrgen/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,15 +1073,15 @@ impl Symtab {
}
}

pub fn constants(&self) -> Iter<String, (i64, Option<String>)> {
pub fn constants(&self) -> Iter<'_, String, (i64, Option<String>)> {
self.consts.iter()
}

pub fn typespecs(&self) -> Iter<String, Type> {
pub fn typespecs(&self) -> Iter<'_, String, Type> {
self.typespecs.iter()
}

pub fn typesyns(&self) -> Iter<String, Type> {
pub fn typesyns(&self) -> Iter<'_, String, Type> {
self.typesyns.iter()
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/proxy/src/config_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ where
}
}

fn default_log_format() -> Option<String> {
Some("file".to_string())
}

#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
#[serde(alias = "fips", deserialize_with = "deserialize_bool")]
Expand All @@ -33,6 +37,11 @@ pub struct ProxyConfig {
#[serde(alias = "output")]
pub output: Option<String>,

/// The format to use for logging. Values can be "file", "stdout"
/// Default is "file" if not specified.
#[serde(alias = "log_format", default = "default_log_format")]
pub log_format: Option<String>,

/// The proxy process is responsible for writing it's PID into this file so that the Watchdog
/// process can monitor it
#[serde(alias = "pid")]
Expand Down Expand Up @@ -136,6 +145,7 @@ checkHost = fs-12341234.efs.us-east-1.amazonaws.com
output: Some(String::from(
"/var/log/amazon/efs/fs-12341234.home.ec2-user.efs.21036.efs-proxy.log",
)),
log_format: Some(String::from("file")),
nested_config: EfsConfig {
listen_addr: String::from("127.0.0.1:21036"),
mount_target_addr: String::from("fs-12341234.efs.us-east-1.amazonaws.com:2049"),
Expand All @@ -162,6 +172,7 @@ socket = a:SO_BINDTODEVICE=lo
pid = /var/run/efs/fs-12341234.home.ec2-user.efs.21036+/stunnel.pid
port = 8081
initial_partition_ip = 127.0.0.1:2049
log_format = stdout

[efs]
accept = 127.0.0.1:21036
Expand All @@ -187,6 +198,7 @@ checkHost = fs-12341234.efs.us-east-1.amazonaws.com
),
debug: DEFAULT_LOG_LEVEL.to_string(),
output: None,
log_format: Some(String::from("stdout")),
nested_config: EfsConfig {
listen_addr: String::from("127.0.0.1:21036"),
mount_target_addr: String::from("fs-12341234.efs.us-east-1.amazonaws.com:2049"),
Expand Down
7 changes: 0 additions & 7 deletions src/proxy/src/efs_prot.x
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,3 @@ struct BindClientResponse {
BindResponse bind_response;
ScaleUpConfig scale_up_config;
};

union OperationResponse switch (OperationType operation_type) {
case OP_BIND_CLIENT_TO_PARTITION:
BindClientResponse response;
default:
void;
};
1 change: 1 addition & 0 deletions src/proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod connections;
pub mod controller;
pub mod efs_rpc;
pub mod error;
pub mod log_encoder;
pub mod logger;
pub mod proxy;
pub mod proxy_identifier;
Expand Down
132 changes: 132 additions & 0 deletions src/proxy/src/log_encoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use anyhow::Result;
use chrono::Utc;
use log4rs::encode::{Encode, Write};
use std::fmt;

/// Custom encoder that replaces newlines with spaces to keep multi-line logs on a single line
pub struct SingleLineEncoder;

impl Encode for SingleLineEncoder {
fn encode(&self, w: &mut dyn Write, record: &log::Record<'_>) -> Result<()> {
let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ");
let level = record.level();
let module = record.module_path().unwrap_or("-");
let message = format!("{}", record.args());
let single_line_message = message.replace('\n', " ");

writeln!(
w,
"{} {} {} {} {}",
timestamp,
std::process::id(),
level,
module,
single_line_message
)
.map_err(|e| anyhow::anyhow!(e))
}
}

impl fmt::Debug for SingleLineEncoder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SingleLineEncoder").finish()
}
}

#[cfg(test)]
mod tests {
use super::*;
use log::{Level, Record};
use regex::Regex;
use std::io;

struct BufferWriter<'a>(&'a mut Vec<u8>);

impl<'a> io::Write for BufferWriter<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}

fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

impl<'a> Write for BufferWriter<'a> {
// This trait is implemented automatically because BufferWriter implements io::Write
}

#[test]
fn test_format_log_message() {
let encoder = SingleLineEncoder;

let record = Record::builder()
.args(format_args!("Test message"))
.level(Level::Info)
.target("test_target")
.module_path(Some("test_module"))
.file(Some("test_file.rs"))
.line(Some(42))
.build();

let mut buffer = Vec::new();

let mut writer = BufferWriter(&mut buffer);
encoder.encode(&mut writer, &record).unwrap();

let output = String::from_utf8_lossy(&buffer);

let timestamp_regex = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z";
let pid_regex = r"\d+";
let level_regex = r"INFO";
let module_regex = r"test_module";
let message_regex = r"Test message";

let pattern = format!(
"^{} {} {} {} {}$",
timestamp_regex, pid_regex, level_regex, module_regex, message_regex
);

let regex = Regex::new(&pattern).unwrap();
assert!(
regex.is_match(output.trim()),
"Output format doesn't match expected pattern. Got: {}",
output
);
}

#[test]
fn test_multiline_message() {
let encoder = SingleLineEncoder;

let record = Record::builder()
.args(format_args!("Test\nmultiline\nmessage"))
.level(Level::Warn)
.target("test_target")
.module_path(Some("test_module"))
.file(Some("test_file.rs"))
.line(Some(42))
.build();

let mut buffer = Vec::new();

let mut writer = BufferWriter(&mut buffer);
encoder.encode(&mut writer, &record).unwrap();

let output = String::from_utf8_lossy(&buffer);

assert!(
output.contains("Test multiline message"),
"Multiline message not properly formatted. Got: {}",
output
);

let newline_count = output.chars().filter(|&c| c == '\n').count();
assert_eq!(
newline_count, 1,
"Expected only one newline at the end. Got: {}",
output
);
}
}
Loading