Your IP : 3.144.72.189
Current Path : /bin/ |
|
Current File : //bin/install_compass |
#!/usr/bin/env python
"""This script downloads the latest version of Compass and installs it on
non-windows platforms."""
import itertools
import logging
import os
import os.path as path
import platform
import re
import shutil
import subprocess
import sys
import tempfile
try:
from urllib2 import urlopen
except ImportError:
from urllib.request import urlopen
try:
from shlex import quote as cmd_quote
except ImportError:
from pipes import quote as cmd_quote
DOWNLOAD_BASE_URL = 'https://compass.mongodb.com/api/v2/download/latest/compass/stable'
# Some systems may have multiple package managers installed - Debian systems may
# have yum installed, and newer RHEL/Fedora systems may have both dnf and yum.
# These must be checked in the given order for consistent results.
SUPPORTED_LINUX_PKG_MANAGERS = ['apt', 'dnf', 'yum']
LINUX_PKG_MANAGER_ARGS = {
'apt': ['install', '--yes'],
'yum': ['install', '--assumeyes'],
'dnf': ['install', '--assumeyes'],
}
PKG_MANAGER_SUFFIXES = {
'apt': '.deb',
'dnf': '.rpm',
'yum': '.rpm',
'dmg': '.dmg',
}
SUPPORTED_OSX_VERSION = 10.10
SUPPORTED_RHEL_VERSION = 7
SUPPORTED_DEBIAN_VERSION = 9
SUPPORTED_DEBIAN_RELEASES = [
'stretch',
'buster',
'bullseye',
'bookworm',
]
class LogLevelFilter(logging.Filter):
"""Filters log messages by level"""
def __init__(self, levelno, *args, **kwargs):
self._levelno = levelno
# In Python 2.6, logging.Filter is not a new-style class, so super()
# will not work with it. We need to call the base class __init__()
# directly here so it works with all versions of Python.
logging.Filter.__init__(self, *args, **kwargs)
def filter(self, record):
return record.levelno <= self._levelno
class FileDownloadError(Exception):
"""Exception raised on file download errors"""
class InstallationError(Exception):
"""Exception raised on installation errors"""
def run_command(*args, **kwargs):
"""Runs a command, with optional stdout and stderr."""
with open(os.devnull, 'w') as fnull:
stdout = kwargs.pop('stdout', fnull)
stderr = kwargs.pop('stderr', fnull)
process = subprocess.Popen([cmd_quote(arg) for arg in args],
stdout=stdout, stderr=stderr, **kwargs)
stdout, stderr = process.communicate()
if stdout is not None:
stdout = stdout.decode('utf-8')
process.stdout.close()
if stderr is not None:
stderr = stderr.decode('utf-8')
process.stderr.close()
return (process.wait(), stdout, stderr)
def get_package_manager():
"""Determine the package manager for this Linux distro."""
if sys.platform == 'darwin':
return 'dmg'
else:
for package_manager in SUPPORTED_LINUX_PKG_MANAGERS:
try:
retcode, _, _ = run_command(package_manager, '--help')
if retcode == 0:
return package_manager
except OSError:
pass
return None
def download_progress(count, block_size, total_size):
"""Log the download progress."""
percent = min(int(count * block_size * 100 / total_size), 100)
if sys.stdout.isatty():
sys.stdout.write("\x1b[KDownloading ... " + str(percent) + "%\r")
sys.stdout.flush()
else:
# If we're not writing to a terminal, don't bother being too fancy. This
# prevents us from filling up logs with many progress lines.
if count == 1:
LOG.info('Downloading ...')
if count % 10 == 0:
LOG.info(" %dK (%d%%)", block_size / 1024, percent)
if (count * block_size) >= total_size:
LOG.info("Complete!")
def download_file(url, filename, block_size=16 * 1024):
"""Download the package from link, logging progress. Returns the filename."""
try:
response = urlopen(url)
total_size = int(response.info()["Content-Length"])
with open(filename, 'wb') as out_file:
for count in itertools.count(1):
chunk = response.read(block_size)
if not chunk:
LOG.info('\nDownload complete!')
break
download_progress(count, block_size, total_size)
out_file.write(chunk)
response.close()
except IOError:
raise FileDownloadError('Unable to download MongoDB Compass, please check your internet' \
' connection. If the issue persists go to' \
' https://www.mongodb.com/download-center?jmp=hero#compass' \
' to download the compass installer for your platform.')
else:
retcode, stdout, stderr = run_command('file', filename,
shell=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if retcode != 0:
raise Exception(stderr)
if 'ASCII Text' in stdout:
raise FileDownloadError('Downloaded file is text, but binary was expected.')
def install_mac(dmg):
"""Use CLI tools to mount the dmg and extract all .apps into Applications."""
tmp = tempfile.mkdtemp()
retcode, _, stderr = run_command(
'hdiutil', 'attach', '-nobrowse', '-noautoopen', '-mountpoint', tmp, dmg,
stderr=subprocess.PIPE)
if retcode != 0:
raise InstallationError('Problem running hdiutil: ' + stderr)
try:
apps = [f for f in os.listdir(tmp) if f.endswith('.app')]
for app in apps:
if path.isdir('/Applications/' + app):
LOG.info('Old version found removing...')
shutil.rmtree('/Applications/' + app)
LOG.info('Copying %s to /Applications', app)
shutil.copytree(path.join(tmp, app), '/Applications/' + app)
# We don't really care about what errors come up here. Just log the failure
# and use the finally to make sure we always unmount the dmg.
except IOError as error:
raise InstallationError('Error copying MongoDB Compass to /Applications: ' + error.message)
finally:
run_command('hdiutil', 'detach', tmp)
if path.isdir('/Applications/MongoDB Compass.app'):
retcode, _, stderr = run_command('open', '/Applications/MongoDB Compass.app',
stderr=subprocess.PIPE)
if retcode == 0:
return
if path.isdir('/Applications/MongoDB Compass Community.app'):
retcode, _, stderr = run_command('open', '/Applications/MongoDB Compass Community.app',
stderr=subprocess.PIPE)
if retcode == 0:
return
raise InstallationError('Problem opening application: ' + stderr)
def install_linux(package_manager, pkg_file):
"""Use the package manager indicated by pkg_format to install pkg_file."""
install = tuple([package_manager] + LINUX_PKG_MANAGER_ARGS[package_manager] + [pkg_file])
retcode, _, stderr = run_command(*install, stderr=subprocess.PIPE)
if retcode != 0:
raise InstallationError('Problem running package manager: ' + stderr)
def get_osx_version():
"""Gets the OSX version."""
mac_version, _, _ = platform.mac_ver()
# Get Mac OSX Major.Minor verson as a float
return float('.'.join(mac_version.split('.')[:2]))
def get_rhel_version():
"""Gets the RHEL compatibility version."""
rhel_version_re = re.compile(r' release (\d+\.\d+)', re.MULTILINE)
with open('/etc/redhat-release', 'r') as release_file:
match = rhel_version_re.search(release_file.read())
if not match:
return None
return float('.'.join(match.group(1).split('.')))
def get_debian_release():
"""Gets the Debian release."""
# Matches from the beginning of the line to a slash or the end of the line
debian_version_re = re.compile(r'^([^/]+)(?:/|$)', re.MULTILINE)
with open('/etc/debian_version', 'r') as version_file:
match = debian_version_re.search(version_file.read())
if not match:
return None
return match.group(1)
def is_supported_linux(): # pylint: disable=too-many-branches,too-many-return-statements
"""Checks whether the Linux version is supported by the Compass package."""
# This isn't intended to be an exhaustive check of all compatible
# distributions. We're only checking the ones that are likely to be
# used when installing either a .deb or a .rpm package.
if os.path.exists('/etc/redhat-release'):
rhel_version = get_rhel_version()
if rhel_version is not None and rhel_version > SUPPORTED_RHEL_VERSION:
return True
if os.path.exists('/etc/debian_version'):
debian_release = get_debian_release()
try:
# The debian version string is sometimes just a float. This appears
# to be only for older releases, but we'll try to be safe here.
if float(debian_release) >= SUPPORTED_DEBIAN_VERSION:
return True
return False
except ValueError:
if debian_release is not None and debian_release in SUPPORTED_DEBIAN_RELEASES:
return True
return False
# Matches the key and value separated by an =, with the value optionally
# being enclosed in quotes.
os_release_re = re.compile(r'^([A-Z_]+)="?([^"]+)(?:"|$)')
if os.path.exists('/etc/os-release'):
os_id = None
version_id = None
with open('/etc/os-release', 'r') as os_release:
for line in os_release.readlines():
match = os_release_re.search(line)
if match:
if match.group(1) == "ID":
os_id = match.group(2)
if match.group(1) == "VERSION_ID":
version_id = match.group(2)
# The only two known incompatible distributions that have a
# /etc/os-release provided by systemd are these two. It's best here to
# assume the rest will succeed and take the ones that fail individually.
if os_id == "amzn":
if version_id.startswith("2018") or version_id.startswith("2016"):
return False
if os_id == "opensuse":
return False
return True
return False
def prerequisites_satisfied(package_manager):
"""Check that prerequisites are satisfied before downloading and installing."""
if package_manager is None:
LOG.error('You are using an unsupported platform.\n' \
'Please visit: https://compass.mongodb.com/community-supported-platforms' \
' to view available community supported packages.')
return False
if platform.machine() != 'x86_64':
LOG.error('Sorry, MongoDB Compass is only supported on 64-bit Intel platforms.' \
' If you believe you\'re seeing this message in error please open a' \
' ticket on the SERVER project at https://jira.mongodb.org/')
return False
if sys.platform == 'osx' and get_osx_version() < SUPPORTED_OSX_VERSION:
LOG.error('You are using an unsupported Mac OSX version. Please upgrade\n' \
'to at least version 10.10 (Yosemite) to install Compass.')
return False
if sys.platform.startswith('linux'):
if os.getuid() != 0:
LOG.error('You must run this script as root.')
return False
if not is_supported_linux():
LOG.error('You are using an unsupported Linux distribution.\n' \
'Please visit: https://compass.mongodb.com/community-supported-platforms' \
' to view available community supported packages.')
return False
return True
def get_package_type(package_manager):
"""Gets the package type from the detected package manager."""
if sys.platform.startswith('linux'):
package_type = 'linux'
if package_manager == 'apt':
package_type += '_deb'
elif package_manager in ('yum', 'dnf'):
package_type += '_rpm'
elif sys.platform == 'darwin':
package_type = 'osx'
return package_type
def download_and_install_compass():
"""Download and install compass for this platform."""
package_manager = get_package_manager()
if not prerequisites_satisfied(package_manager):
return sys.exit(1)
_, filename = tempfile.mkstemp(suffix=PKG_MANAGER_SUFFIXES[package_manager])
is_successful = True
url = DOWNLOAD_BASE_URL + "/" + get_package_type(package_manager)
LOG.info('Retrieving the Compass package from %s', url)
try:
download_file(url=url, filename=filename)
except FileDownloadError as error:
LOG.exception(error)
is_successful = False
except Exception as error: # pylint: disable=broad-except
LOG.error('Unexpected error when downloading: %s', error.message)
is_successful = False
else:
LOG.info('Installing the package...')
try:
if package_manager == 'dmg':
install_mac(filename)
else:
install_linux(package_manager, filename)
except InstallationError as error:
LOG.exception(error)
is_successful = False
except Exception as error: # pylint: disable=broad-except
LOG.error('Unexpected error when installing: %s', error.message)
is_successful = False
LOG.info('Cleaning up...')
os.remove(filename)
LOG.info('Done!')
return is_successful
# Set up logging to stdout and stderr for the console only
LOG = logging.getLogger()
LOG.setLevel(logging.DEBUG)
LOG_STDOUT_HANDLER = logging.StreamHandler(sys.stdout)
LOG_STDOUT_HANDLER.setLevel(logging.INFO)
LOG_STDOUT_HANDLER.addFilter(LogLevelFilter(logging.INFO))
LOG.addHandler(LOG_STDOUT_HANDLER)
LOG_STDERR_HANDLER = logging.StreamHandler(sys.stderr)
LOG_STDERR_HANDLER.setLevel(logging.ERROR)
LOG_STDERR_HANDLER.addFilter(LogLevelFilter(logging.ERROR))
LOG.addHandler(LOG_STDERR_HANDLER)
if __name__ == '__main__':
# Set up a log file for both stdout and stderr, but only when this is being
# executed as a script. This facilitates testing without polluting the test
# environment.
LOG_FILE_NAME = '/tmp/install_compass.log'
LOG_FILE_HANDLER = logging.FileHandler(LOG_FILE_NAME)
LOG_FILE_HANDLER.setLevel(logging.DEBUG)
LOG_FILE_FORMATTER = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
LOG_FILE_HANDLER.setFormatter(LOG_FILE_FORMATTER)
LOG.addHandler(LOG_FILE_HANDLER)
SUCCESS = download_and_install_compass()
LOG.info('A log file for this installation can be found at %s', LOG_FILE_NAME)
if not SUCCESS:
LOG.error('Please see our documentation for how to download and install ' \
'Compass manually: https://docs.mongodb.com/compass/current/install/#download-and-install-compass')
sys.exit(1)