Теперь, когда Apple отказывается от определенных сервисов в Server.app (postfix, dovecot, DNS, и многих других), важно найти решения, чтобы они продолжали работать. Apple предлагает перейти на версии с открытым исходным кодом, но их документ, описывающий переход, далек от завершения (например, почтовые службы еще не задокументированы).
Я думал добавить эти службы с помощью контейнеров. Docker можно запустить на macOS. Мне удалось установить и использовать Docker через homebrew, используя Virtualbox в качестве поставщика гипервизора.
Однако мне не удалось запустить докер-машину во время загрузки до того, как кто-либо войдет в систему. Такой запуск необходим, чтобы сервер macOS сохранил свои службы под Docker.
LaunchDaemon должен помочь. Homebrew даже может управлять запуском .plist
или вы можете создать его вручную.
Но хотя я могу запустить виртуальную машину вручную, я не могу запустить ее через launchctl. В какой-то момент, казалось, произошло то, что macOS (High Sierra в моем случае) возмутилась тем фактом, что то, что я пытаюсь запустить, не было подписано кодом. Что странно, потому что я также запускаю Duplicati на некоторых системах, nginx, minio, а они просто запускаются. Я мог бы преодолеть это препятствие с помощью codesign -s - /usr/local/opt/docker-machine/bin/docker-machine
. Но он все равно не запустит сервис.
[ОБНОВЛЕНИЕ: codeign - отвлекающий маневр. Даже если программы подписаны и ошибки исчезнут (codesign -s - <binary>
) по-прежнему невозможно запустить docker из launchd, не говоря уже о загрузке.]
Здесь любой как я могу запустить докер-машину (с некоторыми службами) во время загрузки на macOS?
Да, это возможно. Существенная проблема заключалась в том, что VirtualBox kexts не загружался при запуске команды docker-machine с помощью launchd. В Launchd нет хорошей системы зависимостей. Итак, я создал скрипт, который проверяет и повторяет проверку через определенные промежутки времени (до максимального времени) и запускает докер-машину только при наличии VirtualBox. Он управляется файлом JSON, который содержит информацию о машинах, которые нужно запустить.
В настоящее время все еще находится в разработке (мне нужно выполнить несколько вещей), но вот пример списка:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin</string>
</dict>
<key>Label</key> <string>nl.rna.docker-machines.manage</string>
<key>ProgramArguments</key>
<array>
<string>/Users/gerben/RNAManageDockerMachines.py</string>
<string>/Users/gerben/RNAManagedDockerMachines.json</string>
<string>--maxwait</string>
<string>60</string>
<string>-vvvv</string>
<string>start</string>
</array>
<key>Disabled</key> <false/>
<key>LaunchOnlyOnce</key> <true/>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <false/>
<key>StandardOutPath</key>
<string>/Library/Logs/rnamanagedocker_out.log</string>
<key>StandardErrorPath></key>
<string>/Library/Logs/rnamanagedocker_err.log</string>
</dict>
</plist>
Список - это LaunchOnlyOnce (своего рода элемент запуска). Конфигурационный JSON:
{
"sysbh-default": {
"displayname": "Sysbh's default docker machine",
"vmservice": "virtualbox",
"user": "sysbh",
"workingdir": "/Users/sysbh",
"machinename": "default",
"enabled": true
},
"gerben-lunaservices": {
"displayname": "Gerben's lunaservices docker machine",
"vmservice": "vmware",
"user": "gerben",
"workingdir": "/Users/gerben",
"machinename": "lunaservices",
"enabled": false
}
}
Как видите, JSON может содержать несколько определений.
И сценарий. Я использую python 3.7, установленный на домашнем пиве. Скрипт может запускать и останавливать докеры.
#!/usr/local/bin/python3
import sys
import os
import pwd
import subprocess
import argparse
import textwrap # Required for 3.7
import json
import time
DOCKERMACHINECOMMAND='/usr/local/opt/docker-machine/bin/docker-machine'
VERSION="1.0beta1"
AUTHOR="Gerben Wierda (with lots of help/copy from stackexchange etc.)"
LICENSE="Free under BSD License (look it up)"
STANDARDRETRY=15
from argparse import RawDescriptionHelpFormatter
class SmartDescriptionFormatter(argparse.RawDescriptionHelpFormatter):
#def _split_lines(self, text, width): # RawTextHelpFormatter, although function name might change depending on Python
def _fill_text(self, text, width, indent): # RawDescriptionHelpFormatter, although function name might change depending on Python
if text.startswith('R|'):
paragraphs = text[2:].splitlines()
# Next line for 3.7 adapted from the StackExchange version to use textwrap module
rebroken = [textwrap.wrap(tpar, width) for tpar in paragraphs]
# 2.7: rebroken = [argparse._textwrap.wrap(tpar, width) for tpar in paragraphs]
rebrokenstr = []
for tlinearr in rebroken:
if (len(tlinearr) == 0):
rebrokenstr.append("")
else:
for tlinepiece in tlinearr:
rebrokenstr.append(tlinepiece)
#print(rebrokenstr)
return '\n'.join(rebrokenstr) #(argparse._textwrap.wrap(text[2:], width))
# this is the RawTextHelpFormatter._split_lines
#return argparse.HelpFormatter._split_lines(self, text, width)
return argparse.RawDescriptionHelpFormatter._fill_text(self, text, width, indent)
parser = argparse.ArgumentParser( formatter_class=SmartDescriptionFormatter,
description=(
"R|Start Docker VMs with docker-machine at macOS boot. This program reads one or\n"
"more JSON files that define docker machines, including which VM provider to\n"
"use (currently only VirtualBox is supported), as what user the machine must be\n"
"started, the working directory to go to before starting or stopping a machine,\n"
"and the name of the docker machine. Example:\n") +
"""
{
\"john-default\": {
\"displayname\": \"John's default docker machine\",
\"vmservice\": \"virtualbox\", # VM provider to use
\"user": \"sysbh\", # User to run as
\"workingdir\": \"/Users/john\", # Dir to cd to before running docker-machine
\"machinename\": \"default\", # Docker machine name
\"enabled\": true # Set to false to ignore entry
},
\"gerben-lunaservices\": {
\"displayname\": \"Gerben's lunaservices docker machine\",
\"vmservice\": \"vmware\", # Not implemented in this version
\"user\": \"gerben\",
\"workingdir\": \"/Users/gerben\",
\"machinename\": \"lunaservices\",
\"enabled\": false
}
}\n
""" +
"This script was written by: " + AUTHOR +
"\nThis is version: " + VERSION +
"\n" + LICENSE +
"\nThe command used is: " + DOCKERMACHINECOMMAND)
parser.add_argument( "-v", "--verbosity", action="count", default=0,
help="Increase output verbosity (5 is maximum effect)")
parser.add_argument( "--maxwait", type=int, choices=range(0, 601), default=0,
metavar="[0-600]",
help=("Maximum wait time in seconds for VM provider to become available (if missing)."
" The program will retry every 20 seconds until the required VM provider"
" becomes available or the maximum wait time is met. Note that this is implemented"
" per VM provider so in the worst case the program will try for number of"
" providers times the maximum wait time. This argument is ignored"
" when the action is not 'start'."))
parser.add_argument( "--only", nargs="*", dest="VMDeclarations_Machines_Subset",
metavar="machine",
help="Restrict actions to these machine names only. Not yet implemented.")
parser.add_argument( "VMDeclarations_files", metavar="JSON_file", nargs="+",
help=("JSON file(s) with Docker Machine launch definitions."
" See description above."))
parser.add_argument( "action", choices=['start','stop'], nargs=1,
help=("Action that is taken. Either start or stop the machine(s)."))
scriptargs = parser.parse_args()
PROGNAME=sys.argv[0]
VERBOSITY=scriptargs.verbosity
# Add VM providers here
vmservices = {'virtualbox':False}
def log( message):
print( "[" + PROGNAME + " " + time.asctime() + "] " + message)
def CheckVMProvider( vmservice):
if vmservice == 'virtualbox':
if vmservices['virtualbox']:
return True
waited=0
while waited <= scriptargs.maxwait:
p1 = subprocess.Popen( ["kextstat"], stdout=subprocess.PIPE)
p2 = subprocess.Popen( ["grep", "org.virtualbox.kext.VBoxNetAdp"], stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
if p2.wait() == 0:
vmservices['virtualbox'] = True
return True
waited = waited + STANDARDRETRY
if waited < scriptargs.maxwait:
if VERBOSITY > 1: log( "Virtual machine provider " + vmservice + " is not (yet) available. Sleeping " + str(STANDARDRETRY) + "sec and retrying...")
time.sleep( STANDARDRETRY)
else:
if VERBOSITY > 1: log( "Virtual machine provider " + vmservice + " is not available. Giving up.")
else:
if VERBOSITY > 1: log( "Virtual machine provider " + vmservice + " is not supported.")
return False
def report_ids( msg):
if VERBOSITY > 4: print( "[" + PROGNAME + "] " + 'uid, gid = %d, %d; %s' % (os.getuid(), os.getgid(), msg))
def demote( user_uid, user_gid):
def result():
report_ids( 'starting demotion')
os.setgid( user_gid)
os.setuid( user_uid)
report_ids( 'finished demotion')
return result
def manageDockerMachine( entryname, definition):
displayname = definition['displayname']
enabled = definition['enabled']
user = definition['user']
workingdir = definition['workingdir']
vmservice = definition['vmservice']
machinename = definition['machinename']
pw_record = pwd.getpwnam( user)
username = pw_record.pw_name
homedir = pw_record.pw_dir
uid = pw_record.pw_uid
gid = pw_record.pw_gid
env = os.environ.copy()
env['HOME'] = homedir
env['USER'] = username
env['PWD'] = workingdir
env['LOGNAME'] = username
dmargs = [DOCKERMACHINECOMMAND, scriptargs.action[0], machinename]
if enabled:
if VERBOSITY > 2: log( "Starting " + vmservice + " docker machine " + machinename + " for user " + username)
if not CheckVMProvider( vmservice):
log( "Virtual machine provider " + vmservice + " not found. Ignoring machine definition " + '"' + machinename + '".')
return False
report_ids('starting ' + str( dmargs))
process = subprocess.Popen( dmargs, preexec_fn=demote(uid, gid),
cwd=workingdir,env=env)
result = process.wait()
report_ids( 'finished ' + str(dmargs))
else:
if VERBOSITY > 3: log( "Ignoring disabled " + vmservice + " docker machine " + machinename + " of user " + user)
return True
for file in scriptargs.VMDeclarations_files:
if VERBOSITY > 1: log( "Processing VM declaration file: " + file)
filedescriptor = open( file, 'r')
machinedefinitions = json.load( filedescriptor)
if VERBOSITY > 4: print( json.dumps( machinedefinitions, sort_keys=True, indent=4))
for machinedefinitionname in list( machinedefinitions):
manageDockerMachine( machinedefinitionname, machinedefinitions[machinedefinitionname])
Еще нужно сделать пару вещей. Например. флаг --only еще не реализован. Макет. Скрипт готов для большего количества поставщиков виртуальных машин, чем VirtualBox (просто добавьте его и добавьте тест, чтобы увидеть, загружен ли он).