Вики IT-KB

Пошаговые руководства, шпаргалки, полезные ссылки...

Инструменты пользователя

Инструменты сайта


apple-mac-os:macos-sequoia:automated-os-deployment-to-apple-mac-and-iphone-or-ipad-mobile-devices

Автоматизированное развёртывание OC на Apple Mac и мобильные устройства iPhone или iPad

Идея подсмотрена на Российской ежегодной конференции системных администраторов Apple.

Реализация в целом похожа, в представленном варианте используется cfgutil, который запускает скрипт AppleScript с логикой обработки подключенных Mac, iPhone или iPad. Последний доступный образ восстановления при необходимости скачивается автоматически. Так же выполняется логирование для понимания процесса. Для перевода Mac в DFU при условии правильного подключения используется macvdmtool. iPhone и iPad, само собой, переводим в DFU в ручном режиме.


Основная логика

Скрипт Restore.scpt разместим здесь /Library/Application Support/Custom/RestoreDevices/

-- В скрипте используется утилита jq, которая встроена в macOS начиная с Sequoia.
-- При использовании более ранних версий, jq нужно установить дополнительно.
-- Перед использованием с AppStore установить Apple Configurator 2, и выполнить установку средств автоматизации из меню.
 
--Объявляем глобальную переменную для работы в обработчиках
global LogPath
set LogPath to "/tmp/DevsRestore/logs/"
 
 
-- Получаем список подключенных устройств с помощью cfgutil
set cfgutilResult to my cfgutilDevList()
-- Получаем список подключенных устройств с помощью system_profiler
set SPResult to my systemprofilerDevList()
-- Объединяем списки для получения требуемой инф. в одной списке
set MergedList to my MergeLists(cfgutilResult, SPResult)
--return MergedList
 
-- Проходим по каждому устройству в подписке списка
repeat with i from 1 to count MergedList
  -- Берём первое устройство
  set thisDevice to item i of MergedList
  -- Записываем атрибуты устройства
  set DeviceType to item 1 of thisDevice
  set DeviceClass to item 2 of thisDevice
  set SerialNumber to item 3 of thisDevice
  set ECID to item 4 of thisDevice
  set LocationID to item 5 of thisDevice
  -- Формируем имя Log-файла
  set LogFileName to ECID & ".log"
 
  --Логируем найденное устройство
  my WriteLog("Найдено устройство:" & space & DeviceType & space & "серийный номер:" & space & SerialNumber & "," & space ¬
    & "ECID:" & space & ECID & space & "в порте:" & space & LocationID, LogFileName)
 
  -- Выполняем подготовку  
  -- Формируем ссылку для получения доступных IPSW
  set ChooseUrl to "https://api.ipsw.me/v4/device/" & DeviceType & "?type=ipsw"
  --Логируем
  my WriteLog("Получаем информацию о последней версии ОС по ссылке:" & space & ChooseUrl, LogFileName)
  -- Получаем информацию о последней доступной версии ОС
  set BaseJSON to my GetJSON(ChooseUrl, LogFileName)
  -- Парсим JSON, если он был получен
  if BaseJSON is false then return
  -- Получаем контрольную сумму из JSON
  set CloudCheckSum to do shell script "echo" & space & quoted form of BaseJSON & space & "| jq -r '.sha1sum'"
  -- Получаем ссылку для скачивания "образа" IPSW из JSON
  set IPSWUrl to do shell script "echo" & space & quoted form of BaseJSON & space & "| jq -r '.url'"
  -- Получаем имя файла из JSON
  set CloudIPSWName to do shell script "basename" & space & quoted form of IPSWUrl
  -- Указываем родительский каталог для сохранения "образов" IPSW
  set IPSWRootPath to (POSIX path of "/Library/Application Support/Custom/RestoreDevices/IPSWs/")
  -- Формируем путь до файла "образа" IPSW 
  set CurrentIPSWPath to (IPSWRootPath & DeviceType & "/" & CloudIPSWName)
  -- Если файл существует, сверяем контрольные суммы.
  -- Если файла нет -- скачиваем. Скачивание файла может занимать длительное время.
  set SelectIPSWStatus to my SelectIPSW(CloudCheckSum, CurrentIPSWPath, IPSWRootPath, DeviceType, IPSWUrl, LogFileName)
  -- При возникновении ошибки процесс останавливается
  if SelectIPSWStatus is not true then return
  -- запускаем восстановление
  my Restore(LocationID, ECID, DeviceClass, CurrentIPSWPath, LogFileName)
 
end repeat
 
 
 
--===================
--Обработчики
--===================
 
-- Проверить или скачать IPSW
--===================
on SelectIPSW(TrustedCheckSum, thisFile, RootFolder, SubFolder, thisURL, LogFileName)
  -- Создаём каталоги для загрузки IPSW, если необходимо
  my CreateFolder(RootFolder, SubFolder, LogFileName)
  -- Проверяем контрольную сумму существующего файла
  set StatusCheckSum to my CheckSum(TrustedCheckSum, thisFile, LogFileName)
  if StatusCheckSum is true then
    return true
  else
    -- Скачиваем
    set GetIPSWStatus to my GetIPSW(thisFile, thisURL, TrustedCheckSum, LogFileName)
    if GetIPSWStatus is true then
      return true
    else
      return false
    end if
  end if
end SelectIPSW
--===================
 
 
 
-- Получить JSON
--===================
on GetJSON(thisURL, LogFileName)
  -- Запрос всех доступных IPSW для подключенной модели  
  set thisJSON to do shell script "curl" & space & thisURL & space & "| jq '.firmwares'"
  -- Если возврат "null", то нет доступных IPSW
  if thisJSON is "null" then
    my WriteLog("Нет доступных IPSW. Остановка процесса.", LogFileName)
    return false
  else
    -- JSON получен, сортируем элементы массива по дате релиза и берём последний
    set thisJSON to do shell script "echo" & space & quoted form of thisJSON & space & "| jq -r 'sort_by(.releasedate) | last'"
    my WriteLog("Ответ сервера в формате JSON" & linefeed & thisJSON, LogFileName)
 
    return thisJSON
 
  end if
 
end GetJSON
--===================
 
 
-- Скачать IPSW
--===================
on GetIPSW(thisFile, thisURL, TrustedCheckSum, LogFileName)
  -- Поиск параллельных процессов скачивания IPSW образов
  set UserName to short user name of (system info)
  set PSShellCommand to "ps -u" & space & UserName & space & "| grep '[c]url' | grep" & space & quoted form of thisURL & space & "> /dev/null 2>&1; echo $?"
  set CurrentDownloads to (do shell script PSShellCommand) as integer
 
  -- Если процесс curl был найден, возврат 0
  if CurrentDownloads is 0 then
    -- Устанавливаем счётчик повторных запусков на 0
    set Counter to 0
    -- Повторяем цикл 5 раз
    repeat while Counter < 6
      my WriteLog("Обнаружено параллельное скачивание файла по ссылке" & space & thisURL & linefeed & "Проверим ещё раз через 10 минут.", LogFileName)
      -- пауза в секундах
      delay 600
      -- Добавляем к счётчику 1 выполненный проход
      set Counter to Counter + 1
      -- Проверяем ещё раз
      set CurrentDownloads to (do shell script PSShellCommand) as integer
      -- Если очередная проверка не обнаружила параллельный процесс, считаем что он завершён
      if CurrentDownloads is 1 then
        -- Процесс завершён, можно продолжать
        my WriteLog("Параллельное скачивание завершено. Повторно выполним проверку контрольной суммы.", LogFileName)
        set StatusCheckSum to my CheckSum(TrustedCheckSum, thisFile, LogFileName)
        if StatusCheckSum is true then
          return true
        else
          my WriteLog("Возможно, параллельный процесс загрузки был завершён некорректно", LogFileName)
          my WriteLog("Попробуйте в ручную загрузить файл по ссылке" & linefeed & thisURL, LogFileName)
          my WriteLog("Остановка процесса.", LogFileName)
          return false
        end if
        exit repeat
      end if
      -- При достижении счётчика, останавливаем процесс
      if Counter is 5 then
        my WriteLog("За последний час процесс загрузки образа не был завершён или есть зависший процесс curl.", LogFileName)
        my WriteLog("Остановка процесса.", LogFileName)
        return false
      end if
 
    end repeat
 
    -- Если процесс curl не был найден, возврат 1
  else if CurrentDownloads is 1 then
    set curlShellCommand to "curl -L --retry 5 --retry-delay 10 --retry-all-errors -J -o" & space & quoted form of thisFile & space & thisURL
    my WriteLog("Запускаем скачивание:" & linefeed & curlShellCommand, LogFileName)
    try
      do shell script curlShellCommand
      my WriteLog("Файл скачен", LogFileName)
      return true
    on error errormsg number errNumber
      my WriteLog("Проблема скачивания файла. Curl вернул ошибку:" & space & errNumber, LogFileName)
      my WriteLog("Коды возврата можно посмотреть в \"man curl\" в разделе \"EXIT CODES\"", LogFileName)
      my WriteLog("Остановка процесса.", LogFileName)
      return false
    end try
  end if
end GetIPSW
--===================
 
 
--===================
-- Записать лог
on WriteLog(message, logFile)
 
  --do shell script "mkdir -p" & space & quoted form of POSIX path of (do shell script "dirname" & space & quoted form of LogPath)
  do shell script "mkdir -p" & space & LogPath
  set timeStamp to (do shell script "date +'%Y-%m-%d %H:%M:%S'")
  set logMessage to timeStamp & space & "-" & space & message & linefeed
  do shell script "echo" & space & quoted form of logMessage & space & ">>" & space & quoted form of (LogPath & logFile)
 
end WriteLog
--===================
 
 
--===================
--Создать каталог для хранения IPSW этой модели
on CreateFolder(RootFolder, SubFolder, LogFileName)
 
  -- Формируем полный путь к каталогу
  set targetFolder to RootFolder & SubFolder
  -- Проверяем существование каталога
  try
    -- Классические методы проверки существования каталога или файла используя вызов "System Events"
    -- не могут быть использованы, т.к. задача запускает от RootFolder, без GUI.
    do shell script "test -d" & space & quoted form of targetFolder
    my WriteLog("Каталог для IPSW файла уже существует:" & space & targetFolder, LogFileName)
  on error
    -- Создать каталог
    do shell script "mkdir -p" & space & quoted form of targetFolder
    my WriteLog("Создан каталог для сохранения IPSW файла:" & space & targetFolder, LogFileName)
  end try
 
end CreateFolder --===================
 
 
--===================
--Сверка контрольной суммы
on CheckSum(TrustedCheckSum, thisFile, LogFileName)
 
  -- Проверяем существование файла
  try
    -- Классические методы проверки существования каталога или файла используя вызов "System Events"
    -- не могут быть использованы, т.к. задача запускает от root, без GUI.
    do shell script "test -f" & space & quoted form of thisFile
  on error
    return false
  end try
 
  -- Файл существует, продолжаем проверку
  my WriteLog("Начинаем проверку контрольной суммы файла" & space & thisFile, LogFileName)
 
  -- Вычисляем контрольную сумму
  set thisCheckSum to word 1 of (do shell script "shasum -a 1" & space & quoted form of thisFile)
  if TrustedCheckSum is equal to thisCheckSum then
    my WriteLog("Контрольные суммы совпадают", LogFileName)
    return true
  else
    my WriteLog("Контрольные суммы не совпадают:" & space & TrustedCheckSum & space & "≠" & space & thisCheckSum, LogFileName)
    return false
  end if
 
end CheckSum
--===================
 
 
--===================
--Перезагрузка в DFU
on RebootToDFU(LocationID, DeviceCalss, LogFileName)
 
  -- Список DFU портов.
  -- Обычно DFU порт хоста имеет идентификатор "0x100000", но зная Apple, в других модeлях это может быть изменено.  
  set DFUPortList to {"0x100000"}
  set macvdmtool to "/usr/local/bin/macvdmtool"
  set macvdmtoolShellCommand to macvdmtool & space & "dfu"
  -- Проверяем существование macvdmtool 
  try
    -- Классические методы проверки существования каталога или файла используя вызов "System Events"
    -- не могут быть использованы, т.к. задача запускает от root, без GUI.
    do shell script "test -f" & space & macvdmtool
 
    -- Если подключенный Mac подключен в DFU порт хоста
    if LocationID is in DFUPortList then
      my WriteLog("Попытка перезагрузить подключенный" & space & DeviceCalss & space & "в DFU режим" & linefeed & macvdmtoolShellCommand, LogFileName)
      try
        set macvdmtoolresult to do shell script macvdmtoolShellCommand
        my WriteLog("Лог утилиты macvdmtool:" & linefeed & macvdmtoolresult, LogFileName)
        my WriteLog("Mac" & space & DeviceCalss & space & "перезагружен в DFU режим", LogFileName)
      on error errormsg
        my WriteLog("Этот" & space & DeviceCalss & space & "не может быть переведён в DFU режим." & linefeed ¬
          & "Убедитесь что это Mac с процессором Apple, кабель подключен в DFU порт и не используется кабель Thunderbol 3/4.", LogFileName)
        my WriteLog("Лог утилиты macvdmtool:" & linefeed & errormsg, LogFileName)
        my WriteLog("Остановка процесса.", LogFileName)
 
        return false
 
      end try
    end if
  on error
    my WriteLog("Утилита macvdmtool отсутствует. Остановка процесса.", LogFileName)
 
    return false
 
  end try
 
end RebootToDFU
--===================
 
 
--===================
--Восстановление с DFU
on Restore(LocationID, ECID, DeviceCalss, PathToIPSW, LogFileName)
 
  if (DeviceCalss contains "Mac") then
    -- Выполняем попытку перезагрузки в DFU режим
    set DFUStatus to my RebootToDFU(LocationID, DeviceCalss, LogFileName)
    if DFUStatus is false then return
  end if
  -- Запуск восстановления в фоновом режиме
  set cfgutilShellCommand to "/usr/local/bin/cfgutil --progress --verbose -e" & space & ECID & space & "restore -I" & space & quoted form of PathToIPSW & space & ">>" & space & LogPath & ECID & ".log 2>&1 &"
  WriteLog("Запуск процесса восстановления:" & linefeed & cfgutilShellCommand, LogFileName)
  WriteLog("Лог работы cfgutil:" & linefeed, LogFileName)
  do shell script cfgutilShellCommand
 
end Restore
--===================
 
--===================
--Поиск подключенных устройств с помощью cfgutil
on cfgutilDevList()
 
  -- Получаем инф. о подключенных устройствах в формате JSON
  set BaseJSON to do shell script "/usr/local/bin/cfgutil --format JSON list | jq"
  -- Парсим JSON, выбирая нужные данные и сохраняем контент как CVS, разделитель ";"
  set ParsedCSV to do shell script "echo" & space & quoted form of BaseJSON & space & "| jq -r '.Output | to_entries[] | [.value.locationID, .value.ECID, .value.deviceType] | join(\";\")'"
  -- Разбиваем на строки
  set LineCSV to paragraphs of ParsedCSV
 
  -- Пустой итоговый список
  set ResultList to {}
 
  -- Цикл обработки строк
  repeat with thisDev in LineCSV
    -- Используем разделитель ";" для работы с CSV
    set text item delimiters to ";"
    -- Развиваем строку на части разделителем установленным разделителем
    set CSVParts to text items of thisDev
    -- Сбрасываем разделитель
    set text item delimiters to ""
 
    -- Извлекаем значения
    -- Первая часть Location ID
    set LocationID to item 1 of CSVParts
    -- Вторая часть ECID
    set ECID to item 2 of CSVParts
    -- Третья часть DeviceType
    set DeviceType to item 3 of CSVParts
 
    -- Модифицируем полученные данные приводя в нужный вид
    -- при использовании вывода инф. о девайсах в не текстовом формате, то используется целочисленное значение Location ID.
    -- Конвертируем в HEX 1048576 -> 0x100000
    set LocationID to "0x" & (do shell script "printf '%X' " & LocationID)
 
    -- Выполняем проверку существования log-файла с именем ECID устройства,
    -- Если лог есть, то считаем, что запускать повторно процедуру восстановления не нужно. Не добавляем в список результатов.
    try
      do shell script "test -f" & space & quoted form of (LogPath & ECID & ".log")
    on error
      -- Добавляем в результат
      set end of ResultList to {DeviceType, ECID, LocationID}
    end try
  end repeat
 
  return ResultList
 
  --Пример вывода
  --{{"iPhone8,4", "0x00480000000E4", "0x8320000"}, {"Macmini9,1", "0x928200000001E", "0x100000"}, {"iPhone7,2", "0x19610000000026", "0x8310000"}}
 
end cfgutilDevList
--===================
 
--===================
--Поиск подключенных устройств с помощью system_profiler
on systemprofilerDevList()
  --Ищем среди подключенных USB устройств имена в которых содержится Mac, DFU или Recovery. 
  -- Отображаем найденные строки, а так же 5 и 8 строку для отображения Serial Number и 
  -- Location. Location необходим в качестве ключа для объединения со списком cfgutilDevList()
  set SPUSBInfo to do shell script "system_profiler SPUSBDataType | awk '{
          lines[NR] = $0
      } 
      /Mac|DFU|Recovery/ { 
          mac_dfu_recovery_line = NR
           mac_dfu_recovery_found = 1
      } 
      /Serial Number:/ &&  mac_dfu_recovery_found && (NR == mac_dfu_recovery_line + 5) { 
          serial_number_line = NR 
      } 
      /Location ID:/ &&  mac_dfu_recovery_found && (NR == mac_dfu_recovery_line + 8) {
          if ( mac_dfu_recovery_found && serial_number_line && NR == mac_dfu_recovery_line + 8) {
              sub(/^ +/, \"\", lines[mac_dfu_recovery_line])
              sub(/^ +/, \"\", lines[serial_number_line])
              sub(/^ +/, \"\", $0)
 
              print lines[mac_dfu_recovery_line]
              print lines[serial_number_line]
              print $0
 
               mac_dfu_recovery_found = 0
              serial_number_line = 0
          }
      }'"
 
  -- Разделяем вывод на строки
  set SPUSBInfoLines to paragraphs of SPUSBInfo
 
  -- Пустой итоговый список
  set ResultList to {}
 
  -- Извлекаем значения проходя по каждой строке с шагом 3
  repeat with i from 1 to count of SPUSBInfoLines by 3
    set Model to item i of SPUSBInfoLines
    set Serial to item (i + 1) of SPUSBInfoLines
    set LocationID to item (i + 2) of SPUSBInfoLines
 
    -- Модифицируем полученные данные приводя в нужный вид
    --Последний символ строки ":", уберём это
    set Model to text 1 thru -2 of Model
    -- Серийный номер подключенный Mac отображается сразу, у iOS строка содержит множество другой информации
    -- Mac загруженный в DFU режиме не отображает серийный номер.
    set Serial to do shell script "echo" & space & Serial & space & "| awk '
                         /Serial Number:/ {
                      value = substr($0, index($0, \"Serial Number:\") + 15);
                      if (value ~ /^[0-9A-Za-z]{6,}/) {
                          print value;
                          found = 1;
                      }
                  }
                  /SRNM:/ {
                      split($0, arr, /SRNM:\\[|\\]/);
                      if (length(arr) > 1) {
                          print arr[2];
                          found = 1;
                      }
                      next;
                  }
                  END {
                      if (found == 0) print \"Serial Number not found\";
                  }
              '"
 
    (*
      Меняем формат Location, т.к. у system_profiler и cfgutil они отличаются
      Пример
      0x00100000 --> 0x100000
      0x08310000 --> 0x8310000
      Поэтому конвертируем 0x08310000 в 8310000 и добавляем 0x спереди, чтобы значение соответствовало формату cfgutil
  *)
    --Вырезаем значение из строки
    set LocationID to text 14 thru 23 of LocationID
    set LocationID to "0x" & (do shell script "printf '%X' " & LocationID)
 
    -- Добавляем инф о каждом устройстве в подсписке в список
    set end of ResultList to {Model, Serial, LocationID}
  end repeat
 
  -- Выводим итоговый список
  return ResultList
 
  --Пример вывода
  --{{"Mac mini", "H2000000Q6NY", "0x100000"}, {"Apple Mobile Device (Recovery Mode)", "DX6000000TVL", "0x8320000"}, {"Apple Mobile Device (Recovery Mode)", "F170000005MR", "0x8310000"}}
 
end systemprofilerDevList
--===================
 
-- Обработчик для объединения списков
--https://www.macscripter.net/t/merge-2-arrays-by-key/76457/10
--===================
on MergeLists(List1, List2)
  -- Создаём пустой итоговый список
  set ResultList to {}
 
  -- Проходим по первому списку
  repeat with subList1 in List1
    -- Извлекаем значения подсписков из первого списка "List1" в подсписок "subList1"
    set {Value01, Value02, ID01} to subList1
 
    -- Проходим по второму списку
    repeat with subList2 in List2
      -- Извлекаем значения подсписков из второго списка "List2" в подсписок "subList2"
      set {Value11, Value12, ID11} to subList2
      --Если ключевые значения равны, создаём итоговый новый список
      if ID01 is equal to ID11 then
        set end of ResultList to {Value01, Value11, Value12, Value02, ID01}
        exit repeat
      end if
    end repeat
  end repeat
 
  -- Выводим итоговый список
  return ResultList
 
end MergeLists
--===================


Создание LaunchDaemon

Создадим Launch Daemon

sudo nano /Library/LaunchDaemons/com.custom.restoredevices.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>Label</key>
	<string>custom.restoredevices</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/local/bin/cfgutil</string>
		<string>exec</string>
		<string>--on-attach</string>
		<string>osascript '/Library/Application Support/Custom/RestoreDevices/Restore.scpt'</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StandardErrorPath</key>
	<string>/tmp/custom.restoredevices.stderr</string>
	<key>StandardOutPath</key>
	<string>/tmp/custom.restoredevices.stdout</string>
</dict>
</plist>

Убедимся, что plist не содержит ошибок

plutil -lint /Library/LaunchDaemons/com.custom.restoredevices.plist

Загрузим и запустим демон

sudo launchctl bootstrap system/ /Library/LaunchDaemons/com.custom.restoredevices.plist

Убедимся, что демон запущен

sudo launchctl list | grep restoredev

Найдём процесс

ps aux | grep '[c]fgutil'

Launch Daemon работает независимо от входа пользователя и будет готов к работе сразу после загрузки macOS. Если что-то пошло не так, то необработанные ошибки будут сохранятся в файле custom.restoredevices.stderr, прочие информационные сообщения /tmp/custom.restoredevices.stdout

При ненадёжном или медленном интернет соединении образы восстановления лучше скачать заблаговременно.


Пример логирования процесса

Процесс восстановления логируется созданным обработчиком WriteLog(message, logFile), на выходе мы получаем примерно такие логи:

Восстановление iPhone

Восстановление Mac


Другой вариант получения списка подключенных устройств

Администраторы, которые уже погружались в автоматизацию Apple Configurator могут сказать, что лепнина из двух обработчиков systemprofilerDevList() и cfgutilDevList(), а затем ещё и объединение результатов обработчиком MergeLists() лишнее и ненужное звено, потому что производитель уже предоставил встроенные инструменты, получить информацию о подключенных устройствах можно так:

set DevicesList to my DevicesList()
 
on DevicesList()
 
  try
    tell script "Configuration Utility"
      -- Вызываем необходимые свойства и присваиваем значения в переменные
      copy CNFGvaluesOfSpecifiedProperties("all", {"deviceType", "ECID", "locationID", "serialNumber", "bootedState"}, false) to ¬
        {deviceCount ¬
          , propertyTitles ¬
          , DeviceTypes ¬
          , deviceECIDs ¬
          , LocationIDs ¬
          , serialNumbers ¬
          , BootedStates}
    end tell
 
    -- Создаём пустой список
    set ResultList to {}
    --Получаем значения атрибутов для каждого устройства
    repeat with i from 1 to the deviceCount
      set DeviceType to item i of DeviceTypes
      set ECID to item i of deviceECIDs
      set LocationID to item i of LocationIDs
      set SerialNumber to item i of serialNumbers -- серийные номера отображаются нас устройствах в статусе Booted. Серийные номер Mac не отображаются вообще.
      set BootedState to item i of BootedStates -- значения Booted, Recovery или DFU
 
      -- Изменим формат Location ID integer на hex
      set LocationID to "0x" & (do shell script "printf '%X' " & LocationID)
      -- Добавляем подсписки с значениями устройства в список    
      set end of ResultList to {DeviceType, ECID, LocationID, SerialNumber, BootedState}
    end repeat
 
    return ResultList
 
    -- Пример вывода
    -- {{"iPhone8,4", "0x0048FC93999E4", "0x8310000", missing value, "Recovery"}, {"Macmini9,1", "0x9282C1440801E", "0x100000", missing value, "DFU"}, {"iPhone7,2", "0x19610E38E81026", "0x8320000", "F17R30000000", "Booted"}}
 
  on error
    return false
  end try

Мне не понравился нюанс, который не позволяет получить серийный номер подключенных Mac, а так же мобильных устройств в режиме DFU. Если этот момент вам неважен, то проще использовать обработчик CNFGvaluesOfSpecifiedProperties(), доступный из коробки.

Получить справку о наборе встроенных обработчиков для управления автоматизацией можно так:

tell script "Configuration Utility" to show help

Подводя итог, можно сказать, что подобная автоматизация может использоваться автономно, например, на стоящем на столе ИТ отдела Mac Mini 2020 года без монитора и периферии с торчащими Type-C кабелями для подключения как одиночных устройств, так и множества.

P.S. В моём ведении нет сотен Mac, iPhone или iPad, которые нужно перезаливать перед выдачей, только несколько личных старых iPhone и Mac Mini которые можно перезаливать каждый день. Попробуйте этот вариант и сообщите в комментариях как это работает на множестве подключенных устройств.


Проверено на следующих конфигурациях:

Версия ОС
Apple macOS Sequoia (15.0)

Автор первичной редакции:
Виталий Якоб
Время публикации: 05.12.2024 16:57

Обсуждение

Ваш комментарий:
 
apple-mac-os/macos-sequoia/automated-os-deployment-to-apple-mac-and-iphone-or-ipad-mobile-devices.txt · Последнее изменение: 10.12.2024 07:45 — Алексей Максимов

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki