既然 Apple 正在放弃 Server.app 中的某些服务(postfix、dovecot、DNS 等等),那么找到解决方案来保持这些服务的运行就变得非常重要。Apple 建议迁移到开源版本,但他们描述迁移的文档还远未完成(例如,邮件服务尚未记录)。
我一直在考虑使用容器添加这些服务。可以在 macOS 上运行 Docker。我已经能够通过 homebrew 安装和使用 Docker,使用 Virtualbox 作为虚拟机管理程序提供程序。
但是,在任何人登录之前,我无法在启动时启动 docker 机器。这样的启动对于 macOS 服务器在 Docker 下保留其服务是必要的
LaunchDaemon 应该可以解决问题。Homebrew 甚至可以管理 launchd .plist
,或者您可以手动创建一个。
但是,虽然我可以手动启动虚拟机,但我无法通过 launchctl 启动它。似乎有一次 macOS(在我的情况下是 High Sierra)拒绝接受我尝试启动的内容未经代码签名的事实。这很奇怪,因为我也在某些系统上运行 Duplicati、nginx、minio,这些都可以运行。我可以使用 克服这个障碍codesign -s - /usr/local/opt/docker-machine/bin/docker-machine
。但它仍然无法启动服务。
[更新:codesign 只是个幌子。即使程序已签名且错误消失(codesign -s - <binary>
),仍然无法从 launchd 启动 docker,更不用说在启动时启动了。]
有没有任何如何让 docker 机器(带有一些服务)在 macOS 启动时启动?
答案1
是的,这是可能的。根本问题是,当 launchd 运行 docker-machine 命令时,VirtualBox kexts 尚未加载。Launched 没有良好的依赖系统。因此,我创建了一个脚本,该脚本会在间隔后(直到最大时间)检查并重试检查,并且仅在 VirtualBox 存在时启动 docker-machine。它由包含要启动的机器信息的 JSON 文件驱动。
目前仍在开发中(我需要完成一些事情)但这里有一个 plist 的示例:
<?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>
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。该脚本可以启动和停止 docker 机器。
#!/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 之外的更多 VM 提供程序(只需添加它并添加测试以查看它是否已加载)。