Commit d8c3a974 authored by Jigar Dedhia's avatar Jigar Dedhia
Browse files

Add support for EC2 Mac instances running macOS Big Sur

parent 929fedba
......@@ -145,6 +145,9 @@ jobs:
workflows:
workflow:
jobs:
- test:
name: python39
image: circleci/python:3.9.1
- test:
name: python38
image: circleci/python:3.8.1
......@@ -213,7 +216,4 @@ workflows:
image: opensuse/leap:15.2
- build-suse-rpm-package:
name: opensuse-leap-latest
image: opensuse/leap:latest
- build-suse-rpm-package:
name: opensuse-tumbleweed-latest
image: opensuse/tumbleweed:latest
\ No newline at end of file
image: opensuse/leap:latest
\ No newline at end of file
......@@ -29,6 +29,12 @@ The `efs-utils` package has been verified against the following Linux distributi
| SLES 12 | `rpm` | `systemd` |
| SLES 15 | `rpm` | `systemd` |
The `efs-utils` package has been verified against the following MacOS distributions:
| Distribution | `init` System |
| ------------ | ------------- |
| MacOS Big Sur | `launchd` |
## Prerequisites
* `nfs-utils` (RHEL/CentOS/Amazon Linux/Fedora) or `nfs-common` (Debian/Ubuntu)
......@@ -106,6 +112,31 @@ $ ./build-deb.sh
$ sudo apt-get -y install ./build/amazon-efs-utils*deb
```
### On MacOS Big Sur distribution
For EC2 Mac instances running macOS Big Sur, you can install amazon-efs-utils from the [homebrew-aws](https://github.com/aws/homebrew-aws) respository.
```
brew install amazon-efs-utils
```
This will install amazon-efs-utils on your EC2 Mac Instance running macOS Big Sur in the directory `/usr/local/Cellar/amazon-efs-utils`. At the end of the installation, it will print a set of commands that must be executed in order to start using efs-utils. The instructions that are printed after amazon-efs-utils and must be executed are:
```
Perform below actions to start using efs:
sudo mkdir -p /Library/Filesystems/efs.fs/Contents/Resources
sudo ln -s /usr/local/bin/mount.efs /Library/Filesystems/efs.fs/Contents/Resources/mount_efs
Perform below actions to stop using efs:
sudo rm /Library/Filesystems/efs.fs/Contents/Resources/mount_efs
To enable watchdog for using TLS mounts:
sudo cp /usr/local/Cellar/amazon-efs-utils/<version>/libexec/amazon-efs-mount-watchdog.plist /Library/LaunchAgents
sudo launchctl load /Library/LaunchAgents/amazon-efs-mount-watchdog.plist
To disable watchdog for using TLS mounts:
sudo launchctl unload /Library/LaunchAgents/amazon-efs-mount-watchdog.plist
```
#### Run tests
- [Set up a virtualenv](http://libzx.so/main/learning/2016/03/13/best-practice-for-virtualenv-and-git-repos.html) for efs-utils
......@@ -175,7 +206,7 @@ or refer to the [documentation](https://docs.aws.amazon.com/efs/latest/ug/using-
### amazon-efs-mount-watchdog
`efs-utils` contains a watchdog process to monitor the health of TLS mounts. This process is managed by either `upstart` or `systemd` depending on your Linux distribution, and is started automatically the first time an EFS file system is mounted over TLS.
`efs-utils` contains a watchdog process to monitor the health of TLS mounts. This process is managed by either `upstart` or `systemd` depending on your Linux distribution and `launchd` on Mac distribution, and is started automatically the first time an EFS file system is mounted over TLS.
## Upgrading stunnel for RHEL/CentOS
......@@ -193,6 +224,14 @@ sudo zypper refresh
sudo zypper install -y stunnel
```
## Upgrading stunnel for MacOS
The installation installs latest stunnel available in brew repository. You can also upgrade the version of stunnel on your instance using the command below:
```
brew upgrade stunnel
```
## Enable mount success/failure notification via CloudWatch log
`efs-utils` now support publishing mount success/failure logs to CloudWatch log. By default, this feature is disabled. There are three
steps you must follow to enable and use this feature:
......@@ -247,10 +286,20 @@ sudo python3 /tmp/get-pip.py
sudo pip3 install --target /usr/lib/python3/dist-packages botocore || sudo /usr/local/bin/pip3 install --target /usr/lib/python3/dist-packages botocore
```
#### To install botocore on MacOS
```bash
sudo pip3 install botocore
```
### Step 2. Enable CloudWatch log feature in efs-utils config file `/etc/amazon/efs/efs-utils.conf`
```bash
sudo sed -i -e '/\[cloudwatch-log\]/{N;s/# enabled = true/enabled = true/}' /etc/amazon/efs/efs-utils.conf
```
- For MacOS:
```bash
sudo sed -i -e '/\[cloudwatch-log\]/{N;s/# enabled = true/enabled = true/;}' /usr/local/Cellar/amazon-efs-utils/<version>/etc/amazon/efs/efs-utils.conf
```
You can also configure CloudWatch log group name and log retention days in the config file.
### Step 3. Attach the CloudWatch logs policy to the IAM role attached to instance.
......
<?xml version="1.0" encoding="UTF-8"?>
<!--
#
# 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.
#
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Unit</key>
<dict>
<key>Description</key>
<string>amazon-efs-mount-watchdog</string>
<key>Before</key>
<string>remote-fs-pre.target</string>
</dict>
<key>Label</key>
<string>amazon-efs-mount-watchdog</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/amazon-efs-mount-watchdog</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>PathState</key>
<dict>
<key>/usr/local/bin/amazon-efs-mount-watchdog</key>
<true/>
</dict>
<key>Crashed</key>
<true/>
</dict>
<key>StandardErrorPath</key>
<string>/var/log/amazon/efs/mount-watchdog.log</string>
<key>StandardOutPath</key>
<string>/var/log/amazon/efs/mount-watchdog.log</string>
<key>StartOnMount</key>
<true/>
</dict>
</plist>
\ No newline at end of file
......@@ -98,6 +98,9 @@ can be used to mount EFS\&.
Use the named profile used to lookup IAM credentials in the AWS CLI credentials file \
(~/.aws/credentials) or AWS CLI config file (~/.aws/config). If "awsprofile" is not \
specified, the "default" profile is used\&.
.TP
mountport
Use the port 2049 to bypass portmapper daemon on EC2 Mac instances running macOS Big Sur\&.
.if n \{\
.RE
.\}
......@@ -181,6 +184,8 @@ The man page for the Amazon EFS mount helper\&.
For more information on using the \fBamazon\-efs\-utils\fR package, see \
\fIhttps://docs\&.aws\&.amazon\&.com/efs/latest/ug/using\-amazon\-efs\-utils\&.html\fR \
in the Amazon EFS User Guide\&.
.sp
The paths on EC2 MacOS instances are relocated under /usr/local/Cellar/amazon-efs-utils/<version>/libexec directory.
.SH "SEE ALSO"
.sp
\fBnfs(8)\fR, \fBstunnel(8)\fR, \fBfstab(5)\fR
......
......@@ -37,6 +37,7 @@ import hmac
import json
import logging
import os
import platform
import pwd
import random
import re
......@@ -215,6 +216,8 @@ STUNNEL_EFS_CONFIG = {
}
WATCHDOG_SERVICE = 'amazon-efs-mount-watchdog'
# 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'
SYSTEM_RELEASE_PATH = '/etc/system-release'
OS_RELEASE_PATH = '/etc/os-release'
RHEL8_RELEASE_NAME = 'Red Hat Enterprise Linux release 8'
......@@ -222,9 +225,17 @@ CENTOS8_RELEASE_NAME = 'CentOS Linux release 8'
FEDORA_RELEASE_NAME = 'Fedora release'
OPEN_SUSE_LEAP_RELEASE_NAME = 'openSUSE Leap'
SUSE_RELEASE_NAME = 'SUSE Linux Enterprise Server'
MACOS_BIG_SUR_RELEASE = 'macOS-11'
SKIP_NO_LIBWRAP_RELEASES = [RHEL8_RELEASE_NAME, CENTOS8_RELEASE_NAME, FEDORA_RELEASE_NAME, OPEN_SUSE_LEAP_RELEASE_NAME,
SUSE_RELEASE_NAME]
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']
def errcheck(ret, func, args):
......@@ -744,6 +755,10 @@ def find_command_path(command, install_method):
def get_system_release_version():
# MacOS does not maintain paths /etc/os-release and /etc/sys-release
if check_if_platform_is_mac():
return platform.platform()
try:
with open(SYSTEM_RELEASE_PATH) as f:
return f.read().strip()
......@@ -770,7 +785,11 @@ def write_stunnel_config_file(config, state_file_dir, fs_id, mountpoint, tls_por
mount_filename = get_mount_specific_filename(fs_id, mountpoint, tls_port)
system_release_version = get_system_release_version()
global_config = dict(STUNNEL_GLOBAL_CONFIG)
if any(release in system_release_version for release in SKIP_NO_SO_BINDTODEVICE_RELEASES):
global_config['socket'].remove('a:SO_BINDTODEVICE=lo')
if config.getboolean(CONFIG_SECTION, 'stunnel_debug_enabled'):
global_config['debug'] = 'debug'
......@@ -809,7 +828,6 @@ def write_stunnel_config_file(config, state_file_dir, fs_id, mountpoint, tls_por
else:
fatal_error(tls_controls_message % 'stunnel_check_cert_validity')
system_release_version = get_system_release_version()
if not any(release in system_release_version for release in SKIP_NO_LIBWRAP_RELEASES):
efs_config['libwrap'] = 'no'
......@@ -870,11 +888,14 @@ def poll_tunnel_process(tunnel_proc, fs_id, mount_completed):
def get_init_system(comm_file='/proc/1/comm'):
init_system = DEFAULT_UNKNOWN_VALUE
try:
with open(comm_file) as f:
init_system = f.read().strip()
except IOError:
logging.warning('Unable to read %s', comm_file)
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'
logging.debug('Identified init system: %s', init_system)
return init_system
......@@ -882,7 +903,10 @@ def get_init_system(comm_file='/proc/1/comm'):
def check_network_target(fs_id):
with open(os.devnull, 'w') as devnull:
rc = subprocess.call(['systemctl', 'status', 'network.target'], stdout=devnull, stderr=devnull, close_fds=True)
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)
if rc != 0:
fatal_error('Failed to mount %s because the network was not yet available, add "_netdev" to your mount options' % fs_id,
......@@ -916,6 +940,18 @@ def start_watchdog(init_system):
else:
logging.debug('%s is already running', WATCHDOG_SERVICE)
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)
else:
error_message = 'Could not start %s, unrecognized init system "%s"' % (WATCHDOG_SERVICE, init_system)
sys.stderr.write('%s\n' % error_message)
......@@ -1024,10 +1060,22 @@ def test_tlsport(tlsport):
retry_times -= 1
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')
def get_nfs_mount_options(options):
# If you change these options, update the man page as well at man/mount.efs.8
if 'nfsvers' not in options and 'vers' not in options:
options['nfsvers'] = '4.1'
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)
if 'rsize' not in options:
options['rsize'] = '1048576'
if 'wsize' not in options:
......@@ -1041,6 +1089,10 @@ def get_nfs_mount_options(options):
if 'noresvport' not in options:
options['noresvport'] = None
# Set mountport to 2049 for MacOS
if check_if_platform_is_mac():
options['mountport'] = '2049'
if 'tls' in options:
options['port'] = options['tlsport']
......@@ -1061,7 +1113,10 @@ def mount_nfs(dns_name, path, mountpoint, options):
else:
mount_path = '%s:%s' % (dns_name, path)
command = ['/sbin/mount.nfs4', mount_path, mountpoint, '-o', get_nfs_mount_options(options)]
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]
if 'netns' in options:
command = ['nsenter', '--net=' + options['netns']] + command
......@@ -1107,13 +1162,23 @@ def parse_arguments(config, args=None):
mountpoint = None
options = {}
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])
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))
if not fsname or not mountpoint:
usage(out=sys.stderr)
......@@ -1804,6 +1869,14 @@ def get_cloudwatch_log_stream_name(fs_id=None):
return log_stream_name
def check_if_platform_is_mac():
return sys.platform in MAC_OS_PLATFORM_LIST
def check_if_mac_version_is_supported():
return any(release in platform.release() for release in MAC_OS_SUPPORTED_VERSION_LIST)
def check_if_cloudwatch_log_enabled(config):
if config.has_option(CLOUDWATCH_LOG_SECTION, 'enabled'):
return config.getboolean(CLOUDWATCH_LOG_SECTION, 'enabled')
......@@ -2046,6 +2119,9 @@ def main():
config = read_config()
bootstrap_logging(config)
if check_if_platform_is_mac() and not check_if_mac_version_is_supported():
fatal_error("We do not support EFS on MacOS " + platform.release())
fs_id, path, mountpoint, options = parse_arguments(config)
logging.info('version=%s options=%s', VERSION, options)
......
......@@ -397,10 +397,19 @@ def get_current_local_nfs_mounts(mount_file='/proc/mounts'):
appears in EFS watchdog state files.
"""
mounts = []
with open(mount_file) as f:
for mount in f:
mounts.append(Mount._make(mount.strip().split()))
if sys.platform != 'darwin':
with open(mount_file) as f:
for mount in f:
mounts.append(Mount._make(mount.strip().split()))
else:
process = subprocess.run(['mount', '-t', 'nfs'], check=True, stdout=subprocess.PIPE, universal_newlines=True)
stdout = process.stdout
if stdout:
output = stdout.split('\n')
for mount in output:
_mount = mount.split()
if len(_mount) >= 4:
mounts.append(Mount._make([_mount[0], _mount[2], _mount[3], '', 0, 0]))
mounts = [m for m in mounts if m.server.startswith('127.0.0.1') and 'nfs' in m.type]
......
......@@ -8,6 +8,18 @@
import mount_efs
import pytest
from mock import MagicMock, patch
def _mock_popen(mocker, returncode=0, stdout='stdout', stderr='stderr'):
popen_mock = MagicMock()
popen_mock.communicate.return_value = (stdout, stderr, )
popen_mock.returncode = returncode
return mocker.patch('subprocess.Popen', return_value=popen_mock)
def test_get_default_nfs_mount_options():
nfs_opts = mount_efs.get_nfs_mount_options({})
......@@ -76,3 +88,38 @@ def test_tlsport():
assert 'port=3030' in nfs_opts
assert 'tls' not in nfs_opts
def test_get_default_nfs_mount_options_macos(mocker):
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
nfs_opts = mount_efs.get_nfs_mount_options({})
assert 'nfsvers=4.0' in nfs_opts
assert 'rsize=1048576' in nfs_opts
assert 'wsize=1048576' in nfs_opts
assert 'hard' in nfs_opts
assert 'timeo=600' in nfs_opts
assert 'retrans=2' in nfs_opts
assert 'mountport=2049' in nfs_opts
def _test_unsupported_mount_options_macos(mocker, capsys, options={}):
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
_mock_popen(mocker, stdout='nfs')
with pytest.raises(SystemExit) as ex:
mount_efs.get_nfs_mount_options(options)
assert 0 != ex.value.code
out, err = capsys.readouterr()
assert 'NFSv4.1 is not supported on MacOS' in err
def test_unsupported_nfsvers_mount_options_macos(mocker, capsys):
_test_unsupported_mount_options_macos(mocker, capsys, {'nfsvers': '4.1'})
def test_unsupported_vers_mount_options_macos(mocker, capsys):
_test_unsupported_mount_options_macos(mocker, capsys, {'vers': '4.1'})
def test_unsupported_minorversion_mount_options_macos(mocker, capsys):
_test_unsupported_mount_options_macos(mocker, capsys, {'minorversion': 1})
......@@ -12,7 +12,7 @@ import pytest
from contextlib import contextmanager
from mock import patch
from mock import MagicMock, patch
from .. import utils
......@@ -194,3 +194,23 @@ def test_main_tls_mount_point_mounted_with_non_nfs(mocker):
mocker.patch('os.path.ismount', return_value=True)
mocker.patch('mount_efs.is_nfs_mount', return_value=False)
_test_main(mocker, tls=True, tlsport=TLS_PORT)
def _mock_popen(mocker, returncode=0, stdout='stdout', stderr='stderr'):
popen_mock = MagicMock()
popen_mock.communicate.return_value = (stdout, stderr, )
popen_mock.returncode = returncode
return mocker.patch('subprocess.Popen', return_value=popen_mock)
def test_main_unsupported_macos(mocker, capsys):
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
# Test for Catalina Client
mocker.patch('mount_efs.check_if_mac_version_is_supported', return_value=False)
expected_err = 'We do not support EFS on MacOS'
_test_main_assert_error(mocker, capsys, expected_err, root=True)
def test_main_supported_macos(mocker):
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
mocker.patch('mount_efs.check_if_mac_version_is_supported', return_value=True)
_test_main(mocker, tls=True, tlsport=TLS_PORT)
......@@ -34,6 +34,10 @@ NETNS_NSENTER_ARG_IDX = 0
NETNS_PATH_ARG_IDX = 1
NETNS_NFS_OFFSET = 2
# indices of different arguments to the NFS call for MACOS
NFS_MOUNT_PATH_IDX_MACOS = -2
NFS_MOUNT_POINT_IDX_MACOS = -1
NETNS = '/proc/1/net/ns'
......@@ -112,4 +116,34 @@ def test_mount_tls_mountpoint_mounted_with_nfs(mocker, capsys):
mount_efs.mount_tls(CONFIG, INIT_SYSTEM, DNS_NAME, PATH, FS_ID, MOUNT_POINT, options)
out, err = capsys.readouterr()
assert 'is already mounted' in out
utils.assert_not_called(bootstrap_tls_mock)
\ No newline at end of file
utils.assert_not_called(bootstrap_tls_mock)
def test_mount_nfs_macos(mocker):
mock = _mock_popen(mocker)
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
DEFAULT_OPTIONS['nfsvers'] = 4.0
mount_efs.mount_nfs(DNS_NAME, '/', '/mnt', DEFAULT_OPTIONS)
args, _ = mock.call_args
args = args[0]
assert '/sbin/mount_nfs' == args[NFS_BIN_ARG_IDX]
assert DNS_NAME in args[-2]
assert '/mnt' == args[-1]
def test_mount_nfs_tls_macos(mocker):
mock = _mock_popen(mocker)
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
DEFAULT_OPTIONS['nfsvers'] = 4.0
options = dict(DEFAULT_OPTIONS)
options['tls'] = None
mount_efs.mount_nfs(DNS_NAME, '/', '/mnt', options)
args, _ = mock.call_args
args = args[0]
assert DNS_NAME not in args[NFS_MOUNT_PATH_IDX_MACOS]
assert '127.0.0.1' in args[NFS_MOUNT_PATH_IDX_MACOS]
......@@ -95,3 +95,15 @@ def test_parse_arguments():
assert '/home' == path
assert '/dir' == mountpoint
assert {'foo': None, 'bar': 'baz', 'quux': None} == options
def test_parse_arguments_macos(mocker):
mocker.patch('mount_efs.check_if_platform_is_mac', return_value=True)
fsid, path, mountpoint, options = mount_efs.parse_arguments(None, ['mount', '-o', 'foo', '-o', 'bar=baz', '-o', 'quux',
'fs-deadbeef:/home', '/dir'])
assert 'fs-deadbeef' == fsid
assert '/home' == path
assert '/dir' == mountpoint
assert {'foo': None, 'bar': 'baz', 'quux': None} == options
......@@ -40,6 +40,20 @@ def test_systemd_system(mocker):
assert 'systemctl' in popen_mock.call_args[0][0]
assert 'start' in popen_mock.call_args[0][0]
def test_launchd_system(mocker):
process_mock = MagicMock()
process_mock.communicate.return_value = ('stop', '', )
process_mock.returncode = 0
popen_mock = mocker.patch('subprocess.Popen', return_value=process_mock)
mocker.patch('os.path.exists', return_value=True)
mount_efs.start_watchdog('launchd')
assert 2 == popen_mock.call_count
assert 'sudo' in popen_mock.call_args[0][0]
assert 'launchctl' in popen_mock.call_args[0][0]
assert 'load' in popen_mock.call_args[0][0]
def test_unknown_system(mocker):
popen_mock = mocker.patch('subprocess.Popen')
......
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