1334 lines
66 KiB
Python
Executable File
1334 lines
66 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved.
|
|
|
|
import argparse
|
|
import datetime
|
|
import fileinput
|
|
import getpass
|
|
import glob
|
|
import json
|
|
import os
|
|
import platform
|
|
import pprint
|
|
import math
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
|
|
try:
|
|
from pwd import getpwuid
|
|
except ImportError:
|
|
getpwuid = None
|
|
from collections import defaultdict, namedtuple
|
|
|
|
from malcolm_common import *
|
|
|
|
###################################################################################################
|
|
DOCKER_COMPOSE_INSTALL_VERSION="1.27.4"
|
|
|
|
DEB_GPG_KEY_FINGERPRINT = '0EBFCD88' # used to verify GPG key for Docker Debian repository
|
|
|
|
MAC_BREW_DOCKER_PACKAGE = 'docker-edge'
|
|
MAC_BREW_DOCKER_SETTINGS = '/Users/{}/Library/Group Containers/group.com.docker/settings.json'
|
|
|
|
###################################################################################################
|
|
ScriptName = os.path.basename(__file__)
|
|
origPath = os.getcwd()
|
|
|
|
###################################################################################################
|
|
args = None
|
|
|
|
###################################################################################################
|
|
# get interactive user response to Y/N question
|
|
def InstallerYesOrNo(question, default=None, forceInteraction=False):
|
|
global args
|
|
return YesOrNo(question, default=default, forceInteraction=forceInteraction, acceptDefault=args.acceptDefaults)
|
|
|
|
###################################################################################################
|
|
# get interactive user response
|
|
def InstallerAskForString(question, default=None, forceInteraction=False):
|
|
global args
|
|
return AskForString(question, default=default, forceInteraction=forceInteraction, acceptDefault=args.acceptDefaults)
|
|
|
|
def TrueOrFalseQuote(expression):
|
|
return "'{}'".format('true' if expression else 'false')
|
|
|
|
###################################################################################################
|
|
class Installer(object):
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def __init__(self, debug=False, configOnly=False):
|
|
self.debug = debug
|
|
self.configOnly = configOnly
|
|
|
|
self.platform = platform.system()
|
|
self.scriptUser = getpass.getuser()
|
|
|
|
self.checkPackageCmds = []
|
|
self.installPackageCmds = []
|
|
self.requiredPackages = []
|
|
|
|
self.pipCmd = 'pip3'
|
|
if not Which(self.pipCmd, debug=self.debug): self.pipCmd = 'pip'
|
|
|
|
self.tempDirName = tempfile.mkdtemp()
|
|
|
|
self.totalMemoryGigs = 0.0
|
|
self.totalCores = 0
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def __del__(self):
|
|
shutil.rmtree(self.tempDirName, ignore_errors=True)
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def run_process(self, command, stdout=True, stderr=True, stdin=None, privileged=False, retry=0, retrySleepSec=5):
|
|
|
|
# if privileged, put the sudo command at the beginning of the command
|
|
if privileged and (len(self.sudoCmd) > 0):
|
|
command = self.sudoCmd + command
|
|
|
|
return run_process(command, stdout=stdout, stderr=stderr, stdin=stdin, retry=retry, retrySleepSec=retrySleepSec, debug=self.debug)
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def package_is_installed(self, package):
|
|
result = False
|
|
for cmd in self.checkPackageCmds:
|
|
ecode, out = self.run_process(cmd + [package])
|
|
if (ecode == 0):
|
|
result = True
|
|
break
|
|
return result
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_package(self, packages):
|
|
result = False
|
|
pkgs = []
|
|
|
|
for package in packages:
|
|
if not self.package_is_installed(package):
|
|
pkgs.append(package)
|
|
|
|
if (len(pkgs) > 0):
|
|
for cmd in self.installPackageCmds:
|
|
ecode, out = self.run_process(cmd + pkgs, privileged=True)
|
|
if (ecode == 0):
|
|
result = True
|
|
break
|
|
else:
|
|
result = True
|
|
|
|
return result
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_required_packages(self):
|
|
if (len(self.requiredPackages) > 0): eprint(f"Installing required packages: {self.requiredPackages}")
|
|
return self.install_package(self.requiredPackages)
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_docker_images(self, docker_image_file):
|
|
result = False
|
|
if docker_image_file and os.path.isfile(docker_image_file) and InstallerYesOrNo(f'Load Malcolm Docker images from {docker_image_file}', default=True, forceInteraction=True):
|
|
ecode, out = self.run_process(['docker', 'load', '-q', '-i', docker_image_file], privileged=True)
|
|
if (ecode == 0):
|
|
result = True
|
|
else:
|
|
eprint(f"Loading Malcolm Docker images failed: {out}")
|
|
return result
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_malcolm_files(self, malcolm_install_file):
|
|
result = False
|
|
installPath = None
|
|
if malcolm_install_file and os.path.isfile(malcolm_install_file) and InstallerYesOrNo(f'Extract Malcolm runtime files from {malcolm_install_file}', default=True, forceInteraction=True):
|
|
|
|
# determine and create destination path for installation
|
|
while True:
|
|
defaultPath = os.path.join(origPath, 'malcolm')
|
|
installPath = InstallerAskForString(f'Enter installation path for Malcolm [{defaultPath}]', default=defaultPath, forceInteraction=True)
|
|
if (len(installPath) == 0): installPath = defaultPath
|
|
if os.path.isdir(installPath):
|
|
eprint(f"{installPath} already exists, please specify a different installation path")
|
|
else:
|
|
try:
|
|
os.makedirs(installPath)
|
|
except:
|
|
pass
|
|
if os.path.isdir(installPath):
|
|
break
|
|
else:
|
|
eprint(f"Failed to create {installPath}, please specify a different installation path")
|
|
|
|
# extract runtime files
|
|
if installPath and os.path.isdir(installPath):
|
|
if self.debug:
|
|
eprint(f"Created {installPath} for Malcolm runtime files")
|
|
tar = tarfile.open(malcolm_install_file)
|
|
try:
|
|
tar.extractall(path=installPath, numeric_owner=True)
|
|
finally:
|
|
tar.close()
|
|
|
|
# .tar.gz normally will contain an intermediate subdirectory. if so, move files back one level
|
|
childDir = glob.glob(f'{installPath}/*/')
|
|
if (len(childDir) == 1) and os.path.isdir(childDir[0]):
|
|
if self.debug:
|
|
eprint(f"{installPath} only contains {childDir[0]}")
|
|
for f in os.listdir(childDir[0]):
|
|
shutil.move(os.path.join(childDir[0], f), installPath)
|
|
shutil.rmtree(childDir[0], ignore_errors=True)
|
|
|
|
# verify the installation worked
|
|
if os.path.isfile(os.path.join(installPath, "docker-compose.yml")):
|
|
eprint(f"Malcolm runtime files extracted to {installPath}")
|
|
result = True
|
|
with open(os.path.join(installPath, "install_source.txt"), 'w') as f:
|
|
f.write(f'{os.path.basename(malcolm_install_file)} (installed {str(datetime.datetime.now())})\n')
|
|
else:
|
|
eprint(f"Malcolm install file extracted to {installPath}, but missing runtime files?")
|
|
|
|
return result, installPath
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def tweak_malcolm_runtime(self, malcolm_install_path, expose_elastic_default=False, expose_logstash_default=False, restart_mode_default=False):
|
|
global args
|
|
|
|
if not args.configFile:
|
|
# get a list of all of the docker-compose files
|
|
composeFiles = glob.glob(os.path.join(malcolm_install_path, 'docker-compose*.yml'))
|
|
|
|
elif os.path.isfile(args.configFile):
|
|
# single docker-compose file explicitly specified
|
|
composeFiles = [os.path.realpath(args.configFile)]
|
|
malcolm_install_path = os.path.dirname(composeFiles[0])
|
|
|
|
# figure out what UID/GID to run non-rood processes under docker as
|
|
puid = '1000'
|
|
pgid = '1000'
|
|
try:
|
|
if (self.platform == PLATFORM_LINUX):
|
|
puid = str(os.getuid())
|
|
pgid = str(os.getgid())
|
|
if (puid == '0') or (pgid == '0'):
|
|
raise Exception('it is preferrable not to run Malcolm as root, prompting for UID/GID instead')
|
|
except:
|
|
puid = '1000'
|
|
pgid = '1000'
|
|
|
|
while (not puid.isdigit()) or (not pgid.isdigit()) or (not InstallerYesOrNo(f'Malcolm processes will run as UID {puid} and GID {pgid}. Is this OK?', default=True)):
|
|
puid = InstallerAskForString('Enter user ID (UID) for running non-root Malcolm processes')
|
|
pgid = InstallerAskForString('Enter group ID (GID) for running non-root Malcolm processes')
|
|
|
|
# guestimate how much memory we should use based on total system memory
|
|
|
|
if self.debug:
|
|
eprint(f"{malcolm_install_path} contains {composeFiles}, system memory is {self.totalMemoryGigs} GiB")
|
|
|
|
if self.totalMemoryGigs >= 63.0:
|
|
esMemory = '30g'
|
|
lsMemory = '6g'
|
|
elif self.totalMemoryGigs >= 31.0:
|
|
esMemory = '21g'
|
|
lsMemory = '3500m'
|
|
elif self.totalMemoryGigs >= 15.0:
|
|
esMemory = '10g'
|
|
lsMemory = '3g'
|
|
elif self.totalMemoryGigs >= 11.0:
|
|
esMemory = '6g'
|
|
lsMemory = '2500m'
|
|
elif self.totalMemoryGigs >= 7.0:
|
|
eprint(f"Detected only {self.totalMemoryGigs} GiB of memory; performance will be suboptimal")
|
|
esMemory = '4g'
|
|
lsMemory = '2500m'
|
|
elif self.totalMemoryGigs > 0.0:
|
|
eprint(f"Detected only {self.totalMemoryGigs} GiB of memory; performance will be suboptimal")
|
|
esMemory = '3500m'
|
|
lsMemory = '2g'
|
|
else:
|
|
eprint("Failed to determine system memory size, using defaults; performance may be suboptimal")
|
|
esMemory = '8g'
|
|
lsMemory = '3g'
|
|
|
|
while not InstallerYesOrNo(f'Setting {esMemory} for Elasticsearch and {lsMemory} for Logstash. Is this OK?', default=True):
|
|
esMemory = InstallerAskForString('Enter memory for Elasticsearch (e.g., 16g, 9500m, etc.)')
|
|
lsMemory = InstallerAskForString('Enter memory for LogStash (e.g., 4g, 2500m, etc.)')
|
|
|
|
restartMode = None
|
|
allowedRestartModes = ('no', 'on-failure', 'always', 'unless-stopped')
|
|
if InstallerYesOrNo('Restart Malcolm upon system or Docker daemon restart?', default=restart_mode_default):
|
|
while restartMode not in allowedRestartModes:
|
|
restartMode = InstallerAskForString(f'Select Malcolm restart behavior {allowedRestartModes}', default='unless-stopped')
|
|
else:
|
|
restartMode = 'no'
|
|
if (restartMode == 'no'): restartMode = '"no"'
|
|
|
|
ldapStartTLS = False
|
|
ldapServerType = 'winldap'
|
|
useBasicAuth = not InstallerYesOrNo('Authenticate against Lightweight Directory Access Protocol (LDAP) server?', default=False)
|
|
if not useBasicAuth:
|
|
allowedLdapModes = ('winldap', 'openldap')
|
|
ldapServerType = None
|
|
while ldapServerType not in allowedLdapModes:
|
|
ldapServerType = InstallerAskForString(f'Select LDAP server compatibility type {allowedLdapModes}', default='winldap')
|
|
ldapStartTLS = InstallerYesOrNo('Use StartTLS for LDAP connection security?', default=True)
|
|
try:
|
|
with open(os.path.join(os.path.realpath(os.path.join(ScriptPath, "..")), ".ldap_config_defaults"), "w") as ldapDefaultsFile:
|
|
print(f"LDAP_SERVER_TYPE='{ldapServerType}'", file=ldapDefaultsFile)
|
|
print(f"LDAP_PROTO='{'ldap://' if useBasicAuth or ldapStartTLS else 'ldaps://'}'", file=ldapDefaultsFile)
|
|
print(f"LDAP_PORT='{3268 if ldapStartTLS else 3269}'", file=ldapDefaultsFile)
|
|
except:
|
|
pass
|
|
|
|
indexSnapshotDir = None
|
|
indexSnapshotCompressed = False
|
|
indexSnapshotAge = '0'
|
|
indexColdAge = '0'
|
|
indexCloseAge = '0'
|
|
indexDeleteAge = '0'
|
|
indexPruneSizeLimit = '0'
|
|
indexPruneNameSort = False
|
|
|
|
if InstallerYesOrNo('Configure Elasticsearch index state management?', default=False):
|
|
|
|
# configure snapshots
|
|
if InstallerYesOrNo('Configure index snapshots?', default=False):
|
|
|
|
# snapshot repository directory and compression
|
|
indexSnapshotDir = './elasticsearch-backup'
|
|
if not InstallerYesOrNo('Store snapshots locally in {}?'.format(os.path.join(malcolm_install_path, 'elasticsearch-backup')), default=True):
|
|
while True:
|
|
indexSnapshotDir = InstallerAskForString('Enter Elasticsearch index snapshot directory')
|
|
if (len(indexSnapshotDir) > 1) and os.path.isdir(indexSnapshotDir):
|
|
indexSnapshotDir = os.path.realpath(indexSnapshotDir)
|
|
break
|
|
indexSnapshotCompressed = InstallerYesOrNo('Compress index snapshots?', default=False)
|
|
|
|
# index age for snapshot
|
|
indexSnapshotAge = ''
|
|
while (not re.match(r'^\d+[dhms]$', indexSnapshotAge)) and (indexSnapshotAge != '0'):
|
|
indexSnapshotAge = InstallerAskForString('Enter index age for snapshot (e.g., 1d)')
|
|
|
|
# cold state age
|
|
if InstallerYesOrNo('Mark indices read-only as they age?', default=False):
|
|
indexColdAge = ''
|
|
while (not re.match(r'^\d+[dhms]$', indexColdAge)) and (indexColdAge != '0'):
|
|
indexColdAge = InstallerAskForString('Enter index age for "read-only" transition (e.g., 30d)')
|
|
|
|
# close state age
|
|
if InstallerYesOrNo('Close indices as they age?', default=False):
|
|
indexCloseAge = ''
|
|
while (not re.match(r'^\d+[dhms]$', indexCloseAge)) and (indexCloseAge != '0'):
|
|
indexCloseAge = InstallerAskForString('Enter index age for "close" transition (e.g., 60d)')
|
|
|
|
# delete state age
|
|
if InstallerYesOrNo('Delete indices as they age?', default=False):
|
|
indexDeleteAge = ''
|
|
while (not re.match(r'^\d+[dhms]$', indexDeleteAge)) and (indexDeleteAge != '0'):
|
|
indexDeleteAge = InstallerAskForString('Enter index age for "delete" transition (e.g., 365d)')
|
|
|
|
# delete based on index pattern size
|
|
if InstallerYesOrNo('Delete the oldest indices when the database exceeds a certain size?', default=False):
|
|
indexPruneSizeLimit = ''
|
|
while (not re.match(r'^\d+(\.\d+)?\s*[kmgtp%]?b?$', indexPruneSizeLimit, flags=re.IGNORECASE)) and (indexPruneSizeLimit != '0'):
|
|
indexPruneSizeLimit = InstallerAskForString('Enter index threshold (e.g., 250GB, 1TB, 60%, etc.)')
|
|
indexPruneNameSort = InstallerYesOrNo('Determine oldest indices by name (instead of creation time)?', default=True)
|
|
|
|
autoZeek = InstallerYesOrNo('Automatically analyze all PCAP files with Zeek?', default=True)
|
|
reverseDns = InstallerYesOrNo('Perform reverse DNS lookup locally for source and destination IP addresses in Zeek logs?', default=False)
|
|
autoOui = InstallerYesOrNo('Perform hardware vendor OUI lookups for MAC addresses?', default=True)
|
|
autoFreq = InstallerYesOrNo('Perform string randomness scoring on some fields?', default=True)
|
|
elasticOpen = InstallerYesOrNo('Expose Elasticsearch port to external hosts?', default=expose_elastic_default)
|
|
logstashOpen = InstallerYesOrNo('Expose Logstash port to external hosts?', default=expose_logstash_default)
|
|
logstashSsl = logstashOpen and InstallerYesOrNo('Should Logstash require SSL for Zeek logs? (Note: This requires the forwarder to be similarly configured and a corresponding copy of the client SSL files.)', default=True)
|
|
externalEsForward = InstallerYesOrNo('Forward Logstash logs to external Elasticstack instance?', default=False)
|
|
if externalEsForward:
|
|
externalEsHost = InstallerAskForString('Enter external Elasticstack host:port (e.g., 10.0.0.123:9200)')
|
|
externalEsSsl = InstallerYesOrNo(f'Connect to "{externalEsHost}" using SSL?', default=True)
|
|
externalEsSslVerify = externalEsSsl and InstallerYesOrNo(f'Require SSL certificate validation for communication with "{externalEsHost}"?', default=False)
|
|
else:
|
|
externalEsHost = ""
|
|
externalEsSsl = False
|
|
externalEsSslVerify = False
|
|
|
|
# input file extraction parameters
|
|
allowedFileCarveModes = ('none', 'known', 'mapped', 'all', 'interesting')
|
|
allowedFilePreserveModes = ('quarantined', 'all', 'none')
|
|
|
|
fileCarveModeUser = None
|
|
fileCarveMode = None
|
|
filePreserveMode = None
|
|
vtotApiKey = '0'
|
|
yaraScan = False
|
|
capaScan = False
|
|
clamAvScan = False
|
|
ruleUpdate = False
|
|
|
|
if InstallerYesOrNo('Enable file extraction with Zeek?', default=False):
|
|
while fileCarveMode not in allowedFileCarveModes:
|
|
fileCarveMode = InstallerAskForString(f'Select file extraction behavior {allowedFileCarveModes}', default=allowedFileCarveModes[0])
|
|
while filePreserveMode not in allowedFilePreserveModes:
|
|
filePreserveMode = InstallerAskForString(f'Select file preservation behavior {allowedFilePreserveModes}', default=allowedFilePreserveModes[0])
|
|
if fileCarveMode is not None:
|
|
if InstallerYesOrNo('Scan extracted files with ClamAV?', default=False):
|
|
clamAvScan = True
|
|
if InstallerYesOrNo('Scan extracted files with Yara?', default=False):
|
|
yaraScan = True
|
|
if InstallerYesOrNo('Scan extracted PE files with Capa?', default=False):
|
|
capaScan = True
|
|
if InstallerYesOrNo('Lookup extracted file hashes with VirusTotal?', default=False):
|
|
while (len(vtotApiKey) <= 1):
|
|
vtotApiKey = InstallerAskForString('Enter VirusTotal API key')
|
|
ruleUpdate = InstallerYesOrNo('Download updated scanner signatures periodically?', default=True)
|
|
|
|
if fileCarveMode not in allowedFileCarveModes:
|
|
fileCarveMode = allowedFileCarveModes[0]
|
|
if filePreserveMode not in allowedFileCarveModes:
|
|
filePreserveMode = allowedFilePreserveModes[0]
|
|
if (vtotApiKey is None) or (len(vtotApiKey) <= 1):
|
|
vtotApiKey = '0'
|
|
|
|
# input packet capture parameters
|
|
pcapNetSniff = False
|
|
pcapTcpDump = False
|
|
pcapIface = 'lo'
|
|
if InstallerYesOrNo('Should Malcolm capture network traffic to PCAP files?', default=False):
|
|
pcapIface = ''
|
|
while (len(pcapIface) <= 0):
|
|
pcapIface = InstallerAskForString('Specify capture interface(s) (comma-separated)')
|
|
pcapNetSniff = InstallerYesOrNo('Capture packets using netsniff-ng?', default=True)
|
|
pcapTcpDump = InstallerYesOrNo('Capture packets using tcpdump?', default=(not pcapNetSniff))
|
|
|
|
# modify specified values in-place in docker-compose files
|
|
for composeFile in composeFiles:
|
|
# save off owner of original files
|
|
composeFileStat = os.stat(composeFile)
|
|
origUid, origGuid = composeFileStat[4], composeFileStat[5]
|
|
composeFileHandle = fileinput.FileInput(composeFile, inplace=True, backup=None)
|
|
try:
|
|
servicesSectionFound = False
|
|
serviceIndent = None
|
|
currentService = None
|
|
|
|
for line in composeFileHandle:
|
|
line = line.rstrip("\n")
|
|
skipLine = False
|
|
|
|
# it would be cleaner to use something like PyYAML to do this, but I want to have as few dependencies
|
|
# as possible so we're going to do it janky instead
|
|
|
|
# determine indentation for each service section (assumes YML file is consistently indented)
|
|
if (not servicesSectionFound) and line.lower().startswith('services:'):
|
|
servicesSectionFound = True
|
|
elif servicesSectionFound and (serviceIndent is None):
|
|
indentMatch = re.search(r'^(\s+)\S+\s*:\s*$', line)
|
|
if indentMatch is not None:
|
|
serviceIndent = indentMatch.group(1)
|
|
|
|
# determine which service we're currently processing in the YML file
|
|
serviceStartLine = False
|
|
if servicesSectionFound and (serviceIndent is not None):
|
|
serviceMatch = re.search(fr'^{serviceIndent}(\S+)\s*:\s*$', line)
|
|
if serviceMatch is not None:
|
|
currentService = serviceMatch.group(1).lower()
|
|
serviceStartLine = True
|
|
|
|
if (currentService is not None) and (restartMode is not None) and re.match(r'^\s*restart\s*:.*$', line):
|
|
# elasticsearch backup directory
|
|
line = f"{serviceIndent * 2}restart: {restartMode}"
|
|
elif 'PUID' in line:
|
|
# process UID
|
|
line = re.sub(r'(PUID\s*:\s*)(\S+)', fr"\g<1>{puid}", line)
|
|
elif 'PGID' in line:
|
|
# process GID
|
|
line = re.sub(r'(PGID\s*:\s*)(\S+)', fr"\g<1>{pgid}", line)
|
|
elif 'NGINX_BASIC_AUTH' in line:
|
|
# basic (useBasicAuth=true) vs ldap (useBasicAuth=false)
|
|
line = re.sub(r'(NGINX_BASIC_AUTH\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(useBasicAuth)}", line)
|
|
elif 'NGINX_LDAP_TLS_STUNNEL' in line:
|
|
# StartTLS vs. ldap:// or ldaps://
|
|
line = re.sub(r'(NGINX_LDAP_TLS_STUNNEL\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(((not useBasicAuth) and ldapStartTLS))}", line)
|
|
elif 'ZEEK_EXTRACTOR_MODE' in line:
|
|
# zeek file extraction mode
|
|
line = re.sub(r'(ZEEK_EXTRACTOR_MODE\s*:\s*)(\S+)', fr"\g<1>'{fileCarveMode}'", line)
|
|
elif 'EXTRACTED_FILE_PRESERVATION' in line:
|
|
# zeek file preservation mode
|
|
line = re.sub(r'(EXTRACTED_FILE_PRESERVATION\s*:\s*)(\S+)', fr"\g<1>'{filePreserveMode}'", line)
|
|
elif 'VTOT_API2_KEY' in line:
|
|
# virustotal API key
|
|
line = re.sub(r'(VTOT_API2_KEY\s*:\s*)(\S+)', fr"\g<1>'{vtotApiKey}'", line)
|
|
elif 'EXTRACTED_FILE_ENABLE_YARA' in line:
|
|
# file scanning via yara
|
|
line = re.sub(r'(EXTRACTED_FILE_ENABLE_YARA\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(yaraScan)}", line)
|
|
elif 'EXTRACTED_FILE_ENABLE_CAPA' in line:
|
|
# PE file scanning via capa
|
|
line = re.sub(r'(EXTRACTED_FILE_ENABLE_CAPA\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(capaScan)}", line)
|
|
elif 'EXTRACTED_FILE_ENABLE_CLAMAV' in line:
|
|
# file scanning via clamav
|
|
line = re.sub(r'(EXTRACTED_FILE_ENABLE_CLAMAV\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(clamAvScan)}", line)
|
|
elif 'EXTRACTED_FILE_UPDATE_RULES' in line:
|
|
# rule updates (yara/capa via git, clamav via freshclam)
|
|
line = re.sub(r'(EXTRACTED_FILE_UPDATE_RULES\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(ruleUpdate)}", line)
|
|
elif 'PCAP_ENABLE_NETSNIFF' in line:
|
|
# capture pcaps via netsniff-ng
|
|
line = re.sub(r'(PCAP_ENABLE_NETSNIFF\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(pcapNetSniff)}", line)
|
|
elif 'PCAP_ENABLE_TCPDUMP' in line:
|
|
# capture pcaps via tcpdump
|
|
line = re.sub(r'(PCAP_ENABLE_TCPDUMP\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(pcapTcpDump)}", line)
|
|
elif 'PCAP_IFACE' in line:
|
|
# capture interface(s)
|
|
line = re.sub(r'(PCAP_IFACE\s*:\s*)(\S+)', fr"\g<1>'{pcapIface}'", line)
|
|
elif 'ES_JAVA_OPTS' in line:
|
|
# elasticsearch memory allowance
|
|
line = re.sub(r'(-Xm[sx])(\w+)', fr'\g<1>{esMemory}', line)
|
|
elif 'LS_JAVA_OPTS' in line:
|
|
# logstash memory allowance
|
|
line = re.sub(r'(-Xm[sx])(\w+)', fr'\g<1>{lsMemory}', line)
|
|
elif 'ZEEK_AUTO_ANALYZE_PCAP_FILES' in line:
|
|
# automatic pcap analysis with Zeek
|
|
line = re.sub(r'(ZEEK_AUTO_ANALYZE_PCAP_FILES\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(autoZeek)}", line)
|
|
elif 'LOGSTASH_REVERSE_DNS' in line:
|
|
# automatic local reverse dns lookup
|
|
line = re.sub(r'(LOGSTASH_REVERSE_DNS\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(reverseDns)}", line)
|
|
elif 'LOGSTASH_OUI_LOOKUP' in line:
|
|
# automatic MAC OUI lookup
|
|
line = re.sub(r'(LOGSTASH_OUI_LOOKUP\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(autoOui)}", line)
|
|
elif 'FREQ_LOOKUP' in line:
|
|
# freq.py string randomness calculations
|
|
line = re.sub(r'(FREQ_LOOKUP\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(autoFreq)}", line)
|
|
elif 'BEATS_SSL' in line:
|
|
# enable/disable beats SSL
|
|
line = re.sub(r'(BEATS_SSL\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(logstashOpen and logstashSsl)}", line)
|
|
elif (currentService == 'elasticsearch') and re.match(r'^\s*-.+:/opt/elasticsearch/backup(:.+)?\s*$', line) and (indexSnapshotDir is not None) and os.path.isdir(indexSnapshotDir):
|
|
# elasticsearch backup directory
|
|
volumeParts = line.strip().lstrip('-').lstrip().split(':')
|
|
volumeParts[0] = indexSnapshotDir
|
|
line = "{}- {}".format(serviceIndent * 3, ':'.join(volumeParts))
|
|
elif 'ISM_SNAPSHOT_AGE' in line:
|
|
# elasticsearch index state management snapshot age
|
|
line = re.sub(r'(ISM_SNAPSHOT_AGE\s*:\s*)(\S+)', fr"\g<1>'{indexSnapshotAge}'", line)
|
|
elif 'ISM_COLD_AGE' in line:
|
|
# elasticsearch index state management cold (read-only) age
|
|
line = re.sub(r'(ISM_COLD_AGE\s*:\s*)(\S+)', fr"\g<1>'{indexColdAge}'", line)
|
|
elif 'ISM_CLOSE_AGE' in line:
|
|
# elasticsearch index state management close age
|
|
line = re.sub(r'(ISM_CLOSE_AGE\s*:\s*)(\S+)', fr"\g<1>'{indexCloseAge}'", line)
|
|
elif 'ISM_DELETE_AGE' in line:
|
|
# elasticsearch index state management close age
|
|
line = re.sub(r'(ISM_DELETE_AGE\s*:\s*)(\S+)', fr"\g<1>'{indexDeleteAge}'", line)
|
|
elif 'ISM_SNAPSHOT_COMPRESSED' in line:
|
|
# elasticsearch index state management snapshot compression
|
|
line = re.sub(r'(ISM_SNAPSHOT_COMPRESSED\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(indexSnapshotCompressed)}", line)
|
|
elif 'ELASTICSEARCH_INDEX_SIZE_PRUNE_LIMIT' in line:
|
|
# delete based on index pattern size
|
|
line = re.sub(r'(ELASTICSEARCH_INDEX_SIZE_PRUNE_LIMIT\s*:\s*)(\S+)', fr"\g<1>'{indexPruneSizeLimit}'", line)
|
|
elif 'ELASTICSEARCH_INDEX_SIZE_PRUNE_NAME_SORT' in line:
|
|
# delete based on index pattern size (sorted by name vs. creation time)
|
|
line = re.sub(r'(ELASTICSEARCH_INDEX_SIZE_PRUNE_NAME_SORT\s*:\s*)(\S+)', fr"\g<1>{TrueOrFalseQuote(indexPruneNameSort)}", line)
|
|
elif 'ES_EXTERNAL_HOSTS' in line:
|
|
# enable/disable forwarding Logstash to external Elasticsearch instance
|
|
line = re.sub(r'(#\s*)?(ES_EXTERNAL_HOSTS\s*:\s*)(\S+)', fr"\g<2>'{externalEsHost}'", line)
|
|
elif 'ES_EXTERNAL_SSL_CERTIFICATE_VERIFICATION' in line:
|
|
# enable/disable SSL certificate verification for external Elasticsearch instance
|
|
line = re.sub(r'(#\s*)?(ES_EXTERNAL_SSL_CERTIFICATE_VERIFICATION\s*:\s*)(\S+)', fr"\g<2>{TrueOrFalseQuote(externalEsSsl and externalEsSslVerify)}", line)
|
|
elif 'ES_EXTERNAL_SSL' in line:
|
|
# enable/disable SSL certificate verification for external Elasticsearch instance
|
|
line = re.sub(r'(#\s*)?(ES_EXTERNAL_SSL\s*:\s*)(\S+)', fr"\g<2>{TrueOrFalseQuote(externalEsSsl)}", line)
|
|
elif logstashOpen and serviceStartLine and (currentService == 'logstash'):
|
|
# exposing logstash port 5044 to the world
|
|
print(line)
|
|
line = f"{serviceIndent * 2}ports:"
|
|
print(line)
|
|
line = f'{serviceIndent * 3}- "0.0.0.0:5044:5044"'
|
|
elif (not serviceStartLine) and (currentService == 'logstash') and re.match(fr'^({serviceIndent * 2}ports:|{serviceIndent * 3}-.*5044:5044)"?\s*$', line):
|
|
# remove previous/leftover/duplicate exposing logstash port 5044 to the world
|
|
skipLine = True
|
|
elif (not serviceStartLine) and (currentService == 'nginx-proxy') and re.match(r'^.*-.*\b9200:9200"?\s*$', line):
|
|
# comment/uncomment port forwarding for elastic based on elasticOpen
|
|
leadingSpaces = len(line) - len(line.lstrip())
|
|
if leadingSpaces <= 0: leadingSpaces = 6
|
|
line = f"{' ' * leadingSpaces}{'' if elasticOpen else '# '}{line.lstrip().lstrip('#').lstrip()}"
|
|
|
|
if not skipLine: print(line)
|
|
|
|
finally:
|
|
composeFileHandle.close()
|
|
# restore ownership
|
|
os.chown(composeFile, origUid, origGuid)
|
|
|
|
# if the Malcolm dir is owned by root, see if they want to reassign ownership to a non-root user
|
|
if (((self.platform == PLATFORM_LINUX) or (self.platform == PLATFORM_MAC)) and
|
|
(self.scriptUser == "root") and (getpwuid(os.stat(malcolm_install_path).st_uid).pw_name == self.scriptUser) and
|
|
InstallerYesOrNo(f'Set ownership of {malcolm_install_path} to an account other than {self.scriptUser}?', default=True, forceInteraction=True)):
|
|
tmpUser = ''
|
|
while (len(tmpUser) == 0):
|
|
tmpUser = InstallerAskForString('Enter user account').strip()
|
|
err, out = self.run_process(['id', '-g', '-n', tmpUser], stderr=True)
|
|
if (err == 0) and (len(out) > 0) and (len(out[0]) > 0):
|
|
tmpUser = f"{tmpUser}:{out[0]}"
|
|
err, out = self.run_process(['chown', '-R', tmpUser, malcolm_install_path], stderr=True)
|
|
if (err == 0):
|
|
if self.debug: eprint(f"Changing ownership of {malcolm_install_path} to {tmpUser} succeeded")
|
|
else:
|
|
eprint(f"Changing ownership of {malcolm_install_path} to {tmpUser} failed: {out}")
|
|
|
|
|
|
###################################################################################################
|
|
class LinuxInstaller(Installer):
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def __init__(self, debug=False, configOnly=False):
|
|
super().__init__(debug, configOnly)
|
|
|
|
self.distro = None
|
|
self.codename = None
|
|
self.release = None
|
|
|
|
# determine the distro (e.g., ubuntu) and code name (e.g., bionic) if applicable
|
|
|
|
# check /etc/os-release values first
|
|
if os.path.isfile('/etc/os-release'):
|
|
osInfo = dict()
|
|
|
|
with open("/etc/os-release", 'r') as f:
|
|
for line in f:
|
|
try:
|
|
k, v = line.rstrip().split("=")
|
|
osInfo[k] = v.strip('"')
|
|
except:
|
|
pass
|
|
|
|
if ('NAME' in osInfo) and (len(osInfo['NAME']) > 0):
|
|
distro = osInfo['NAME'].lower().split()[0]
|
|
|
|
if ('VERSION_CODENAME' in osInfo) and (len(osInfo['VERSION_CODENAME']) > 0):
|
|
codename = osInfo['VERSION_CODENAME'].lower().split()[0]
|
|
|
|
if ('VERSION_ID' in osInfo) and (len(osInfo['VERSION_ID']) > 0):
|
|
release = osInfo['VERSION_ID'].lower().split()[0]
|
|
|
|
# try lsb_release next
|
|
if (self.distro is None):
|
|
err, out = self.run_process(['lsb_release', '-is'], stderr=False)
|
|
if (err == 0) and (len(out) > 0):
|
|
self.distro = out[0].lower()
|
|
|
|
if (self.codename is None):
|
|
err, out = self.run_process(['lsb_release', '-cs'], stderr=False)
|
|
if (err == 0) and (len(out) > 0):
|
|
self.codename = out[0].lower()
|
|
|
|
if (self.release is None):
|
|
err, out = self.run_process(['lsb_release', '-rs'], stderr=False)
|
|
if (err == 0) and (len(out) > 0):
|
|
self.release = out[0].lower()
|
|
|
|
# try release-specific files
|
|
if (self.distro is None):
|
|
if os.path.isfile('/etc/centos-release'):
|
|
distroFile = '/etc/centos-release'
|
|
if os.path.isfile('/etc/redhat-release'):
|
|
distroFile = '/etc/redhat-release'
|
|
elif os.path.isfile('/etc/issue'):
|
|
distroFile = '/etc/issue'
|
|
else:
|
|
distroFile = None
|
|
if (distroFile is not None):
|
|
with open(distroFile, 'r') as f:
|
|
distroVals = f.read().lower().split()
|
|
distroNums = [x for x in distroVals if x[0].isdigit()]
|
|
self.distro = distroVals[0]
|
|
if (self.release is None) and (len(distroNums) > 0):
|
|
self.release = distroNums[0]
|
|
|
|
if (self.distro is None):
|
|
self.distro = "linux"
|
|
|
|
if self.debug:
|
|
eprint(f"distro: {self.distro}{f' {self.codename}' if self.codename else ''}{f' {self.release}' if self.release else ''}")
|
|
|
|
if not self.codename: self.codename = self.distro
|
|
|
|
# determine packages required by Malcolm itself (not docker, those will be done later)
|
|
if (self.distro == PLATFORM_LINUX_UBUNTU) or (self.distro == PLATFORM_LINUX_DEBIAN):
|
|
self.requiredPackages.extend(['apache2-utils', 'make', 'openssl'])
|
|
elif (self.distro == PLATFORM_LINUX_FEDORA) or (self.distro == PLATFORM_LINUX_CENTOS):
|
|
self.requiredPackages.extend(['httpd-tools', 'make', 'openssl'])
|
|
|
|
# on Linux this script requires root, or sudo, unless we're in local configuration-only mode
|
|
if os.getuid() == 0:
|
|
self.scriptUser = "root"
|
|
self.sudoCmd = []
|
|
else:
|
|
self.sudoCmd = ["sudo", "-n"]
|
|
err, out = self.run_process(['whoami'], privileged=True)
|
|
if ((err != 0) or (len(out) == 0) or (out[0] != 'root')) and (not self.configOnly):
|
|
raise Exception(f'{ScriptName} must be run as root, or {self.sudoCmd} must be available')
|
|
|
|
# determine command to use to query if a package is installed
|
|
if Which('dpkg', debug=self.debug):
|
|
os.environ["DEBIAN_FRONTEND"] = "noninteractive"
|
|
self.checkPackageCmds.append(['dpkg', '-s'])
|
|
elif Which('rpm', debug=self.debug):
|
|
self.checkPackageCmds.append(['rpm', '-q'])
|
|
elif Which('dnf', debug=self.debug):
|
|
self.checkPackageCmds.append(['dnf', 'list', 'installed'])
|
|
elif Which('yum', debug=self.debug):
|
|
self.checkPackageCmds.append(['yum', 'list', 'installed'])
|
|
|
|
# determine command to install a package from the distro's repos
|
|
if Which('apt-get', debug=self.debug):
|
|
self.installPackageCmds.append(['apt-get', 'install', '-y', '-qq'])
|
|
elif Which('apt', debug=self.debug):
|
|
self.installPackageCmds.append(['apt', 'install', '-y', '-qq'])
|
|
elif Which('dnf', debug=self.debug):
|
|
self.installPackageCmds.append(['dnf', '-y', 'install', '--nobest'])
|
|
elif Which('yum', debug=self.debug):
|
|
self.installPackageCmds.append(['yum', '-y', 'install'])
|
|
|
|
# determine total system memory
|
|
try:
|
|
totalMemBytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')
|
|
self.totalMemoryGigs = math.ceil(totalMemBytes/(1024.**3))
|
|
except:
|
|
self.totalMemoryGigs = 0.0
|
|
|
|
# determine total system memory a different way if the first way didn't work
|
|
if (self.totalMemoryGigs <= 0.0):
|
|
err, out = self.run_process(['awk', '/MemTotal/ { printf "%.0f \\n", $2 }', '/proc/meminfo'])
|
|
if (err == 0) and (len(out) > 0):
|
|
totalMemKiloBytes = int(out[0])
|
|
self.totalMemoryGigs = math.ceil(totalMemKiloBytes/(1024.**2))
|
|
|
|
# determine total system CPU cores
|
|
try:
|
|
self.totalCores = os.sysconf('SC_NPROCESSORS_ONLN')
|
|
except:
|
|
self.totalCores = 0
|
|
|
|
# determine total system CPU cores a different way if the first way didn't work
|
|
if (self.totalCores <= 0):
|
|
err, out = self.run_process(['grep', '-c', '^processor', '/proc/cpuinfo'])
|
|
if (err == 0) and (len(out) > 0):
|
|
self.totalCores = int(out[0])
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_docker(self):
|
|
result = False
|
|
|
|
# first see if docker is already installed and runnable
|
|
err, out = self.run_process(['docker', 'info'], privileged=True)
|
|
|
|
if (err == 0):
|
|
result = True
|
|
|
|
elif InstallerYesOrNo('"docker info" failed, attempt to install Docker?', default=True):
|
|
|
|
if InstallerYesOrNo('Attempt to install Docker using official repositories?', default=True):
|
|
|
|
# install required packages for repo-based install
|
|
if self.distro == PLATFORM_LINUX_UBUNTU:
|
|
requiredRepoPackages = ['apt-transport-https', 'ca-certificates', 'curl', 'gnupg-agent', 'software-properties-common']
|
|
elif self.distro == PLATFORM_LINUX_DEBIAN:
|
|
requiredRepoPackages = ['apt-transport-https', 'ca-certificates', 'curl', 'gnupg2', 'software-properties-common']
|
|
elif self.distro == PLATFORM_LINUX_FEDORA:
|
|
requiredRepoPackages = ['dnf-plugins-core']
|
|
elif self.distro == PLATFORM_LINUX_CENTOS:
|
|
requiredRepoPackages = ['yum-utils', 'device-mapper-persistent-data', 'lvm2']
|
|
else:
|
|
requiredRepoPackages = []
|
|
|
|
if len(requiredRepoPackages) > 0:
|
|
eprint(f"Installing required packages: {requiredRepoPackages}")
|
|
self.install_package(requiredRepoPackages)
|
|
|
|
# install docker via repo if possible
|
|
dockerPackages = []
|
|
if ((self.distro == PLATFORM_LINUX_UBUNTU) or (self.distro == PLATFORM_LINUX_DEBIAN)) and self.codename:
|
|
|
|
# for debian/ubuntu, add docker GPG key and check its fingerprint
|
|
if self.debug:
|
|
eprint("Requesting docker GPG key for package signing")
|
|
dockerGpgKey = requests.get(f'https://download.docker.com/linux/{self.distro}/gpg', allow_redirects=True)
|
|
err, out = self.run_process(['apt-key', 'add'], stdin=dockerGpgKey.content.decode(sys.getdefaultencoding()), privileged=True, stderr=False)
|
|
if (err == 0):
|
|
err, out = self.run_process(['apt-key', 'fingerprint', DEB_GPG_KEY_FINGERPRINT], privileged=True, stderr=False)
|
|
|
|
# add docker .deb repository
|
|
if (err == 0):
|
|
if self.debug:
|
|
eprint("Adding docker repository")
|
|
err, out = self.run_process(['add-apt-repository', '-y', '-r', f'deb [arch=amd64] https://download.docker.com/linux/{self.distro} {self.codename} stable'], privileged=True)
|
|
err, out = self.run_process(['add-apt-repository', '-y', '-u', f'deb [arch=amd64] https://download.docker.com/linux/{self.distro} {self.codename} stable'], privileged=True)
|
|
|
|
# docker packages to install
|
|
if (err == 0):
|
|
dockerPackages.extend(['docker-ce', 'docker-ce-cli', 'containerd.io'])
|
|
|
|
elif self.distro == PLATFORM_LINUX_FEDORA:
|
|
|
|
# add docker fedora repository
|
|
if self.debug:
|
|
eprint("Adding docker repository")
|
|
err, out = self.run_process(['dnf', 'config-manager', '-y', '--add-repo', 'https://download.docker.com/linux/fedora/docker-ce.repo'], privileged=True)
|
|
|
|
# docker packages to install
|
|
if (err == 0):
|
|
dockerPackages.extend(['docker-ce', 'docker-ce-cli', 'containerd.io'])
|
|
|
|
elif self.distro == PLATFORM_LINUX_CENTOS:
|
|
# add docker centos repository
|
|
if self.debug:
|
|
eprint("Adding docker repository")
|
|
err, out = self.run_process(['yum-config-manager', '-y', '--add-repo', 'https://download.docker.com/linux/centos/docker-ce.repo'], privileged=True)
|
|
|
|
# docker packages to install
|
|
if (err == 0):
|
|
dockerPackages.extend(['docker-ce', 'docker-ce-cli', 'containerd.io'])
|
|
|
|
else:
|
|
err, out = None, None
|
|
|
|
if len(dockerPackages) > 0:
|
|
eprint(f"Installing docker packages: {dockerPackages}")
|
|
if self.install_package(dockerPackages):
|
|
eprint("Installation of docker packages apparently succeeded")
|
|
result = True
|
|
else:
|
|
eprint("Installation of docker packages failed")
|
|
|
|
# the user either chose not to use the official repos, the official repo installation failed, or there are not official repos available
|
|
# see if we want to attempt using the convenience script at https://get.docker.com (see https://github.com/docker/docker-install)
|
|
if not result and InstallerYesOrNo('Docker not installed via official repositories. Attempt to install Docker via convenience script (please read https://github.com/docker/docker-install)?', default=False):
|
|
tempFileName = os.path.join(self.tempDirName, 'docker-install.sh')
|
|
if DownloadToFile("https://get.docker.com/", tempFileName, debug=self.debug):
|
|
os.chmod(tempFileName, 493) # 493 = 0o755
|
|
err, out = self.run_process(([tempFileName]), privileged=True)
|
|
if (err == 0):
|
|
eprint("Installation of docker apparently succeeded")
|
|
result = True
|
|
else:
|
|
eprint(f"Installation of docker failed: {out}")
|
|
else:
|
|
eprint(f"Downloading {dockerComposeUrl} to {tempFileName} failed")
|
|
|
|
if result and ((self.distro == PLATFORM_LINUX_FEDORA) or (self.distro == PLATFORM_LINUX_CENTOS)):
|
|
# centos/fedora don't automatically start/enable the daemon, so do so now
|
|
err, out = self.run_process(['systemctl', 'start', 'docker'], privileged=True)
|
|
if (err == 0):
|
|
err, out = self.run_process(['systemctl', 'enable', 'docker'], privileged=True)
|
|
if (err != 0):
|
|
eprint(f"Enabling docker service failed: {out}")
|
|
else:
|
|
eprint(f"Starting docker service failed: {out}")
|
|
|
|
# at this point we either have installed docker successfully or we have to give up, as we've tried all we could
|
|
err, out = self.run_process(['docker', 'info'], privileged=True, retry=6, retrySleepSec=5)
|
|
if result and (err == 0):
|
|
if self.debug:
|
|
eprint('"docker info" succeeded')
|
|
|
|
# add non-root user to docker group if required
|
|
usersToAdd = []
|
|
if self.scriptUser == 'root':
|
|
while InstallerYesOrNo(f"Add {'a' if len(usersToAdd) == 0 else 'another'} non-root user to the \"docker\" group?"):
|
|
tmpUser = InstallerAskForString('Enter user account')
|
|
if (len(tmpUser) > 0): usersToAdd.append(tmpUser)
|
|
else:
|
|
usersToAdd.append(self.scriptUser)
|
|
|
|
for user in usersToAdd:
|
|
err, out = self.run_process(['usermod', '-a', '-G', 'docker', user], privileged=True)
|
|
if (err == 0):
|
|
if self.debug:
|
|
eprint(f'Adding {user} to "docker" group succeeded')
|
|
else:
|
|
eprint(f'Adding {user} to "docker" group failed')
|
|
|
|
elif (err != 0):
|
|
result = False
|
|
raise Exception(f'{ScriptName} requires docker, please see {DOCKER_INSTALL_URLS[self.distro]}')
|
|
|
|
return result
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_docker_compose(self):
|
|
result = False
|
|
|
|
dockerComposeCmd = 'docker-compose'
|
|
if not Which(dockerComposeCmd, debug=self.debug) and os.path.isfile('/usr/local/bin/docker-compose'):
|
|
dockerComposeCmd = '/usr/local/bin/docker-compose'
|
|
|
|
# first see if docker-compose is already installed and runnable (try non-root and root)
|
|
err, out = self.run_process([dockerComposeCmd, 'version'], privileged=False)
|
|
if (err != 0):
|
|
err, out = self.run_process([dockerComposeCmd, 'version'], privileged=True)
|
|
|
|
if (err != 0) and InstallerYesOrNo('"docker-compose version" failed, attempt to install docker-compose?', default=True):
|
|
|
|
if InstallerYesOrNo('Install docker-compose directly from docker github?', default=True):
|
|
# download docker-compose from github and put it in /usr/local/bin
|
|
|
|
# need to know some linux platform info
|
|
unames = []
|
|
err, out = self.run_process((['uname', '-s']))
|
|
if (err == 0) and (len(out) > 0): unames.append(out[0])
|
|
err, out = self.run_process((['uname', '-m']))
|
|
if (err == 0) and (len(out) > 0): unames.append(out[0])
|
|
if len(unames) == 2:
|
|
# download docker-compose from github and save it to a temporary file
|
|
tempFileName = os.path.join(self.tempDirName, dockerComposeCmd)
|
|
dockerComposeUrl = f"https://github.com/docker/compose/releases/download/{DOCKER_COMPOSE_INSTALL_VERSION}/docker-compose-{unames[0]}-{unames[1]}"
|
|
if DownloadToFile(dockerComposeUrl, tempFileName, debug=self.debug):
|
|
os.chmod(tempFileName, 493) # 493 = 0o755, mark as executable
|
|
# put docker-compose into /usr/local/bin
|
|
err, out = self.run_process((['cp', '-f', tempFileName, '/usr/local/bin/docker-compose']), privileged=True)
|
|
if (err == 0):
|
|
eprint("Download and installation of docker-compose apparently succeeded")
|
|
dockerComposeCmd = '/usr/local/bin/docker-compose'
|
|
else:
|
|
raise Exception(f'Error copying {tempFileName} to /usr/local/bin: {out}')
|
|
|
|
else:
|
|
eprint(f"Downloading {dockerComposeUrl} to {tempFileName} failed")
|
|
|
|
elif InstallerYesOrNo('Install docker-compose via pip (privileged)?', default=False):
|
|
# install docker-compose via pip (as root)
|
|
err, out = self.run_process([self.pipCmd, 'install', dockerComposeCmd], privileged=True)
|
|
if (err == 0):
|
|
eprint("Installation of docker-compose apparently succeeded")
|
|
else:
|
|
eprint(f"Install docker-compose via pip failed with {err}, {out}")
|
|
|
|
elif InstallerYesOrNo('Install docker-compose via pip (user)?', default=True):
|
|
# install docker-compose via pip (regular user)
|
|
err, out = self.run_process([self.pipCmd, 'install', dockerComposeCmd], privileged=False)
|
|
if (err == 0):
|
|
eprint("Installation of docker-compose apparently succeeded")
|
|
else:
|
|
eprint(f"Install docker-compose via pip failed with {err}, {out}")
|
|
|
|
# see if docker-compose is now installed and runnable (try non-root and root)
|
|
err, out = self.run_process([dockerComposeCmd, 'version'], privileged=False)
|
|
if (err != 0):
|
|
err, out = self.run_process([dockerComposeCmd, 'version'], privileged=True)
|
|
|
|
if (err == 0):
|
|
result = True
|
|
if self.debug:
|
|
eprint('"docker-compose version" succeeded')
|
|
|
|
else:
|
|
raise Exception(f'{ScriptName} requires docker-compose, please see {DOCKER_COMPOSE_INSTALL_URLS[self.platform]}')
|
|
|
|
return result
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def tweak_system_files(self):
|
|
|
|
# make some system configuration changes with permission
|
|
|
|
ConfigLines = namedtuple("ConfigLines", ["distros", "filename", "prefix", "description", "lines"], rename=False)
|
|
|
|
configLinesToAdd = [ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'fs.file-max=',
|
|
'fs.file-max increases allowed maximum for file handles',
|
|
['# the maximum number of open file handles',
|
|
'fs.file-max=2097152']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'fs.inotify.max_user_watches=',
|
|
'fs.inotify.max_user_watches increases allowed maximum for monitored files',
|
|
['# the maximum number of user inotify watches',
|
|
'fs.inotify.max_user_watches=131072']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'fs.inotify.max_queued_events=',
|
|
'fs.inotify.max_queued_events increases queue size for monitored files',
|
|
['# the inotify event queue size',
|
|
'fs.inotify.max_queued_events=131072']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'fs.inotify.max_user_instances=',
|
|
'fs.inotify.max_user_instances increases allowed maximum monitor file watchers',
|
|
['# the maximum number of user inotify monitors',
|
|
'fs.inotify.max_user_instances=512']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'vm.max_map_count=',
|
|
'vm.max_map_count increases allowed maximum for memory segments',
|
|
['# the maximum number of memory map areas a process may have',
|
|
'vm.max_map_count=262144']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'net.core.somaxconn=',
|
|
'net.core.somaxconn increases allowed maximum for socket connections',
|
|
['# the maximum number of incoming connections',
|
|
'net.core.somaxconn=65535']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'vm.swappiness=',
|
|
'vm.swappiness adjusts the preference of the system to swap vs. drop runtime memory pages',
|
|
['# decrease "swappiness" (swapping out runtime memory vs. dropping pages)',
|
|
'vm.swappiness=1']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'vm.dirty_background_ratio=',
|
|
'vm.dirty_background_ratio defines the percentage of system memory fillable with "dirty" pages before flushing',
|
|
['# the % of system memory fillable with "dirty" pages before flushing',
|
|
'vm.dirty_background_ratio=40']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'vm.dirty_background_ratio=',
|
|
'vm.dirty_background_ratio defines the percentage of dirty system memory before flushing',
|
|
['# maximum % of dirty system memory before committing everything',
|
|
'vm.dirty_background_ratio=40']),
|
|
ConfigLines([],
|
|
'/etc/sysctl.conf',
|
|
'vm.dirty_ratio=',
|
|
'vm.dirty_ratio defines the maximum percentage of dirty system memory before committing everything',
|
|
['# maximum % of dirty system memory before committing everything',
|
|
'vm.dirty_ratio=80']),
|
|
ConfigLines(['centos', 'core'],
|
|
'/etc/systemd/system.conf.d/limits.conf',
|
|
'',
|
|
'/etc/systemd/system.conf.d/limits.conf increases the allowed maximums for file handles and memlocked segments',
|
|
['[Manager]',
|
|
'DefaultLimitNOFILE=65535:65535',
|
|
'DefaultLimitMEMLOCK=infinity']),
|
|
ConfigLines(['bionic', 'cosmic', 'disco', 'eoan', 'focal', 'groovy', 'stretch', 'buster', 'bullseye', 'sid', 'fedora'],
|
|
'/etc/security/limits.d/limits.conf',
|
|
'',
|
|
'/etc/security/limits.d/limits.conf increases the allowed maximums for file handles and memlocked segments',
|
|
['* soft nofile 65535',
|
|
'* hard nofile 65535',
|
|
'* soft memlock unlimited',
|
|
'* hard memlock unlimited'])]
|
|
|
|
for config in configLinesToAdd:
|
|
|
|
if (((len(config.distros) == 0) or (self.codename in config.distros)) and
|
|
(os.path.isfile(config.filename) or InstallerYesOrNo(f'\n{config.description}\n{config.filename} does not exist, create it?', default=True))):
|
|
|
|
confFileLines = [line.rstrip('\n') for line in open(config.filename)] if os.path.isfile(config.filename) else []
|
|
|
|
if ((len(confFileLines) == 0) or
|
|
(not os.path.isfile(config.filename) and (len(config.prefix) == 0)) or
|
|
((len(list(filter(lambda x: x.startswith(config.prefix), confFileLines))) == 0) and
|
|
InstallerYesOrNo(f'\n{config.description}\n{config.prefix} appears to be missing from {config.filename}, append it?', default=True))):
|
|
|
|
echoNewLineJoin = '\\n'
|
|
err, out = self.run_process(['bash',
|
|
'-c',
|
|
f"mkdir -p {os.path.dirname(config.filename)} && echo -n -e '{echoNewLineJoin}{echoNewLineJoin.join(config.lines)}{echoNewLineJoin} >> '{config.filename}'"], privileged=True)
|
|
|
|
###################################################################################################
|
|
class MacInstaller(Installer):
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def __init__(self, debug=False, configOnly=False):
|
|
super().__init__(debug, configOnly)
|
|
|
|
self.sudoCmd = []
|
|
|
|
# first see if brew is already installed and runnable
|
|
err, out = self.run_process(['brew', 'info'])
|
|
brewInstalled = (err == 0)
|
|
|
|
if brewInstalled and InstallerYesOrNo('Homebrew is installed: continue with Homebrew?', default=True):
|
|
self.useBrew = True
|
|
|
|
else:
|
|
self.useBrew = False
|
|
eprint('Docker can be installed and maintained with Homebrew, or manually.')
|
|
if (not brewInstalled) and (not InstallerYesOrNo('Homebrew is not installed: continue with manual installation?', default=False)):
|
|
raise Exception(f'Follow the steps at {HOMEBREW_INSTALL_URLS[self.platform]} to install Homebrew, then re-run {ScriptName}')
|
|
|
|
if self.useBrew:
|
|
# make sure we have brew cask
|
|
err, out = self.run_process(['brew', 'info', 'cask'])
|
|
if (err != 0):
|
|
self.install_package(['cask'])
|
|
if (err == 0):
|
|
if self.debug: eprint('"brew install cask" succeeded')
|
|
else:
|
|
eprint(f'"brew install cask" failed with {err}, {out}')
|
|
|
|
err, out = self.run_process(['brew', 'tap', 'homebrew/cask-versions'])
|
|
if (err == 0):
|
|
if self.debug: eprint('"brew tap homebrew/cask-versions" succeeded')
|
|
else:
|
|
eprint(f'"brew tap homebrew/cask-versions" failed with {err}, {out}')
|
|
|
|
self.checkPackageCmds.append(['brew', 'cask', 'ls', '--versions'])
|
|
self.installPackageCmds.append(['brew', 'cask', 'install'])
|
|
|
|
# determine total system memory
|
|
try:
|
|
totalMemBytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES')
|
|
self.totalMemoryGigs = math.ceil(totalMemBytes/(1024.**3))
|
|
except:
|
|
self.totalMemoryGigs = 0.0
|
|
|
|
# determine total system memory a different way if the first way didn't work
|
|
if (self.totalMemoryGigs <= 0.0):
|
|
err, out = self.run_process(['sysctl', '-n', 'hw.memsize'])
|
|
if (err == 0) and (len(out) > 0):
|
|
totalMemBytes = int(out[0])
|
|
self.totalMemoryGigs = math.ceil(totalMemBytes/(1024.**3))
|
|
|
|
# determine total system CPU cores
|
|
try:
|
|
self.totalCores = os.sysconf('SC_NPROCESSORS_ONLN')
|
|
except:
|
|
self.totalCores = 0
|
|
|
|
# determine total system CPU cores a different way if the first way didn't work
|
|
if (self.totalCores <= 0):
|
|
err, out = self.run_process(['sysctl', '-n', 'hw.ncpu'])
|
|
if (err == 0) and (len(out) > 0):
|
|
self.totalCores = int(out[0])
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
def install_docker(self):
|
|
result = False
|
|
|
|
# first see if docker is already installed/runnable
|
|
err, out = self.run_process(['docker', 'info'])
|
|
|
|
if (err != 0) and self.useBrew and self.package_is_installed(MAC_BREW_DOCKER_PACKAGE):
|
|
# if docker is installed via brew, but not running, prompt them to start it
|
|
eprint(f'{MAC_BREW_DOCKER_PACKAGE} appears to be installed via Homebrew, but "docker info" failed')
|
|
while True:
|
|
response = InstallerAskForString('Starting Docker the first time may require user interaction. Please find and start Docker in the Applications folder, then return here and type YES').lower()
|
|
if (response == 'yes'):
|
|
break
|
|
err, out = self.run_process(['docker', 'info'], retry=12, retrySleepSec=5)
|
|
|
|
# did docker info work?
|
|
if (err == 0):
|
|
result = True
|
|
|
|
elif InstallerYesOrNo('"docker info" failed, attempt to install Docker?', default=True):
|
|
|
|
if self.useBrew:
|
|
# install docker via brew cask (requires user interaction)
|
|
dockerPackages = [MAC_BREW_DOCKER_PACKAGE]
|
|
eprint(f"Installing docker packages: {dockerPackages}")
|
|
if self.install_package(dockerPackages):
|
|
eprint("Installation of docker packages apparently succeeded")
|
|
while True:
|
|
response = InstallerAskForString('Starting Docker the first time may require user interaction. Please find and start Docker in the Applications folder, then return here and type YES').lower()
|
|
if (response == 'yes'):
|
|
break
|
|
else:
|
|
eprint("Installation of docker packages failed")
|
|
|
|
else:
|
|
# install docker via downloaded dmg file (requires user interaction)
|
|
dlDirName = f'/Users/{self.scriptUser}/Downloads'
|
|
if os.path.isdir(dlDirName):
|
|
tempFileName = os.path.join(dlDirName, 'Docker.dmg')
|
|
else:
|
|
tempFileName = os.path.join(self.tempDirName, 'Docker.dmg')
|
|
if DownloadToFile('https://download.docker.com/mac/edge/Docker.dmg', tempFileName, debug=self.debug):
|
|
while True:
|
|
response = InstallerAskForString(f'Installing and starting Docker the first time may require user interaction. Please open Finder and install {tempFileName}, start Docker from the Applications folder, then return here and type YES').lower()
|
|
if (response == 'yes'):
|
|
break
|
|
|
|
# at this point we either have installed docker successfully or we have to give up, as we've tried all we could
|
|
err, out = self.run_process(['docker', 'info'], retry=12, retrySleepSec=5)
|
|
if (err == 0):
|
|
result = True
|
|
if self.debug:
|
|
eprint('"docker info" succeeded')
|
|
|
|
elif (err != 0):
|
|
raise Exception(f'{ScriptName} requires docker edge, please see {DOCKER_INSTALL_URLS[self.platform]}')
|
|
|
|
elif (err != 0):
|
|
raise Exception(f'{ScriptName} requires docker edge, please see {DOCKER_INSTALL_URLS[self.platform]}')
|
|
|
|
# tweak CPU/RAM usage for Docker in Mac
|
|
settingsFile = MAC_BREW_DOCKER_SETTINGS.format(self.scriptUser)
|
|
if result and os.path.isfile(settingsFile) and InstallerYesOrNo(f'Configure Docker resource usage in {settingsFile}?', default=True):
|
|
|
|
# adjust CPU and RAM based on system resources
|
|
if self.totalCores >= 16:
|
|
newCpus = 12
|
|
elif self.totalCores >= 12:
|
|
newCpus = 8
|
|
elif self.totalCores >= 8:
|
|
newCpus = 6
|
|
elif self.totalCores >= 4:
|
|
newCpus = 4
|
|
else:
|
|
newCpus = 2
|
|
|
|
if self.totalMemoryGigs >= 64.0:
|
|
newMemoryGiB = 32
|
|
elif self.totalMemoryGigs >= 32.0:
|
|
newMemoryGiB = 24
|
|
elif self.totalMemoryGigs >= 24.0:
|
|
newMemoryGiB = 16
|
|
elif self.totalMemoryGigs >= 16.0:
|
|
newMemoryGiB = 12
|
|
elif self.totalMemoryGigs >= 8.0:
|
|
newMemoryGiB = 8
|
|
elif self.totalMemoryGigs >= 4.0:
|
|
newMemoryGiB = 4
|
|
else:
|
|
newMemoryGiB = 2
|
|
|
|
while not InstallerYesOrNo(f"Setting {newCpus if newCpus else '(unchanged)'} for CPU cores and {newMemoryGiB if newMemoryGiB else '(unchanged)'} GiB for RAM. Is this OK?", default=True):
|
|
newCpus = InstallerAskForString('Enter Docker CPU cores (e.g., 4, 8, 16)')
|
|
newMemoryGiB = InstallerAskForString('Enter Docker RAM MiB (e.g., 8, 16, etc.)')
|
|
|
|
if newCpus or newMemoryMiB:
|
|
with open(settingsFile, 'r+') as f:
|
|
data = json.load(f)
|
|
if newCpus: data['cpus'] = int(newCpus)
|
|
if newMemoryGiB: data['memoryMiB'] = int(newMemoryGiB)*1024
|
|
f.seek(0)
|
|
json.dump(data, f, indent=2)
|
|
f.truncate()
|
|
|
|
# at this point we need to essentially update our system memory stats because we're running inside docker
|
|
# and don't have the whole banana at our disposal
|
|
self.totalMemoryGigs = newMemoryGiB
|
|
|
|
eprint("Docker resource settings adjusted, attempting restart...")
|
|
|
|
err, out = self.run_process(['osascript', '-e', 'quit app "Docker"'])
|
|
if (err == 0):
|
|
time.sleep(5)
|
|
err, out = self.run_process(['open', '-a', 'Docker'])
|
|
|
|
if (err == 0):
|
|
err, out = self.run_process(['docker', 'info'], retry=12, retrySleepSec=5)
|
|
if (err == 0):
|
|
if self.debug:
|
|
eprint('"docker info" succeeded')
|
|
|
|
else:
|
|
eprint(f"Restarting Docker automatically failed: {out}")
|
|
while True:
|
|
response = InstallerAskForString('Please restart Docker via the system taskbar, then return here and type YES').lower()
|
|
if (response == 'yes'):
|
|
break
|
|
|
|
return result
|
|
|
|
###################################################################################################
|
|
# main
|
|
def main():
|
|
global args
|
|
|
|
# extract arguments from the command line
|
|
# print (sys.argv[1:]);
|
|
parser = argparse.ArgumentParser(description='Malcolm install script', add_help=False, usage=f'{ScriptName} <arguments>')
|
|
parser.add_argument('-v', '--verbose', dest='debug', type=str2bool, nargs='?', const=True, default=False, help="Verbose output")
|
|
parser.add_argument('-m', '--malcolm-file', required=False, dest='mfile', metavar='<STR>', type=str, default='', help='Malcolm .tar.gz file for installation')
|
|
parser.add_argument('-i', '--image-file', required=False, dest='ifile', metavar='<STR>', type=str, default='', help='Malcolm docker images .tar.gz file for installation')
|
|
parser.add_argument('-c', '--configure', dest='configOnly', type=str2bool, nargs='?', const=True, default=False, help="Only do configuration (not installation)")
|
|
parser.add_argument('-f', '--configure-file', required=False, dest='configFile', metavar='<STR>', type=str, default='', help='Single docker-compose YML file to configure')
|
|
parser.add_argument('-d', '--defaults', dest='acceptDefaults', type=str2bool, nargs='?', const=True, default=False, help="Accept defaults to prompts without user interaction")
|
|
parser.add_argument('-l', '--logstash-expose', dest='exposeLogstash', type=str2bool, nargs='?', const=True, default=False, help="Expose Logstash port to external hosts")
|
|
parser.add_argument('-e', '--elasticsearch-expose', dest='exposeElastic', type=str2bool, nargs='?', const=True, default=False, help="Expose Elasticsearch port to external hosts")
|
|
parser.add_argument('-r', '--restart-malcolm', dest='malcolmAutoRestart', type=str2bool, nargs='?', const=True, default=False, help="Restart Malcolm on system restart (unless-stopped)")
|
|
|
|
try:
|
|
parser.error = parser.exit
|
|
args = parser.parse_args()
|
|
except SystemExit:
|
|
parser.print_help()
|
|
exit(2)
|
|
|
|
if args.debug:
|
|
eprint(os.path.join(ScriptPath, ScriptName))
|
|
eprint(f"Arguments: {sys.argv[1:]}")
|
|
eprint(f"Arguments: {args}")
|
|
else:
|
|
sys.tracebacklimit = 0
|
|
|
|
if not ImportRequests(debug=args.debug):
|
|
exit(2)
|
|
|
|
# If Malcolm and images tarballs are provided, we will use them.
|
|
# If they are not provided, look in the pwd first, then in the script directory, to see if we
|
|
# can locate the most recent tarballs
|
|
malcolmFile = None
|
|
imageFile = None
|
|
|
|
if args.mfile and os.path.isfile(args.mfile):
|
|
malcolmFile = args.mfile
|
|
else:
|
|
# find the most recent non-image tarball, first checking in the pwd then in the script path
|
|
files = list(filter(lambda x: "_images" not in x, glob.glob(os.path.join(origPath, '*.tar.gz'))))
|
|
if (len(files) == 0):
|
|
files = list(filter(lambda x: "_images" not in x, glob.glob(os.path.join(ScriptPath, '*.tar.gz'))))
|
|
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
|
if (len(files) > 0):
|
|
malcolmFile = files[0]
|
|
|
|
if args.ifile and os.path.isfile(args.ifile):
|
|
imageFile = args.ifile
|
|
|
|
if (malcolmFile and os.path.isfile(malcolmFile)) and (not imageFile or not os.path.isfile(imageFile)):
|
|
# if we've figured out the malcolm tarball, the _images tarball should match it
|
|
imageFile = malcolmFile.replace('.tar.gz', '_images.tar.gz')
|
|
if not os.path.isfile(imageFile): imageFile = None
|
|
|
|
if args.debug:
|
|
if args.configOnly:
|
|
eprint("Only doing configuration, not installation")
|
|
else:
|
|
eprint(f"Malcolm install file: {malcolmFile}")
|
|
eprint(f"Docker images file: {imageFile}")
|
|
|
|
installerPlatform = platform.system()
|
|
if installerPlatform == PLATFORM_LINUX:
|
|
installer = LinuxInstaller(debug=args.debug, configOnly=args.configOnly)
|
|
elif installerPlatform == PLATFORM_MAC:
|
|
installer = MacInstaller(debug=args.debug, configOnly=args.configOnly)
|
|
elif installerPlatform == PLATFORM_WINDOWS:
|
|
raise Exception(f'{ScriptName} is not yet supported on {installerPlatform}')
|
|
installer = WindowsInstaller(debug=args.debug, configOnly=args.configOnly)
|
|
|
|
success = False
|
|
installPath = None
|
|
|
|
if (not args.configOnly):
|
|
if hasattr(installer, 'install_required_packages'): success = installer.install_required_packages()
|
|
if hasattr(installer, 'install_docker'): success = installer.install_docker()
|
|
if hasattr(installer, 'install_docker_compose'): success = installer.install_docker_compose()
|
|
if hasattr(installer, 'tweak_system_files'): success = installer.tweak_system_files()
|
|
if hasattr(installer, 'install_docker_images'): success = installer.install_docker_images(imageFile)
|
|
|
|
if args.configOnly or (args.configFile and os.path.isfile(args.configFile)):
|
|
if not args.configFile:
|
|
for testPath in [origPath, ScriptPath, os.path.realpath(os.path.join(ScriptPath, ".."))]:
|
|
if os.path.isfile(os.path.join(testPath, "docker-compose.yml")):
|
|
installPath = testPath
|
|
else:
|
|
installPath = os.path.dirname(os.path.realpath(args.configFile))
|
|
success = (installPath is not None) and os.path.isdir(installPath)
|
|
if args.debug:
|
|
eprint(f"Malcolm installation detected at {installPath}")
|
|
|
|
elif hasattr(installer, 'install_malcolm_files'):
|
|
success, installPath = installer.install_malcolm_files(malcolmFile)
|
|
|
|
if (installPath is not None) and os.path.isdir(installPath) and hasattr(installer, 'tweak_malcolm_runtime'):
|
|
installer.tweak_malcolm_runtime(installPath, expose_elastic_default=args.exposeElastic, expose_logstash_default=args.exposeLogstash, restart_mode_default=args.malcolmAutoRestart)
|
|
eprint(f"\nMalcolm has been installed to {installPath}. See README.md for more information.")
|
|
eprint(f"Scripts for starting and stopping Malcolm and changing authentication-related settings can be found in {os.path.join(installPath, 'scripts')}.")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|