web-dev-qa-db-ja.com

macOS(サーバー)の起動時にDockerマシンを起動することは可能ですか?

AppleがServer.appで特定のサービス(postfix、dovecot、DNSなど)を削除しているので、これらを実行し続けるためのソリューションを見つけることが重要です。Appleは、オープンソースバージョンへの移行を提案していますが、移行について説明しているドキュメントは完全にはほど遠いです(たとえば、メールサービスはまだドキュメント化されていません)。

コンテナを使ってこれらのサービスを追加することを考えていました。 macOSでDockerを実行することは可能です。 Virtualboxをハイパーバイザープロバイダーとして使用して、自作でDockerをインストールして使用することができました。

ただし、誰かがログインする前に、起動時にDockerマシンを起動できませんでした。このような起動は、macOSServerがDockerでサービスを保持するために必要です。

LaunchDaemonがそのトリックを実行するはずです。 Homebrewは、起動された.plistを管理することも、手動で作成することもできます。

しかし、VMを手動で開始することはできますが、launchctlを介して開始することはできません。ある時点で起こったように見えたのは、macOS(私の場合はHigh Sierra)が私が何であるかという事実に呆然としました。起動しようとしてもコード署名されていませんでした。これは奇妙なことです。一部のシステム、nginx、minioでもDuplicatiを実行していて、これらは実行されているだけです。codesign -s - /usr/local/opt/docker-machine/bin/docker-machineでこのハードルを通過できました。しかし、それでも起動しません。サービス。

[更新:codesignは赤いニシンです。プログラムに署名してエラーがなくなっても(codesign -s - <binary>)、起動時はもちろん、launchdからdockerを起動することはできません。]

MacOSで起動時にDockerマシン(一部のサービスを含む)を起動させる方法はありますか?any

2
gctwnl

はい、可能です。本質的な問題は、docker-machineコマンドがlaunchdによって実行されたときにVirtualBoxkextsがロードされていなかったことでした。 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(ある種のstartupitem)です。構成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を使用しています。スクリプトはdedockerマシンを起動および停止できます。

#!/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プロバイダーの準備ができています(スクリプトを追加し、テストを追加して、ロードされているかどうかを確認してください)。

1
gctwnl