__init__.py 83.2 KB
Newer Older
1
#!/usr/bin/env python3
Max Beckett's avatar
Max Beckett committed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
# 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.
#
#
# Copy this script to /sbin/mount.efs and make sure it is executable.
#
# You will be able to mount an EFS file system by its short name, by adding it
# to /etc/fstab. The syntax of an fstab entry is:
#
# [Device] [Mount Point] [File System Type] [Options] [Dump] [Pass]
#
# Add an entry like this:
#
#   fs-deadbeef     /mount_point    efs     _netdev         0   0
#
# Using the 'efs' type will cause '/sbin/mount.efs' to be called by 'mount -a'
# for this file system. The '_netdev' option tells the init system that the
# 'efs' type is a networked file system type. This has been tested with systemd
# (Amazon Linux 2, CentOS 7, RHEL 7, Debian 9, and Ubuntu 16.04), and upstart
# (Amazon Linux 2017.09).
#
# Once there is an entry in fstab, the file system can be mounted with:
#
#   sudo mount /mount_point
#
# The script will add recommended mount options, if not provided in fstab.

33
import base64
34
import errno
35
36
import hashlib
import hmac
Max Beckett's avatar
Max Beckett committed
37
38
39
import json
import logging
import os
40
import platform
41
import pwd
Max Beckett's avatar
Max Beckett committed
42
43
44
45
46
47
import random
import re
import socket
import subprocess
import sys
import threading
48
import time
Max Beckett's avatar
Max Beckett committed
49
50

from contextlib import contextmanager
51
from datetime import datetime, timedelta
Max Beckett's avatar
Max Beckett committed
52
53
from logging.handlers import RotatingFileHandler

Yuan Gao's avatar
Yuan Gao committed
54
try:
55
56
    from configparser import ConfigParser, NoOptionError, NoSectionError
except ImportError:
57
58
    import ConfigParser
    from ConfigParser import NoOptionError, NoSectionError
Yuan Gao's avatar
Yuan Gao committed
59

Max Beckett's avatar
Max Beckett committed
60
try:
61
    from urllib.parse import quote_plus
Max Beckett's avatar
Max Beckett committed
62
except ImportError:
63
    from urllib import quote_plus
Max Beckett's avatar
Max Beckett committed
64
65

try:
66
67
    from urllib.request import urlopen, Request
    from urllib.error import URLError, HTTPError
68
    from urllib.parse import urlencode
69
70
71
except ImportError:
    from urllib2 import URLError, HTTPError, build_opener, urlopen, Request, HTTPHandler
    from urllib import urlencode
Max Beckett's avatar
Max Beckett committed
72

73
74
75
76
77
78
79
try:
    import botocore.session
    from botocore.exceptions import ClientError, NoCredentialsError, EndpointConnectionError
    BOTOCORE_PRESENT = True
except ImportError:
    BOTOCORE_PRESENT = False

80

81
VERSION = '1.30.1'
82
SERVICE = 'elasticfilesystem'
Max Beckett's avatar
Max Beckett committed
83

84
CLONE_NEWNET = 0x40000000
Max Beckett's avatar
Max Beckett committed
85
86
CONFIG_FILE = '/etc/amazon/efs/efs-utils.conf'
CONFIG_SECTION = 'mount'
87
88
CLIENT_INFO_SECTION = 'client-info'
CLIENT_SOURCE_STR_LEN_LIMIT = 100
89
90
# Cloudwatchlog agent dict includes cloudwatchlog botocore client, cloudwatchlog group name, cloudwatchlog stream name
CLOUDWATCHLOG_AGENT = None
91
92
93
CLOUDWATCH_LOG_SECTION = 'cloudwatch-log'
DEFAULT_CLOUDWATCH_LOG_GROUP = '/aws/efs/utils'
DEFAULT_RETENTION_DAYS = 14
94
DEFAULT_UNKNOWN_VALUE = 'unknown'
95

Max Beckett's avatar
Max Beckett committed
96
97
98
99
100
LOG_DIR = '/var/log/amazon/efs'
LOG_FILE = 'mount.log'

STATE_FILE_DIR = '/var/run/efs'

101
102
103
104
105
PRIVATE_KEY_FILE = '/etc/amazon/efs/privateKey.pem'
DATE_ONLY_FORMAT = '%Y%m%d'
SIGV4_DATETIME_FORMAT = '%Y%m%dT%H%M%SZ'
CERT_DATETIME_FORMAT = '%y%m%d%H%M%SZ'

106
107
108
AWS_CREDENTIALS_FILE = os.path.expanduser(os.path.join('~' + pwd.getpwuid(os.getuid()).pw_name, '.aws', 'credentials'))
AWS_CONFIG_FILE = os.path.expanduser(os.path.join('~' + pwd.getpwuid(os.getuid()).pw_name, '.aws', 'config'))

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
CA_CONFIG_BODY = """dir = %s
RANDFILE = $dir/database/.rand

[ ca ]
default_ca = local_ca

[ local_ca ]
database = $dir/database/index.txt
serial = $dir/database/serial
private_key = %s
cert = $dir/certificate.pem
new_certs_dir = $dir/certs
default_md = sha256
preserve = no
policy = efsPolicy
x509_extensions = v3_ca

[ efsPolicy ]
CN = supplied

[ req ]
prompt = no
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
CN = %s

%s

138
139
%s

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
%s
"""

# SigV4 Auth
ALGORITHM = 'AWS4-HMAC-SHA256'
AWS4_REQUEST = 'aws4_request'

HTTP_REQUEST_METHOD = 'GET'
CANONICAL_URI = '/'
CANONICAL_HEADERS_DICT = {
    'host': '%s'
}
CANONICAL_HEADERS = '\n'.join(['%s:%s' % (k, v) for k, v in sorted(CANONICAL_HEADERS_DICT.items())])
SIGNED_HEADERS = ';'.join(CANONICAL_HEADERS_DICT.keys())
REQUEST_PAYLOAD = ''

156
FS_ID_RE = re.compile('^(?P<fs_id>fs-[0-9a-f]+)$')
157
158
EFS_FQDN_RE = re.compile(r'^((?P<az>[a-z0-9-]+)\.)?(?P<fs_id>fs-[0-9a-f]+)\.efs\.'
                         r'(?P<region>[a-z0-9-]+)\.(?P<dns_name_suffix>[a-z0-9.]+)$')
159
AP_ID_RE = re.compile('^fsap-[0-9a-f]{17}$')
Max Beckett's avatar
Max Beckett committed
160

161
162
CREDENTIALS_KEYS = ['AccessKeyId', 'SecretAccessKey', 'Token']
ECS_URI_ENV = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'
163
ECS_TASK_METADATA_API = 'http://169.254.170.2'
164
165
WEB_IDENTITY_ROLE_ARN_ENV = 'AWS_ROLE_ARN'
WEB_IDENTITY_TOKEN_FILE_ENV = 'AWS_WEB_IDENTITY_TOKEN_FILE'
166
STS_ENDPOINT_URL_FORMAT = 'https://sts.{}.amazonaws.com/'
167
INSTANCE_METADATA_TOKEN_URL = 'http://169.254.169.254/latest/api/token'
Max Beckett's avatar
Max Beckett committed
168
INSTANCE_METADATA_SERVICE_URL = 'http://169.254.169.254/latest/dynamic/instance-identity/document/'
169
170
INSTANCE_IAM_URL = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/'
SECURITY_CREDS_ECS_URI_HELP_URL = 'https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html'
171
SECURITY_CREDS_WEBIDENTITY_HELP_URL = 'https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html'
172
SECURITY_CREDS_IAM_ROLE_HELP_URL = 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html'
Max Beckett's avatar
Max Beckett committed
173
174

DEFAULT_STUNNEL_VERIFY_LEVEL = 2
Ian Patel's avatar
Ian Patel committed
175
DEFAULT_STUNNEL_CAFILE = '/etc/amazon/efs/efs-utils.crt'
Max Beckett's avatar
Max Beckett committed
176

177
178
179
NOT_BEFORE_MINS = 15
NOT_AFTER_HOURS = 3

Max Beckett's avatar
Max Beckett committed
180
EFS_ONLY_OPTIONS = [
181
    'accesspoint',
182
    'awscredsuri',
183
    'awsprofile',
184
    'az',
185
    'cafile',
186
187
188
189
    'iam',
    'netns',
    'noocsp',
    'ocsp',
Max Beckett's avatar
Max Beckett committed
190
191
    'tls',
    'tlsport',
192
    'verify'
Max Beckett's avatar
Max Beckett committed
193
194
]

195
UNSUPPORTED_OPTIONS = [
196
    'capath'
197
198
]

Max Beckett's avatar
Max Beckett committed
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
STUNNEL_GLOBAL_CONFIG = {
    'fips': 'no',
    'foreground': 'yes',
    'socket': [
        'l:SO_REUSEADDR=yes',
        'a:SO_BINDTODEVICE=lo',
    ],
}

STUNNEL_EFS_CONFIG = {
    'client': 'yes',
    'accept': '127.0.0.1:%s',
    'connect': '%s:2049',
    'sslVersion': 'TLSv1.2',
    'renegotiation': 'no',
    'TIMEOUTbusy': '20',
215
    'TIMEOUTclose': '0',
Max Beckett's avatar
Max Beckett committed
216
    'TIMEOUTidle': '70',
Ian Patel's avatar
Ian Patel committed
217
    'delay': 'yes',
Max Beckett's avatar
Max Beckett committed
218
219
220
}

WATCHDOG_SERVICE = 'amazon-efs-mount-watchdog'
221
222
# MacOS instances use plist files. This files needs to be loaded on launchctl (init system of MacOS)
WATCHDOG_SERVICE_PLIST_PATH = '/Library/LaunchAgents/amazon-efs-mount-watchdog.plist'
Max Beckett's avatar
Max Beckett committed
223
SYSTEM_RELEASE_PATH = '/etc/system-release'
224
OS_RELEASE_PATH = '/etc/os-release'
Max Beckett's avatar
Max Beckett committed
225
RHEL8_RELEASE_NAME = 'Red Hat Enterprise Linux release 8'
Yuan Gao's avatar
Yuan Gao committed
226
CENTOS8_RELEASE_NAME = 'CentOS Linux release 8'
227
FEDORA_RELEASE_NAME = 'Fedora release'
228
229
OPEN_SUSE_LEAP_RELEASE_NAME = 'openSUSE Leap'
SUSE_RELEASE_NAME = 'SUSE Linux Enterprise Server'
230
MACOS_BIG_SUR_RELEASE = 'macOS-11'
231
232

SKIP_NO_LIBWRAP_RELEASES = [RHEL8_RELEASE_NAME, CENTOS8_RELEASE_NAME, FEDORA_RELEASE_NAME, OPEN_SUSE_LEAP_RELEASE_NAME,
233
234
235
236
237
238
239
240
                            SUSE_RELEASE_NAME, MACOS_BIG_SUR_RELEASE]

# MacOS does not support the property of Socket SO_BINDTODEVICE in stunnel configuration
SKIP_NO_SO_BINDTODEVICE_RELEASES = [MACOS_BIG_SUR_RELEASE]

MAC_OS_PLATFORM_LIST = ['darwin']
# MacOS Versions : Big Sur - 20.*, Catalina - 19.*, Mojave - 18.*. Catalina and Mojave are not supported for now
MAC_OS_SUPPORTED_VERSION_LIST = ['20']
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272


def errcheck(ret, func, args):
    from ctypes import get_errno
    if ret == -1:
        e = get_errno()
        raise OSError(e, os.strerror(e))


def setns(fd, nstype):
    from ctypes import CDLL
    libc = CDLL('libc.so.6', use_errno=True)
    libc.setns.errcheck = errcheck
    if hasattr(fd, 'fileno'):
        fd = fd.fileno()
    return libc.setns(fd, nstype)


class NetNS(object):
    # Open sockets from given network namespace: stackoverflow.com/questions/28846059
    def __init__(self, nspath):
        self.original_nspath = '/proc/%d/ns/net' % os.getpid()
        self.target_nspath = nspath

    def __enter__(self):
        self.original_namespace = open(self.original_nspath)
        with open(self.target_nspath) as fd:
            setns(fd, CLONE_NEWNET)

    def __exit__(self, *args):
        setns(self.original_namespace, CLONE_NEWNET)
        self.original_namespace.close()
Max Beckett's avatar
Max Beckett committed
273
274
275
276
277
278
279
280


def fatal_error(user_message, log_message=None, exit_code=1):
    if log_message is None:
        log_message = user_message

    sys.stderr.write('%s\n' % user_message)
    logging.error(log_message)
281
    publish_cloudwatch_log(CLOUDWATCHLOG_AGENT, 'Mount failed, %s' % log_message)
Max Beckett's avatar
Max Beckett committed
282
283
284
    sys.exit(exit_code)


285
def get_target_region(config):
Max Beckett's avatar
Max Beckett committed
286
    def _fatal_error(message):
287
288
289
290
291
292
293
294
295
296
297
298
        fatal_error('Error retrieving region. Please set the "region" parameter in the efs-utils configuration file.', message)

    metadata_exception = 'Unknown error'
    try:
        return config.get(CONFIG_SECTION, 'region')
    except NoOptionError:
        pass

    try:
        return get_region_from_instance_metadata()
    except Exception as e:
        metadata_exception = e
Yuan Gao's avatar
Yuan Gao committed
299
300
        logging.warning('Region not found in config file and metadata service call failed, falling back '
                        'to legacy "dns_name_format" check')
Max Beckett's avatar
Max Beckett committed
301

302
303
304
305
306
307
    try:
        region = get_region_from_legacy_dns_format(config)
        sys.stdout.write('Warning: region obtained from "dns_name_format" field. Please set the "region" '
                         'parameter in the efs-utils configuration file.')
        return region
    except Exception:
Yuan Gao's avatar
Yuan Gao committed
308
        logging.warning('Legacy check for region in "dns_name_format" failed')
309
310
311
312
313

    _fatal_error(metadata_exception)


def get_region_from_instance_metadata():
314
    instance_identity = get_instance_identity_info_from_instance_metadata('region')
315

316
317
    if not instance_identity:
        raise Exception("Cannot retrieve region from instance_metadata")
318
319
320
321
322

    return instance_identity


def get_instance_identity_info_from_instance_metadata(property):
323
324
325
326
327
328
329
330
331
332
333
    ec2_metadata_unsuccessful_resp = 'Unsuccessful retrieval of EC2 metadata at %s.' % INSTANCE_METADATA_SERVICE_URL
    ec2_metadata_url_error_msg = 'Unable to reach %s to retrieve EC2 instance metadata.' % INSTANCE_METADATA_SERVICE_URL
    instance_identity = url_request_helper(INSTANCE_METADATA_SERVICE_URL, ec2_metadata_unsuccessful_resp,
                                           ec2_metadata_url_error_msg, retry_with_new_header_token=True)
    if instance_identity:
        try:
            return instance_identity[property]
        except KeyError as e:
            logging.warning('%s not present in %s: %s' % (property, instance_identity, e))
        except TypeError as e:
            logging.warning('response %s is not a json object: %s' % (instance_identity, e))
334

335
    return None
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350


def get_region_from_legacy_dns_format(config):
    """
    For backwards compatibility check dns_name_format to obtain the target region. This functionality
    should only be used if region is not present in the config file and metadata calls fail.
    """
    dns_name_format = config.get(CONFIG_SECTION, 'dns_name_format')
    if '{region}' not in dns_name_format:
        split_dns_name_format = dns_name_format.split('.')
        if '{dns_name_suffix}' in dns_name_format:
            return split_dns_name_format[-2]
        elif 'amazonaws.com' in dns_name_format:
            return split_dns_name_format[-3]
    raise Exception('Region not found in dns_name_format')
Max Beckett's avatar
Max Beckett committed
351
352


353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def get_aws_ec2_metadata_token():
    try:
        opener = build_opener(HTTPHandler)
        request = Request(INSTANCE_METADATA_TOKEN_URL)
        request.add_header('X-aws-ec2-metadata-token-ttl-seconds', 21600)
        request.get_method = lambda: 'PUT'
        res = opener.open(request)
        return res.read()
    except NameError:
        headers = {'X-aws-ec2-metadata-token-ttl-seconds': 21600}
        req = Request(INSTANCE_METADATA_TOKEN_URL, headers=headers, method='PUT')
        res = urlopen(req)
        return res.read()


368
def get_aws_security_credentials(use_iam, region, awsprofile=None, aws_creds_uri=None):
369
370
371
372
373
    """
    Lookup AWS security credentials (access key ID and secret access key). Adapted credentials provider chain from:
    https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html and
    https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html
    """
374
375
376
    if not use_iam:
        return None, None

377
378
379
380
381
382
    # attempt to lookup AWS security credentials through the credentials URI the ECS agent generated
    if aws_creds_uri:
        return get_aws_security_credentials_from_ecs(aws_creds_uri, True)

    # attempt to lookup AWS security credentials in AWS credentials file (~/.aws/credentials)
    # and configs file (~/.aws/config) with given awsprofile
383
    if awsprofile:
384
385
386
387
388
389
390
391
        return get_aws_security_credentials_from_awsprofile(awsprofile, True)

    # attempt to lookup AWS security credentials through AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variable
    if ECS_URI_ENV in os.environ:
        credentials, credentials_source = get_aws_security_credentials_from_ecs(os.environ[ECS_URI_ENV], False)
        if credentials and credentials_source:
            return credentials, credentials_source

392
393
394
395
396
397
    # attempt to lookup AWS security credentials through AssumeRoleWithWebIdentity
    # (e.g. for IAM Role for Service Accounts (IRSA) approach on EKS)
    if WEB_IDENTITY_ROLE_ARN_ENV in os.environ and WEB_IDENTITY_TOKEN_FILE_ENV in os.environ:
        credentials, credentials_source = get_aws_security_credentials_from_webidentity(
            os.environ[WEB_IDENTITY_ROLE_ARN_ENV],
            os.environ[WEB_IDENTITY_TOKEN_FILE_ENV],
398
            region,
399
400
401
402
403
            False
        )
        if credentials and credentials_source:
            return credentials, credentials_source

404
405
406
407
408
409
410
    # attempt to lookup AWS security credentials with IAM role name attached to instance
    # through IAM role name security credentials lookup uri
    iam_role_name = get_iam_role_name()
    if iam_role_name:
        credentials, credentials_source = get_aws_security_credentials_from_instance_metadata(iam_role_name)
        if credentials and credentials_source:
            return credentials, credentials_source
411

412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
    error_msg = 'AWS Access Key ID and Secret Access Key are not found in AWS credentials file (%s), config file (%s), ' \
                'from ECS credentials relative uri, or from the instance security credentials service' % \
                (AWS_CREDENTIALS_FILE, AWS_CONFIG_FILE)
    fatal_error(error_msg, error_msg)


def get_aws_security_credentials_from_awsprofile(awsprofile, is_fatal=False):
    for file_path in [AWS_CREDENTIALS_FILE, AWS_CONFIG_FILE]:
        if os.path.exists(file_path):
            credentials = credentials_file_helper(file_path, awsprofile)
            if credentials['AccessKeyId']:
                return credentials, os.path.basename(file_path) + ':' + awsprofile

    # Fail if credentials cannot be fetched from the given awsprofile
    if is_fatal:
427
        log_message = 'AWS security credentials not found in %s or %s under named profile [%s]' % \
428
                    (AWS_CREDENTIALS_FILE, AWS_CONFIG_FILE, awsprofile)
429
        fatal_error(log_message)
430
431
    else:
        return None, None
432

433

434
435
436
437
438
439
def get_aws_security_credentials_from_ecs(aws_creds_uri, is_fatal=False):
    ecs_uri = ECS_TASK_METADATA_API + aws_creds_uri
    ecs_unsuccessful_resp = 'Unsuccessful retrieval of AWS security credentials at %s.' % ecs_uri
    ecs_url_error_msg = 'Unable to reach %s to retrieve AWS security credentials. See %s for more info.' \
                        % (ecs_uri, SECURITY_CREDS_ECS_URI_HELP_URL)
    ecs_security_dict = url_request_helper(ecs_uri, ecs_unsuccessful_resp, ecs_url_error_msg)
440

441
442
    if ecs_security_dict and all(k in ecs_security_dict for k in CREDENTIALS_KEYS):
        return ecs_security_dict, 'ecs:' + aws_creds_uri
443

444
445
446
447
448
449
450
    # Fail if credentials cannot be fetched from the given aws_creds_uri
    if is_fatal:
        fatal_error(ecs_unsuccessful_resp, ecs_unsuccessful_resp)
    else:
        return None, None


451
def get_aws_security_credentials_from_webidentity(role_arn, token_file, region, is_fatal=False):
452
453
454
455
456
457
458
459
460
461
    try:
        with open(token_file, 'r') as f:
            token = f.read()
    except Exception as e:
        if is_fatal:
            unsuccessful_resp = 'Error reading token file %s: %s' % (token_file, e)
            fatal_error(unsuccessful_resp, unsuccessful_resp)
        else:
            return None, None

462
    STS_ENDPOINT_URL = STS_ENDPOINT_URL_FORMAT.format(region)
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
    webidentity_url = STS_ENDPOINT_URL + '?' + urlencode({
        'Version': '2011-06-15',
        'Action': 'AssumeRoleWithWebIdentity',
        'RoleArn': role_arn,
        'RoleSessionName': 'efs-mount-helper',
        'WebIdentityToken': token
    })

    unsuccessful_resp = 'Unsuccessful retrieval of AWS security credentials at %s.' % STS_ENDPOINT_URL
    url_error_msg = 'Unable to reach %s to retrieve AWS security credentials. See %s for more info.' % \
                    (STS_ENDPOINT_URL, SECURITY_CREDS_WEBIDENTITY_HELP_URL)
    resp = url_request_helper(webidentity_url, unsuccessful_resp, url_error_msg, headers={'Accept': 'application/json'})

    if resp:
        creds = resp \
                .get('AssumeRoleWithWebIdentityResponse', {}) \
                .get('AssumeRoleWithWebIdentityResult', {}) \
                .get('Credentials', {})
        if all(k in creds for k in ['AccessKeyId', 'SecretAccessKey', 'SessionToken']):
            return {
                'AccessKeyId': creds['AccessKeyId'],
                'SecretAccessKey': creds['SecretAccessKey'],
                'Token': creds['SessionToken']
            }, 'webidentity:' + ','.join([role_arn, token_file])

    # Fail if credentials cannot be fetched from the given aws_creds_uri
    if is_fatal:
        fatal_error(unsuccessful_resp, unsuccessful_resp)
    else:
        return None, None


495
def get_aws_security_credentials_from_instance_metadata(iam_role_name):
Yuan Gao's avatar
Yuan Gao committed
496
    security_creds_lookup_url = INSTANCE_IAM_URL + iam_role_name
497
    unsuccessful_resp = 'Unsuccessful retrieval of AWS security credentials at %s.' % security_creds_lookup_url
Yuan Gao's avatar
Yuan Gao committed
498
499
    url_error_msg = 'Unable to reach %s to retrieve AWS security credentials. See %s for more info.' % \
                    (security_creds_lookup_url, SECURITY_CREDS_IAM_ROLE_HELP_URL)
500
501
    iam_security_dict = url_request_helper(security_creds_lookup_url, unsuccessful_resp,
                                           url_error_msg, retry_with_new_header_token=True)
502
503
504
505
506
507
508
509

    if iam_security_dict and all(k in iam_security_dict for k in CREDENTIALS_KEYS):
        return iam_security_dict, 'metadata:'
    else:
        return None, None


def get_iam_role_name():
510
    iam_role_unsuccessful_resp = 'Unsuccessful retrieval of IAM role name at %s.' % INSTANCE_IAM_URL
Yuan Gao's avatar
Yuan Gao committed
511
512
    iam_role_url_error_msg = 'Unable to reach %s to retrieve IAM role name. See %s for more info.' % \
                             (INSTANCE_IAM_URL, SECURITY_CREDS_IAM_ROLE_HELP_URL)
513
514
    iam_role_name = url_request_helper(INSTANCE_IAM_URL, iam_role_unsuccessful_resp,
                                       iam_role_url_error_msg, retry_with_new_header_token=True)
515
    return iam_role_name
516
517


518
def credentials_file_helper(file_path, awsprofile):
519
520
521
522
    aws_credentials_configs = read_config(file_path)
    credentials = {'AccessKeyId': None, 'SecretAccessKey': None, 'Token': None}

    try:
523
524
525
        access_key = aws_credentials_configs.get(awsprofile, 'aws_access_key_id')
        secret_key = aws_credentials_configs.get(awsprofile, 'aws_secret_access_key')
        session_token = aws_credentials_configs.get(awsprofile, 'aws_session_token')
526
527
528
529
530
531

        credentials['AccessKeyId'] = access_key
        credentials['SecretAccessKey'] = secret_key
        credentials['Token'] = session_token
    except NoOptionError as e:
        if 'aws_access_key_id' in str(e) or 'aws_secret_access_key' in str(e):
532
533
            logging.debug('aws_access_key_id or aws_secret_access_key not found in %s under named profile [%s]', file_path,
                          awsprofile)
534
535
        if 'aws_session_token' in str(e):
            logging.debug('aws_session_token not found in %s', file_path)
536
537
            credentials['AccessKeyId'] = aws_credentials_configs.get(awsprofile, 'aws_access_key_id')
            credentials['SecretAccessKey'] = aws_credentials_configs.get(awsprofile, 'aws_secret_access_key')
538
    except NoSectionError:
539
        logging.debug('No [%s] section found in config file %s', awsprofile, file_path)
540
541
542
543

    return credentials


544
545
546
547
548
def get_aws_profile(options, use_iam):
    awsprofile = options.get('awsprofile')
    if not awsprofile and use_iam:
        for file_path in [AWS_CREDENTIALS_FILE, AWS_CONFIG_FILE]:
            aws_credentials_configs = read_config(file_path)
549
550
551
552
553
554
555
            # check if aws access key id is found under [default] section in current file and return 'default' if so
            try:
                access_key = aws_credentials_configs.get('default', 'aws_access_key_id')
                if access_key is not None:
                    return 'default'
            except (NoSectionError, NoOptionError):
                continue
556
557

    return awsprofile
558
559


560
def url_request_helper(url, unsuccessful_resp, url_error_msg, headers={}, retry_with_new_header_token=False):
561
    try:
562
563
564
565
        req = Request(url)
        for k, v in headers.items():
            req.add_header(k, v)
        request_resp = urlopen(req, timeout=1)
566

567
568
569
570
571
572
573
574
575
576
        return get_resp_obj(request_resp, url, unsuccessful_resp)
    except HTTPError as e:
        # For instance enable with IMDSv2, Unauthorized 401 error will be thrown,
        # to retrieve metadata, the header should embeded with metadata token
        if e.code == 401 and retry_with_new_header_token:
            token = get_aws_ec2_metadata_token()
            req.add_header('X-aws-ec2-metadata-token', token)
            request_resp = urlopen(req, timeout=1)
            return get_resp_obj(request_resp, url, unsuccessful_resp)
        err_msg = 'Unable to reach the url at %s: status=%d, reason is %s' % (url, e.code, e.reason)
577
    except URLError as e:
578
579
580
581
582
583
584
585
586
587
        err_msg = 'Unable to reach the url at %s, reason is %s' % (url, e.reason)

    if err_msg:
        logging.debug('%s %s', url_error_msg, err_msg)
    return None


def get_resp_obj(request_resp, url, unsuccessful_resp):
    if request_resp.getcode() != 200:
        logging.debug(unsuccessful_resp + ' %s: ResponseCode=%d', url, request_resp.getcode())
588
589
        return None

590
591
592
593
594
595
596
597
598
599
600
601
602
    resp_body = request_resp.read()
    resp_body_type = type(resp_body)
    try:
        if resp_body_type is str:
            resp_dict = json.loads(resp_body)
        else:
            resp_dict = json.loads(resp_body.decode(request_resp.headers.get_content_charset() or 'us-ascii'))

        return resp_dict
    except ValueError as e:
        logging.info('ValueError parsing "%s" into json: %s. Returning response body.' % (str(resp_body), e))
        return resp_body if resp_body_type is str else resp_body.decode('utf-8')

603

Max Beckett's avatar
Max Beckett committed
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
def parse_options(options):
    opts = {}
    for o in options.split(','):
        if '=' in o:
            k, v = o.split('=')
            opts[k] = v
        else:
            opts[o] = None
    return opts


def get_tls_port_range(config):
    lower_bound = config.getint(CONFIG_SECTION, 'port_range_lower_bound')
    upper_bound = config.getint(CONFIG_SECTION, 'port_range_upper_bound')

    if lower_bound >= upper_bound:
        fatal_error('Configuration option "port_range_upper_bound" defined as %d '
                    'must be strictly greater than "port_range_lower_bound" defined as %d.'
                    % (upper_bound, lower_bound))

    return lower_bound, upper_bound


627
628
def choose_tls_port(config, options):
    if 'tlsport' in options:
629
        ports_to_try = [int(options['tlsport'])]
630
631
    else:
        lower_bound, upper_bound = get_tls_port_range(config)
Max Beckett's avatar
Max Beckett committed
632

633
        tls_ports = list(range(lower_bound, upper_bound))
Max Beckett's avatar
Max Beckett committed
634

635
636
        # Choose a random midpoint, and then try ports in-order from there
        mid = random.randrange(len(tls_ports))
Max Beckett's avatar
Max Beckett committed
637

638
639
        ports_to_try = tls_ports[mid:] + tls_ports[:mid]
        assert len(tls_ports) == len(ports_to_try)
Max Beckett's avatar
Max Beckett committed
640

641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
    if 'netns' not in options:
        tls_port = find_tls_port_in_range(ports_to_try)
    else:
        with NetNS(nspath=options['netns']):
            tls_port = find_tls_port_in_range(ports_to_try)

    if tls_port:
        return tls_port

    if 'tlsport' in options:
        fatal_error('Specified port [%s] is unavailable. Try selecting a different port.' % options['tlsport'])
    else:
        fatal_error('Failed to locate an available port in the range [%d, %d], try specifying a different port range in %s'
                    % (lower_bound, upper_bound, CONFIG_FILE))


def find_tls_port_in_range(ports_to_try):
658
    sock = socket.socket()
Max Beckett's avatar
Max Beckett committed
659
660
    for tls_port in ports_to_try:
        try:
661
            logging.info("binding %s", tls_port)
Max Beckett's avatar
Max Beckett committed
662
663
664
            sock.bind(('localhost', tls_port))
            sock.close()
            return tls_port
665
666
        except socket.error as e:
            logging.info(e)
Max Beckett's avatar
Max Beckett committed
667
            continue
668
    sock.close()
669
    return None
670
671
672


def is_ocsp_enabled(config, options):
673
    if 'ocsp' in options:
674
675
676
677
678
        return True
    elif 'noocsp' in options:
        return False
    else:
        return config.getboolean(CONFIG_SECTION, 'stunnel_check_cert_validity')
Max Beckett's avatar
Max Beckett committed
679
680
681


def get_mount_specific_filename(fs_id, mountpoint, tls_port):
Ian Patel's avatar
Ian Patel committed
682
    return '%s.%s.%d' % (fs_id, os.path.abspath(mountpoint).replace(os.sep, '.').lstrip('.'), tls_port)
Max Beckett's avatar
Max Beckett committed
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700


def serialize_stunnel_config(config, header=None):
    lines = []

    if header:
        lines.append('[%s]' % header)

    for k, v in config.items():
        if type(v) is list:
            for item in v:
                lines.append('%s = %s' % (k, item))
        else:
            lines.append('%s = %s' % (k, v))

    return lines


701
def add_stunnel_ca_options(efs_config, config, options, region):
702
703
704
705
    if 'cafile' in options:
        stunnel_cafile = options['cafile']
    else:
        try:
706
707
708
            config_section = get_config_section(config, region)
            stunnel_cafile = config.get(config_section, 'stunnel_cafile')
            logging.debug("Using stunnel_cafile %s in config section [%s]", stunnel_cafile, config_section)
709
710
711
712
        except NoOptionError:
            logging.debug('No CA file configured, using default CA file %s', DEFAULT_STUNNEL_CAFILE)
            stunnel_cafile = DEFAULT_STUNNEL_CAFILE

Ian Patel's avatar
Ian Patel committed
713
    if not os.path.exists(stunnel_cafile):
714
715
716
        fatal_error('Failed to find certificate authority file for verification',
                    'Failed to find CAfile "%s"' % stunnel_cafile)

Ian Patel's avatar
Ian Patel committed
717
718
719
    efs_config['CAfile'] = stunnel_cafile


720
721
722
723
724
725
726
727
728
def get_config_section(config, region):
    region_specific_config_section = '%s.%s' % (CONFIG_SECTION, region)
    if config.has_section(region_specific_config_section):
        config_section = region_specific_config_section
    else:
        config_section = CONFIG_SECTION
    return config_section


Ian Patel's avatar
Ian Patel committed
729
730
731
732
733
734
735
736
def is_stunnel_option_supported(stunnel_output, stunnel_option_name):
    supported = False
    for line in stunnel_output:
        if line.startswith(stunnel_option_name):
            supported = True
            break

    if not supported:
Yuan Gao's avatar
Yuan Gao committed
737
        logging.warning('stunnel does not support "%s"', stunnel_option_name)
Ian Patel's avatar
Ian Patel committed
738
739
740
741

    return supported


742
743
744
def get_version_specific_stunnel_options():
    stunnel_command = [_stunnel_bin(), '-help']
    proc = subprocess.Popen(stunnel_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
Ian Patel's avatar
Ian Patel committed
745
746
    proc.wait()
    _, err = proc.communicate()
Max Beckett's avatar
Max Beckett committed
747

Ian Patel's avatar
Ian Patel committed
748
    stunnel_output = err.splitlines()
Max Beckett's avatar
Max Beckett committed
749

750
751
    check_host_supported = is_stunnel_option_supported(stunnel_output, b'checkHost')
    ocsp_aia_supported = is_stunnel_option_supported(stunnel_output, b'OCSPaia')
Ian Patel's avatar
Ian Patel committed
752

753
    return check_host_supported, ocsp_aia_supported
Ian Patel's avatar
Ian Patel committed
754
755


756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
def _stunnel_bin():
    return find_command_path('stunnel',
                             'Please install it following the instructions at '
                             'https://docs.aws.amazon.com/efs/latest/ug/using-amazon-efs-utils.html#upgrading-stunnel')


def find_command_path(command, install_method):
    try:
        env_path = '/sbin:/usr/sbin:/usr/local/sbin:/root/bin:/usr/local/bin:/usr/bin:/bin'
        os.putenv('PATH', env_path)
        path = subprocess.check_output(['which', command])
    except subprocess.CalledProcessError as e:
        fatal_error('Failed to locate %s in %s - %s' % (command, env_path, install_method), e)
    return path.strip().decode()


Max Beckett's avatar
Max Beckett committed
772
def get_system_release_version():
773
774
775
776
    # MacOS does not maintain paths /etc/os-release and /etc/sys-release
    if check_if_platform_is_mac():
        return platform.platform()

Max Beckett's avatar
Max Beckett committed
777
778
    try:
        with open(SYSTEM_RELEASE_PATH) as f:
779
            return f.read().strip()
Max Beckett's avatar
Max Beckett committed
780
781
782
    except IOError:
        logging.debug('Unable to read %s', SYSTEM_RELEASE_PATH)

783
784
785
786
787
788
789
790
    try:
        with open(OS_RELEASE_PATH) as f:
            for line in f:
                if 'PRETTY_NAME' in line:
                    return line.split('=')[1].strip()
    except IOError:
        logging.debug('Unable to read %s', OS_RELEASE_PATH)

791
    return DEFAULT_UNKNOWN_VALUE
Max Beckett's avatar
Max Beckett committed
792
793


794
def write_stunnel_config_file(config, state_file_dir, fs_id, mountpoint, tls_port, dns_name, verify_level, ocsp_enabled,
795
                              options, region, log_dir=LOG_DIR, cert_details=None):
Max Beckett's avatar
Max Beckett committed
796
797
798
799
800
801
802
    """
    Serializes stunnel configuration to a file. Unfortunately this does not conform to Python's config file format, so we have to
    hand-serialize it.
    """

    mount_filename = get_mount_specific_filename(fs_id, mountpoint, tls_port)

803
    system_release_version = get_system_release_version()
Max Beckett's avatar
Max Beckett committed
804
    global_config = dict(STUNNEL_GLOBAL_CONFIG)
805
806
807
    if any(release in system_release_version for release in SKIP_NO_SO_BINDTODEVICE_RELEASES):
        global_config['socket'].remove('a:SO_BINDTODEVICE=lo')

Max Beckett's avatar
Max Beckett committed
808
809
    if config.getboolean(CONFIG_SECTION, 'stunnel_debug_enabled'):
        global_config['debug'] = 'debug'
810
811
812
813
814

        if config.has_option(CONFIG_SECTION, 'stunnel_logs_file'):
            global_config['output'] = config.get(CONFIG_SECTION, 'stunnel_logs_file').replace('{fs_id}', fs_id)
        else:
            global_config['output'] = os.path.join(log_dir, '%s.stunnel.log' % mount_filename)
Max Beckett's avatar
Max Beckett committed
815
816
817
818

    efs_config = dict(STUNNEL_EFS_CONFIG)
    efs_config['accept'] = efs_config['accept'] % tls_port
    efs_config['connect'] = efs_config['connect'] % dns_name
Ian Patel's avatar
Ian Patel committed
819
820
    efs_config['verify'] = verify_level
    if verify_level > 0:
821
        add_stunnel_ca_options(efs_config, config, options, region)
822
823
824
825

    if cert_details:
        efs_config['cert'] = cert_details['certificate']
        efs_config['key'] = cert_details['privateKey']
Ian Patel's avatar
Ian Patel committed
826

827
    check_host_supported, ocsp_aia_supported = get_version_specific_stunnel_options()
Ian Patel's avatar
Ian Patel committed
828
829
830
831

    tls_controls_message = 'WARNING: Your client lacks sufficient controls to properly enforce TLS. Please upgrade stunnel, ' \
        'or disable "%%s" in %s.\nSee %s for more detail.' % (CONFIG_FILE,
                                                              'https://docs.aws.amazon.com/console/efs/troubleshooting-tls')
Max Beckett's avatar
Max Beckett committed
832

833
834
    if config.getboolean(CONFIG_SECTION, 'stunnel_check_cert_hostname'):
        if check_host_supported:
835
836
837
838
            # Stunnel checkHost option checks if the specified DNS host name or wildcard matches any of the provider in peer
            # certificate's CN fields, after introducing the AZ field in dns name, the host name in the stunnel config file
            # is not valid, remove the az info there
            efs_config['checkHost'] = dns_name[dns_name.index(fs_id):]
839
840
        else:
            fatal_error(tls_controls_message % 'stunnel_check_cert_hostname')
Max Beckett's avatar
Max Beckett committed
841

842
843
    # Only use the config setting if the override is not set
    if ocsp_enabled:
844
845
846
847
        if ocsp_aia_supported:
            efs_config['OCSPaia'] = 'yes'
        else:
            fatal_error(tls_controls_message % 'stunnel_check_cert_validity')
Max Beckett's avatar
Max Beckett committed
848

Yuan Gao's avatar
Yuan Gao committed
849
    if not any(release in system_release_version for release in SKIP_NO_LIBWRAP_RELEASES):
Max Beckett's avatar
Max Beckett committed
850
851
        efs_config['libwrap'] = 'no'

Max Beckett's avatar
Max Beckett committed
852
853
854
855
856
857
858
859
860
861
862
    stunnel_config = '\n'.join(serialize_stunnel_config(global_config) + serialize_stunnel_config(efs_config, 'efs'))
    logging.debug('Writing stunnel configuration:\n%s', stunnel_config)

    stunnel_config_file = os.path.join(state_file_dir, 'stunnel-config.%s' % mount_filename)

    with open(stunnel_config_file, 'w') as f:
        f.write(stunnel_config)

    return stunnel_config_file


863
def write_tls_tunnel_state_file(fs_id, mountpoint, tls_port, tunnel_pid, command, files, state_file_dir, cert_details=None):
Max Beckett's avatar
Max Beckett committed
864
865
866
867
868
869
870
871
872
873
874
875
    """
    Return the name of the temporary file containing TLS tunnel state, prefixed with a '~'. This file needs to be renamed to a
    non-temporary version following a successful mount.
    """
    state_file = '~' + get_mount_specific_filename(fs_id, mountpoint, tls_port)

    state = {
        'pid': tunnel_pid,
        'cmd': command,
        'files': files,
    }

876
877
878
    if cert_details:
        state.update(cert_details)

Max Beckett's avatar
Max Beckett committed
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
    with open(os.path.join(state_file_dir, state_file), 'w') as f:
        json.dump(state, f)

    return state_file


def test_tunnel_process(tunnel_proc, fs_id):
    tunnel_proc.poll()
    if tunnel_proc.returncode is not None:
        out, err = tunnel_proc.communicate()
        fatal_error('Failed to initialize TLS tunnel for %s' % fs_id,
                    'Failed to start TLS tunnel (errno=%d). stdout="%s" stderr="%s"'
                    % (tunnel_proc.returncode, out.strip(), err.strip()))


def poll_tunnel_process(tunnel_proc, fs_id, mount_completed):
    """
    poll the tunnel process health every .5s during the mount attempt to fail fast if the tunnel dies - since this is not called
    from the main thread, if the tunnel fails, exit uncleanly with os._exit
    """
    while not mount_completed.is_set():
        try:
            test_tunnel_process(tunnel_proc, fs_id)
        except SystemExit as e:
            os._exit(e.code)
        mount_completed.wait(.5)


def get_init_system(comm_file='/proc/1/comm'):
908
    init_system = DEFAULT_UNKNOWN_VALUE
909
910
911
912
913
914
915
916
    if not check_if_platform_is_mac():
        try:
            with open(comm_file) as f:
                init_system = f.read().strip()
        except IOError:
            logging.warning('Unable to read %s', comm_file)
    else:
        init_system = 'launchd'
Max Beckett's avatar
Max Beckett committed
917
918
919
920
921

    logging.debug('Identified init system: %s', init_system)
    return init_system


Yuan Gao's avatar
Yuan Gao committed
922
def check_network_target(fs_id):
Max Beckett's avatar
Max Beckett committed
923
    with open(os.devnull, 'w') as devnull:
924
925
926
927
        if not check_if_platform_is_mac():
            rc = subprocess.call(['systemctl', 'status', 'network.target'], stdout=devnull, stderr=devnull, close_fds=True)
        else:
            rc = subprocess.call(['sudo', 'ifconfig', 'en0'], stdout=devnull, stderr=devnull, close_fds=True)
Max Beckett's avatar
Max Beckett committed
928
929
930
931
932
933

    if rc != 0:
        fatal_error('Failed to mount %s because the network was not yet available, add "_netdev" to your mount options' % fs_id,
                    exit_code=0)


Yuan Gao's avatar
Yuan Gao committed
934
935
936
937
938
939
940
941
def check_network_status(fs_id, init_system):
    if init_system != 'systemd':
        logging.debug('Not testing network on non-systemd init systems')
        return

    check_network_target(fs_id)


Max Beckett's avatar
Max Beckett committed
942
943
def start_watchdog(init_system):
    if init_system == 'init':
944
945
        proc = subprocess.Popen(
                ['/sbin/status', WATCHDOG_SERVICE], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
Ian Patel's avatar
Ian Patel committed
946
        status, _ = proc.communicate()
947
        if 'stop' in str(status):
Max Beckett's avatar
Max Beckett committed
948
            with open(os.devnull, 'w') as devnull:
949
                subprocess.Popen(['/sbin/start', WATCHDOG_SERVICE], stdout=devnull, stderr=devnull, close_fds=True)
950
        elif 'start' in str(status):
Max Beckett's avatar
Max Beckett committed
951
952
953
            logging.debug('%s is already running', WATCHDOG_SERVICE)

    elif init_system == 'systemd':
954
        rc = subprocess.call(['systemctl', 'is-active', '--quiet', WATCHDOG_SERVICE], close_fds=True)
Max Beckett's avatar
Max Beckett committed
955
956
        if rc != 0:
            with open(os.devnull, 'w') as devnull:
957
                subprocess.Popen(['systemctl', 'start', WATCHDOG_SERVICE], stdout=devnull, stderr=devnull, close_fds=True)
Max Beckett's avatar
Max Beckett committed
958
959
960
        else:
            logging.debug('%s is already running', WATCHDOG_SERVICE)

961
962
963
964
965
966
967
968
969
970
971
972
    elif init_system == 'launchd':
        rc = subprocess.Popen(['sudo', 'launchctl', 'list', WATCHDOG_SERVICE], stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE, close_fds=True)
        if rc != 0:
            if not os.path.exists(WATCHDOG_SERVICE_PLIST_PATH):
                fatal_error('Watchdog plist file missing. Copy the watchdog plist file in directory /Library/LaunchAgents')
            with open(os.devnull, 'w') as devnull:
                subprocess.Popen(['sudo', 'launchctl', 'load', WATCHDOG_SERVICE_PLIST_PATH], stdout=devnull,
                                 stderr=devnull, close_fds=True)
        else:
            logging.debug('%s is already running', WATCHDOG_SERVICE)

Max Beckett's avatar
Max Beckett committed
973
974
975
976
977
978
    else:
        error_message = 'Could not start %s, unrecognized init system "%s"' % (WATCHDOG_SERVICE, init_system)
        sys.stderr.write('%s\n' % error_message)
        logging.warning(error_message)


979
def create_required_directory(config, directory):
980
981
982
983
984
985
    mode = 0o750
    try:
        mode_str = config.get(CONFIG_SECTION, 'state_file_dir_mode')
        try:
            mode = int(mode_str, 8)
        except ValueError:
Yuan Gao's avatar
Yuan Gao committed
986
987
            logging.warning('Bad state_file_dir_mode "%s" in config file "%s"', mode_str, CONFIG_FILE)
    except NoOptionError:
988
989
        pass

990
    try:
991
        os.makedirs(directory, mode)
992
    except OSError as e:
993
        if errno.EEXIST != e.errno or not os.path.isdir(directory):
994
            raise
995
996


Max Beckett's avatar
Max Beckett committed
997
@contextmanager
998
def bootstrap_tls(config, init_system, dns_name, fs_id, mountpoint, options, state_file_dir=STATE_FILE_DIR):
999
1000
1001
    tls_port = choose_tls_port(config, options)
    # override the tlsport option so that we can later override the port the NFS client uses to connect to stunnel.
    # if the user has specified tlsport=X at the command line this will just re-set tlsport to X.
Max Beckett's avatar
Max Beckett committed
1002
    options['tlsport'] = tls_port
1003

1004
    use_iam = 'iam' in options
1005
    ap_id = options.get('accesspoint')
1006
    cert_details = {}
1007
1008
    security_credentials = None
    client_info = get_client_info(config)
1009
    region = get_target_region(config)
1010

1011
    if use_iam:
1012
1013
1014
1015
1016
1017
        aws_creds_uri = options.get('awscredsuri')
        if aws_creds_uri:
            kwargs = {'aws_creds_uri': aws_creds_uri}
        else:
            kwargs = {'awsprofile': get_aws_profile(options, use_iam)}

1018
        security_credentials, credentials_source = get_aws_security_credentials(use_iam, region, **kwargs)
1019
1020
1021

        if credentials_source:
            cert_details['awsCredentialsMethod'] = credentials_source
1022

1023
1024
1025
1026
1027
1028
1029
    if ap_id:
        cert_details['accessPoint'] = ap_id

    # additional symbol appended to avoid naming collisions
    cert_details['mountStateDir'] = get_mount_specific_filename(fs_id, mountpoint, tls_port) + '+'
    # common name for certificate signing request is max 64 characters
    cert_details['commonName'] = socket.gethostname()[0:64]
1030
    region = get_target_region(config)
1031
    cert_details['region'] = region
1032
1033
1034
1035
1036
1037
1038
    cert_details['certificateCreationTime'] = create_certificate(config, cert_details['mountStateDir'],
                                                                 cert_details['commonName'], cert_details['region'], fs_id,
                                                                 security_credentials, ap_id, client_info,
                                                                 base_path=state_file_dir)
    cert_details['certificate'] = os.path.join(state_file_dir, cert_details['mountStateDir'], 'certificate.pem')
    cert_details['privateKey'] = get_private_key_path()
    cert_details['fsId'] = fs_id
1039
1040
1041
1042
1043
1044

    start_watchdog(init_system)

    if not os.path.exists(state_file_dir):
        create_required_directory(config, state_file_dir)

Ian Patel's avatar
Ian Patel committed
1045
    verify_level = int(options.get('verify', DEFAULT_STUNNEL_VERIFY_LEVEL))
1046
    ocsp_enabled = is_ocsp_enabled(config, options)
Max Beckett's avatar
Max Beckett committed
1047

1048
    stunnel_config_file = write_stunnel_config_file(config, state_file_dir, fs_id, mountpoint, tls_port, dns_name, verify_level,
1049
                                                    ocsp_enabled, options, region, cert_details=cert_details)
1050
    tunnel_args = [_stunnel_bin(), stunnel_config_file]
1051
1052
    if 'netns' in options:
        tunnel_args = ['nsenter', '--net=' + options['netns']] + tunnel_args
Max Beckett's avatar
Max Beckett committed
1053
1054
1055

    # launch the tunnel in a process group so if it has any child processes, they can be killed easily by the mount watchdog
    logging.info('Starting TLS tunnel: "%s"', ' '.join(tunnel_args))
1056
    tunnel_proc = subprocess.Popen(
1057
        tunnel_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid, close_fds=True)
Max Beckett's avatar
Max Beckett committed
1058
1059
1060
    logging.info('Started TLS tunnel, pid: %d', tunnel_proc.pid)

    temp_tls_state_file = write_tls_tunnel_state_file(fs_id, mountpoint, tls_port, tunnel_proc.pid, tunnel_args,
1061
                                                      [stunnel_config_file], state_file_dir, cert_details=cert_details)
Max Beckett's avatar
Max Beckett committed
1062

1063
1064
1065
1066
1067
1068
    if 'netns' not in options:
        test_tlsport(options['tlsport'])
    else:
        with NetNS(nspath=options['netns']):
            test_tlsport(options['tlsport'])

Max Beckett's avatar
Max Beckett committed
1069
1070
1071
1072
1073
1074
    try:
        yield tunnel_proc
    finally:
        os.rename(os.path.join(state_file_dir, temp_tls_state_file), os.path.join(state_file_dir, temp_tls_state_file[1:]))


1075
1076
1077
1078
1079
1080
1081
1082
def test_tlsport(tlsport):
    retry_times = 5
    while not verify_tlsport_can_be_connected(tlsport) and retry_times > 0:
        logging.debug('The tlsport %s cannot be connected yet, sleep 0.05s, %s retry time(s) left', tlsport, retry_times)
        time.sleep(0.05)
        retry_times -= 1


1083
1084
1085
1086
1087
1088
1089
1090
def check_if_nfsvers_is_compatible_with_macos(options):
    # MacOS does not support NFSv4.1
    if ('nfsvers' in options and options['nfsvers'] == '4.1') \
            or ('vers' in options and options['vers'] == '4.1')\
            or ('minorversion' in options and options['minorversion'] == 1):
        fatal_error('NFSv4.1 is not supported on MacOS, please switch to NFSv4.0')


Max Beckett's avatar
Max Beckett committed
1091
def get_nfs_mount_options(options):
1092
    # If you change these options, update the man page as well at man/mount.efs.8
Max Beckett's avatar
Max Beckett committed
1093
    if 'nfsvers' not in options and 'vers' not in options:
1094
1095
1096
1097
1098
        options['nfsvers'] = '4.1' if not check_if_platform_is_mac() else '4.0'

    if check_if_platform_is_mac():
        check_if_nfsvers_is_compatible_with_macos(options)

Max Beckett's avatar
Max Beckett committed
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
    if 'rsize' not in options:
        options['rsize'] = '1048576'
    if 'wsize' not in options:
        options['wsize'] = '1048576'
    if 'soft' not in options and 'hard' not in options:
        options['hard'] = None
    if 'timeo' not in options:
        options['timeo'] = '600'
    if 'retrans' not in options:
        options['retrans'] = '2'
1109
1110
    if 'noresvport' not in options:
        options['noresvport'] = None
Max Beckett's avatar
Max Beckett committed
1111

1112
1113
1114
1115
    # Set mountport to 2049 for MacOS
    if check_if_platform_is_mac():
        options['mountport'] = '2049'

Max Beckett's avatar
Max Beckett committed
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
    if 'tls' in options:
        options['port'] = options['tlsport']

    def to_nfs_option(k, v):
        if v is None:
            return k
        return '%s=%s' % (str(k), str(v))

    nfs_options = [to_nfs_option(k, v) for k, v in options.items() if k not in EFS_ONLY_OPTIONS]

    return ','.join(nfs_options)


def mount_nfs(dns_name, path, mountpoint, options):
1130

Max Beckett's avatar
Max Beckett committed
1131
1132
1133
1134
1135
    if 'tls' in options:
        mount_path = '127.0.0.1:%s' % path
    else:
        mount_path = '%s:%s' % (dns_name, path)

1136
1137
1138
1139
    if not check_if_platform_is_mac():
        command = ['/sbin/mount.nfs4', mount_path, mountpoint, '-o', get_nfs_mount_options(options)]
    else:
        command = ['/sbin/mount_nfs', '-o', get_nfs_mount_options(options), mount_path, mountpoint]
Max Beckett's avatar
Max Beckett committed
1140

1141
1142
1143
    if 'netns' in options:
        command = ['nsenter', '--net=' + options['netns']] + command

Max Beckett's avatar
Max Beckett committed
1144
1145
    logging.info('Executing: "%s"', ' '.join(command))

1146
    proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
Ian Patel's avatar
Ian Patel committed
1147
    out, err = proc.communicate()
Max Beckett's avatar
Max Beckett committed
1148

Ian Patel's avatar
Ian Patel committed
1149
    if proc.returncode == 0:
1150
1151
1152
        message = 'Successfully mounted %s at %s' % (dns_name, mountpoint)
        logging.info(message)
        publish_cloudwatch_log(CLOUDWATCHLOG_AGENT, message)
Max Beckett's avatar
Max Beckett committed
1153
    else:
Ian Patel's avatar
Ian Patel committed
1154
1155
        message = 'Failed to mount %s at %s: returncode=%d, stderr="%s"' % (dns_name, mountpoint, proc.returncode, err.strip())
        fatal_error(err.strip(), message, proc.returncode)
Max Beckett's avatar
Max Beckett committed
1156
1157


Ian Patel's avatar
Ian Patel committed
1158
1159
1160
1161
1162
1163
1164
def usage(out, exit_code=1):
    out.write('Usage: mount.efs [--version] [-h|--help] <fsname> <mountpoint> [-o <options>]\n')
    sys.exit(exit_code)


def parse_arguments_early_exit(args=None):
    """Parse arguments, checking for early exit conditions only"""
Max Beckett's avatar
Max Beckett committed
1165
1166
1167
1168
1169
1170
1171
    if args is None:
        args = sys.argv

    if '-h' in args[1:] or '--help' in args[1:]:
        usage(out=sys.stdout, exit_code=0)

    if '--version' in args[1:]:
Ian Patel's avatar
Ian Patel committed
1172
        sys.stdout.write('%s Version: %s\n' % (args[0], VERSION))
Max Beckett's avatar
Max Beckett committed
1173
1174
        sys.exit(0)

Ian Patel's avatar
Ian Patel committed
1175
1176
1177
1178
1179
1180

def parse_arguments(config, args=None):
    """Parse arguments, return (fsid, path, mountpoint, options)"""
    if args is None:
        args = sys.argv

Max Beckett's avatar
Max Beckett committed
1181
1182
1183
1184
    fsname = None
    mountpoint = None
    options = {}

1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
    if not check_if_platform_is_mac():
        if len(args) > 1:
            fsname = args[1]
        if len(args) > 2:
            mountpoint = args[2]
        if len(args) > 4 and '-o' in args[:-1]:
            options_index = args.index('-o') + 1
            options = parse_options(args[options_index])
    else:
        if len(args) > 1:
            fsname = args[-2]
        if len(args) > 2:
            mountpoint = args[-1]
        if len(args) > 4 and '-o' in args[:-2]:
            for arg in args[1:-2]:
                if arg != '-o':
                    options.update(parse_options(arg))
Max Beckett's avatar
Max Beckett committed
1202
1203

    if not fsname or not mountpoint:
Ian Patel's avatar
Ian Patel committed
1204
        usage(out=sys.stderr)
Max Beckett's avatar
Max Beckett committed
1205

1206
1207
1208
    # We treat az as an option when customer is using dns name of az mount target to mount,
    # even if they don't provide az with option, we update the options with that info
    fs_id, path, az = match_device(config, fsname, options)
Max Beckett's avatar
Max Beckett committed
1209

1210
    return fs_id, path, mountpoint, add_field_in_options(options, 'az', az)
Max Beckett's avatar
Max Beckett committed
1211
1212


1213
1214
1215
1216
1217
1218
1219
1220
def get_client_info(config):
    client_info = {}

    # source key/value pair in config file
    if config.has_option(CLIENT_INFO_SECTION, 'source'):
        client_source = config.get(CLIENT_INFO_SECTION, 'source')
        if 0 < len(client_source) <= CLIENT_SOURCE_STR_LEN_LIMIT:
            client_info['source'] = client_source
1221
1222
1223
1224
    if not client_info.get('source'):
        client_info['source'] = DEFAULT_UNKNOWN_VALUE

    client_info['efs_utils_version'] = VERSION
1225
1226
1227
1228

    return client_info


1229
1230
def create_certificate(config, mount_name, common_name, region, fs_id, security_credentials, ap_id, client_info,
                       base_path=STATE_FILE_DIR):
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
    current_time = get_utc_now()
    tls_paths = tls_paths_dictionary(mount_name, base_path)

    certificate_config = os.path.join(tls_paths['mount_dir'], 'config.conf')
    certificate_signing_request = os.path.join(tls_paths['mount_dir'], 'request.csr')
    certificate = os.path.join(tls_paths['mount_dir'], 'certificate.pem')

    ca_dirs_check(config, tls_paths['database_dir'], tls_paths['certs_dir'])
    ca_supporting_files_check(tls_paths['index'], tls_paths['index_attr'], tls_paths['serial'], tls_paths['rand'])

    private_key = check_and_create_private_key(base_path)

1243
    if security_credentials:
1244
1245
1246
        public_key = os.path.join(tls_paths['mount_dir'], 'publicKey.pem')
        create_public_key(private_key, public_key)

1247
    create_ca_conf(certificate_config, common_name, tls_paths['mount_dir'], private_key, current_time, region, fs_id,
1248
                   security_credentials, ap_id, client_info)
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268