Commit e9af4aa4 authored by Pit Kleyersburg's avatar Pit Kleyersburg Committed by maxbecke
Browse files

Allow a CNAME DNS-name in addition to the EFS ID

This change introduces additional logic which allows the user to specify
either a fully-qualified EFS DNS name, or a custom DNS name that
resolves to a fully-qualified EFS DNS name via a CNAME record.

The EFS DNS name will then be compared to the EFS name we would have
expected for the given EFS ID. If it doesn't match, we'll show the user
a readable error message for this to aid in troubleshooting.

Once the EFS DNS name has been verified, the rest of the logic is
untouched. This means that this change only impacts the parameter logic
at the very start and does not touch anything of the already tested,
more critical logic.

Important: the usage and internal logic to mount by EFS ID is unchanged,
making this change fully backwards compatible.

Fixes #9.
parent 87af3b6d
......@@ -3,7 +3,7 @@
\fBmount\&.efs\fR \- Mount helper for using Amazon EFS file systems\&.
.SH "SYNOPSIS"
.sp
\fBmount\&.efs\fR \fIfile\-system\-id\fR \fImount-point\fR [\fB\-o\fR \fIoptions\fR]
\fBmount\&.efs\fR \fIfs-id-or-dns-name\fR \fImount-point\fR [\fB\-o\fR \fIoptions\fR]
.SH "DESCRIPTION"
.sp
\fBmount\&.efs\fR is part of the \fBamazon\-efs\-utils\fR \
......@@ -12,9 +12,21 @@ package, which simplifies using EFS file systems\&.
\fBmount\&.efs\fR is meant to be used through the \
\fBmount\fR(8) command for mounting EFS file systems\&.
.sp
\fIfile\-system\-id\fR is an EFS file system ID in the \
form of "fs\-abcd1234", generated when the file system \
is created\&. \fImount-point\fR is the local directory \
\fIfs-id-or-dns-name\fR has to be of any of the following \
three forms:
.P
.IP \(bu
An EFS filesystem ID in the form of "fs\-abcd1234", generated \
when the file system is created\&.
.IP \(bu
A fully-qualified EFS DNS name in the form of \
"fs\-abcd1234\&.efs\&.us-east-1\&.amazonaws\&.com"\&.
.IP \(bu
A domain name that has a resolvable DNS-CNAME record, \
which in turn points to a fully-qualified EFS DNS name \
in the form of "fs\-abcd1234\&.efs\&.us-east-1\&.amazonaws\&.com"\&.
.P
\fImount-point\fR is the local directory \
on which the file system will be mounted\&.
.sp
\fBmount\&.efs\fR automatically applies the following NFS options:
......@@ -87,6 +99,20 @@ Mount an EFS file system with file system ID "fs-abcd1234" at mount point \
sudo mount -t efs -o tls,verify=0 fs-abcd1234 /mnt/efs
Mount an EFS file system with file system ID "fs-abcd1234" at mount point \
"/mnt/efs" using encryption of data in transit and a verify level of 0\&.
.TP
sudo mount -t efs fs-abcd1234.efs.us-east-1.amazonaws.com /mnt/efs
Mount an EFS file system with the fully-qualified EFS DNS name \
"fs\-abcd1234\&.efs\&.us-east-1\&.amazonaws\&.com" at mount point \
"/mnt/efs" without encryption of data in transit\&. If the region \
is correct, this is identical to mounting with just the file system ID\&.
.TP
sudo mount -t efs custom-cname.example.com /mnt/efs
Mount an EFS file system using the custom DNS name \
"custom-cname\&.example\&.com" \(em which has to \
resolve to a fully-qualified EFS DNS name such as \
"fs\-abcd1234\&.efs\&.us-east-1\&.amazonaws\&.com" \
\(em at mount point "/mnt/efs" without encryption \
of data in transit\&.
.SH "FILES"
.TP
\fI/sbin/mount.efs\fR
......
......@@ -64,7 +64,8 @@ LOG_FILE = 'mount.log'
STATE_FILE_DIR = '/var/run/efs'
FS_NAME_RE = re.compile('^(?P<fs_id>fs-[0-9a-f]+)(?::(?P<path>/.*))?$')
FS_ID_RE = re.compile('^(?P<fs_id>fs-[0-9a-f]+)$')
EFS_FQDN_RE = re.compile('^(?P<fs_id>fs-[0-9a-f]+)\.efs\.(?P<region>[a-z0-9-]+)\.amazonaws.com$')
INSTANCE_METADATA_SERVICE_URL = 'http://169.254.169.254/latest/dynamic/instance-identity/document/'
......@@ -465,7 +466,7 @@ def mount_nfs(dns_name, path, mountpoint, options):
fatal_error(err.strip(), message, proc.returncode)
def parse_arguments(args=None):
def parse_arguments(config, args=None):
"""Parse arguments, return (fsid, path, mountpoint, options)"""
if args is None:
args = sys.argv
......@@ -495,12 +496,7 @@ def parse_arguments(args=None):
if not fsname or not mountpoint:
usage()
match = FS_NAME_RE.match(fsname)
if not match:
fatal_error('Invalid file system name: %s' % fsname)
fs_id = match.group('fs_id')
path = match.group('path') or '/'
fs_id, path = match_device(config, fsname)
return fs_id, path, mountpoint, options
......@@ -578,6 +574,72 @@ def get_dns_name(config, fs_id):
return dns_name
def match_device(config, device):
"""
Return the EFS id and the remote path to mount.
:param config: the current configuration
:param device: the device descriptor, separating an EFS id or a DNS name and the remote path to mount by a colon
:return: a two element tuple of the EFS id and the remote path to mount
"""
# The device descriptor as specified separates the remote filesystem by the path to mount by a colon. Since colons
# are not allowed in either an EFS id or in a domain name, we can left-split once. (If left-splitting fails, the
# user didn't specify a path and we use '/' as the default.)
try:
remote, path = device.split(':', 1)
except ValueError:
remote = device
path = '/'
# The simplest case is that the remote is already the EFS id. If so, we return it as is.
if FS_ID_RE.match(remote):
return remote, path
# If the user did not specify an EFS id, we first check for the special case where the user supplied us with the
# FQDN of the EFS.
efs_fqdn_match = EFS_FQDN_RE.match(remote)
if efs_fqdn_match:
fs_id = efs_fqdn_match.group('fs_id')
expected_dns_name = get_dns_name(config, fs_id)
if remote == expected_dns_name:
return fs_id, path
else:
fatal_error(
'Fully qualified EFS domain name specified "%s", but it didn\'t match the expected value "%s"'
% (remote, expected_dns_name),
'EFS FQDN "%s" didn\'t match expected "%s"' % (remote, expected_dns_name)
)
# For the final case we now assume that the user specified a DNS resolvable name with a CNAME record that points to
# a valid EFS FQDN. To verify this, we'll use `socket.gethostbyname_ex()` which returns an alias-list for, among
# other things, a CNAME.
try:
primary, secondaries, _ = socket.gethostbyname_ex(remote)
hostnames = filter(lambda e: e is not None, [primary] + secondaries)
except socket.gaierror:
fatal_error(
'Failed to resolve "%s" - check that the specified DNS name is a CNAME record resolving to a valid EFS DNS '
'name' % remote,
'Failed to resolve "%s"' % remote
)
if not hostnames:
fatal_error(
'The specified domain name "%s" returned no entries, where at least one was expected' % remote
)
for hostname in hostnames:
efs_fqdn_match = EFS_FQDN_RE.match(hostname)
if efs_fqdn_match:
fs_id = efs_fqdn_match.group('fs_id')
expected_dns_name = get_dns_name(config, fs_id)
if hostname == expected_dns_name:
return fs_id, path
else:
fatal_error('The specified domain name "%s" resolved to no valid/expected EFS DNS name' % remote)
def mount_tls(config, init_system, dns_name, path, fs_id, mountpoint, options):
with bootstrap_tls(config, init_system, dns_name, fs_id, mountpoint, options) as tunnel_proc:
mount_completed = threading.Event()
......@@ -599,12 +661,13 @@ def check_unsupported_options(options):
def main():
fs_id, path, mountpoint, options = parse_arguments()
assert_root()
config = read_config()
bootstrap_logging(config)
fs_id, path, mountpoint, options = parse_arguments(config)
logging.info('version=%s options=%s', VERSION, options)
check_unsupported_options(options)
......
#
# Copyright 2017-2018 Amazon.com, Inc. and its affiliates. All Rights Reserved.
#
# Licensed under the MIT License. See the LICENSE accompanying this file
# for the specific language governing permissions and limitations under
# the License.
#
import socket
import pytest
import mount_efs
CORRECT_DEVICE_DESCRIPTORS_FS_ID = [
('fs-deadbeef', ('fs-deadbeef', '/')),
('fs-deadbeef:/', ('fs-deadbeef', '/')),
('fs-deadbeef:/some/subpath', ('fs-deadbeef', '/some/subpath')),
('fs-deadbeef:/some/subpath/with:colons', ('fs-deadbeef', '/some/subpath/with:colons')),
]
CORRECT_DEVICE_DESCRIPTORS_EFS_FQDN = [
('fs-deadbeef.efs.us-east-1.amazonaws.com', ('fs-deadbeef', '/')),
('fs-deadbeef.efs.us-east-1.amazonaws.com:/', ('fs-deadbeef', '/')),
('fs-deadbeef.efs.us-east-1.amazonaws.com:/some/subpath', ('fs-deadbeef', '/some/subpath')),
('fs-deadbeef.efs.us-east-1.amazonaws.com:/some/subpath/with:colons', ('fs-deadbeef', '/some/subpath/with:colons')),
]
CORRECT_DEVICE_DESCRIPTORS_CNAME_DNS = [
('custom-cname.example.com', ('fs-deadbeef', '/')),
('custom-cname.example.com:/', ('fs-deadbeef', '/')),
('custom-cname.example.com:/some/subpath', ('fs-deadbeef', '/some/subpath')),
('custom-cname.example.com:/some/subpath/with:colons', ('fs-deadbeef', '/some/subpath/with:colons')),
]
def test_match_device_correct_descriptors_fs_id(mocker):
for device, (fs_id, path) in CORRECT_DEVICE_DESCRIPTORS_FS_ID:
assert (fs_id, path) == mount_efs.match_device(None, device)
def test_match_device_correct_descriptors_efs_fqdn(mocker):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-east-1.amazonaws.com')
for device, (fs_id, path) in CORRECT_DEVICE_DESCRIPTORS_EFS_FQDN:
assert (fs_id, path) == mount_efs.match_device(None, device)
get_dns_name_mock.assert_called()
def test_match_device_correct_descriptors_cname_dns_primary(mocker):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-east-1.amazonaws.com')
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=('fs-deadbeef.efs.us-east-1.amazonaws.com', [], None))
for device, (fs_id, path) in CORRECT_DEVICE_DESCRIPTORS_CNAME_DNS:
assert (fs_id, path) == mount_efs.match_device(None, device)
get_dns_name_mock.assert_called()
gethostbyname_ex_mock.assert_called()
def test_match_device_correct_descriptors_cname_dns_secondary(mocker):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-east-1.amazonaws.com')
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=(None, ['fs-deadbeef.efs.us-east-1.amazonaws.com'], None))
for device, (fs_id, path) in CORRECT_DEVICE_DESCRIPTORS_CNAME_DNS:
assert (fs_id, path) == mount_efs.match_device(None, device)
get_dns_name_mock.assert_called()
gethostbyname_ex_mock.assert_called()
def test_match_device_correct_descriptors_cname_dns_tertiary(mocker):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-east-1.amazonaws.com')
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=(None, [None, 'fs-deadbeef.efs.us-east-1.amazonaws.com'], None))
for device, (fs_id, path) in CORRECT_DEVICE_DESCRIPTORS_CNAME_DNS:
assert (fs_id, path) == mount_efs.match_device(None, device)
get_dns_name_mock.assert_called()
gethostbyname_ex_mock.assert_called()
def test_match_device_correct_descriptors_cname_dns_amongst_invalid(mocker):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-east-1.amazonaws.com')
gethostbyname_ex_mock = mocker.patch(
'socket.gethostbyname_ex',
return_value=('fs-deadbeef.efs.us-west-1.amazonaws.com',
['fs-deadbeef.efs.us-east-1.amazonaws.com', 'invalid-efs-name.example.com'],
None)
)
for device, (fs_id, path) in CORRECT_DEVICE_DESCRIPTORS_CNAME_DNS:
assert (fs_id, path) == mount_efs.match_device(None, device)
get_dns_name_mock.assert_called()
gethostbyname_ex_mock.assert_called()
def test_match_device_wrong_efs_dns_name(mocker, capsys):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-west-1.amazonaws.com')
with pytest.raises(SystemExit) as ex:
mount_efs.match_device(None, 'fs-deadbeef.efs.us-east-1.amazonaws.com:/')
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'Fully qualified EFS domain name specified' in err
assert 'didn\'t match the expected value' in err
get_dns_name_mock.assert_called()
def test_match_device_unresolvable_domain(mocker, capsys):
mocker.patch('socket.gethostbyname_ex', side_effect=socket.gaierror)
with pytest.raises(SystemExit) as ex:
mount_efs.match_device(None, 'custom-cname.example.com')
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'Failed to resolve' in err
def test_match_device_no_hostnames(mocker, capsys):
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=(None, [], None))
with pytest.raises(SystemExit) as ex:
mount_efs.match_device(None, 'custom-cname.example.com')
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'returned no entries' in err
gethostbyname_ex_mock.assert_called()
def test_match_device_no_hostnames2(mocker, capsys):
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=(None, [None, None], None))
with pytest.raises(SystemExit) as ex:
mount_efs.match_device(None, 'custom-cname.example.com')
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'returned no entries' in err
gethostbyname_ex_mock.assert_called()
def test_match_device_resolve_to_invalid_efs_dns_name(mocker, capsys):
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=('invalid-efs-name.example.com', [], None))
with pytest.raises(SystemExit) as ex:
mount_efs.match_device(None, 'custom-cname.example.com')
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'resolved to no valid/expected EFS DNS name' in err
gethostbyname_ex_mock.assert_called()
def test_match_device_resolve_to_unexpected_efs_dns_name(mocker, capsys):
get_dns_name_mock = mocker.patch('mount_efs.get_dns_name', return_value='fs-deadbeef.efs.us-west-1.amazonaws.com')
gethostbyname_ex_mock = mocker.patch('socket.gethostbyname_ex',
return_value=('fs-deadbeef.efs.us-east-1.amazonaws.com', [], None))
with pytest.raises(SystemExit) as ex:
mount_efs.match_device(None, 'custom-cname.example.com')
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'resolved to no valid/expected EFS DNS name' in err
get_dns_name_mock.assert_called()
gethostbyname_ex_mock.assert_called()
......@@ -13,7 +13,7 @@ import pytest
def _test_parse_arguments_help(capsys, help):
with pytest.raises(SystemExit) as ex:
mount_efs.parse_arguments(['mount', 'foo', 'bar', help])
mount_efs.parse_arguments(None, ['mount', 'foo', 'bar', help])
assert 0 == ex.value.code
......@@ -31,7 +31,7 @@ def test_parse_arguments_help_short(capsys):
def test_parse_arguments_version(capsys):
with pytest.raises(SystemExit) as ex:
mount_efs.parse_arguments(['mount', 'foo', 'bar', '--version'])
mount_efs.parse_arguments(None, ['mount', 'foo', 'bar', '--version'])
assert 0 == ex.value.code
......@@ -41,7 +41,7 @@ def test_parse_arguments_version(capsys):
def test_parse_arguments_no_fs_id(capsys):
with pytest.raises(SystemExit) as ex:
mount_efs.parse_arguments(['mount'])
mount_efs.parse_arguments(None, ['mount'])
assert 0 != ex.value.code
......@@ -51,7 +51,7 @@ def test_parse_arguments_no_fs_id(capsys):
def test_parse_arguments_no_mount_point(capsys):
with pytest.raises(SystemExit) as ex:
mount_efs.parse_arguments(['mount', 'fs-deadbeef'])
mount_efs.parse_arguments(None, ['mount', 'fs-deadbeef'])
assert 0 != ex.value.code
......@@ -59,18 +59,8 @@ def test_parse_arguments_no_mount_point(capsys):
assert 'Usage:' in err
def test_parse_arguments_invalid_fs_id(capsys):
with pytest.raises(SystemExit) as ex:
mount_efs.parse_arguments(['mount', 'not-a-file-system-id', '/dir'])
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'Invalid file system name' in err
def test_parse_arguments_default_path():
fsid, path, mountpoint, options = mount_efs.parse_arguments(['mount', 'fs-deadbeef', '/dir'])
fsid, path, mountpoint, options = mount_efs.parse_arguments(None, ['mount', 'fs-deadbeef', '/dir'])
assert 'fs-deadbeef' == fsid
assert '/' == path
......@@ -79,7 +69,7 @@ def test_parse_arguments_default_path():
def test_parse_arguments_custom_path():
fsid, path, mountpoint, options = mount_efs.parse_arguments(['mount', 'fs-deadbeef:/home', '/dir'])
fsid, path, mountpoint, options = mount_efs.parse_arguments(None, ['mount', 'fs-deadbeef:/home', '/dir'])
assert 'fs-deadbeef' == fsid
assert '/home' == path
......@@ -88,7 +78,7 @@ def test_parse_arguments_custom_path():
def test_parse_arguments():
fsid, path, mountpoint, options = mount_efs.parse_arguments(['mount', 'fs-deadbeef:/home', '/dir', '-o', 'foo,bar=baz,quux'])
fsid, path, mountpoint, options = mount_efs.parse_arguments(None, ['mount', 'fs-deadbeef:/home', '/dir', '-o', 'foo,bar=baz,quux'])
assert 'fs-deadbeef' == fsid
assert '/home' == path
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment