Собрал пост по крупицам о том, как автоматизировать обновления Microsoft Office 2016 для macOS. Да, вот так вот внезапно. В посте присутствуют такие слова, как bash, python, powershell, docker, ConfigMgr, Parallels и всё только самое современное. ;) Ну, что? Поехали?
Предисловие
Идея в том, что у нас есть пачка девайсов под управление macOS на которых стоит Microsoft Office. Софт на них централизованно катится с ConfigMgr, какие-то апдейты для операционки из аппстора, а вот с офисом беда была. MS Office пока нет в маковском аппсторе, а обновлять и следить за ним очень надо, поэтому было решено, что нам требуется что-то, что будет его обновлять. ;)
Вы ведь знаете, как работают обновления для офиса на Windows? Microsoft выпустила апдейты, wsus отсинхронизировал каталог, вы скачали обновления и раздали их через WSUS или SUP у ConfigMgr, апдейт агенты отчитались с клиентов до WSUS и дальше процесс более-менее понятен. На macOS нет Windows Update Agent, маки не отчитываются на WSUS. Так что же нам с ними делать?
У Microsoft есть Microsoft AutoUpdate (MAU). Вот он и занимается проверкой версий и стучит в CDN Microsoft для проверки и скачиваний последних обновлений. MAU можно скачать здесь — https://docs.microsoft.com/en-us/officeupdates/release-history-microsoft-autoupdate
С версии MAU 3.18 и выше в /Library/Application\ Support/Microsoft/MAU2.0/Microsoft\ AutoUpdate.app/Contents/MacOS появилась консольная утилина msupdate. Весь список команд можно глянуть через ./msupdate —help
Вот она и занимается обновлением офиса. Что нам от неё надо? Она читает настройки из plist com.microsoft.autoupdate2, в котором мы уже можем настроить всё, что нам надо. Запомните главную вещь, настройки MAU берёт только из пользовательского плиста в ~/Library/Preferences/, это важно.
Но мы ведь хотим, чтобы обновления раздавались из внутренней сети, поэтому в первую очередь нам требуется утянуть из CDN все апдейты для офисных продуктов. В этом нам поможет powershell скрипт.
Всего есть 5 каналов поставки офиса: Production (ежемесячные релизы), External (или Insider Slow — ранний доступ), Internal (без понятия, но в документации присутствует), Custom (наш собственный) и InsiderFast (недельные релизы).
Внимание! Одна ветка Production занимает > 13 ГБ, если добавлять остальные, то выйдет > 25 ГБ.
.\psMacUpdatesOFFICEv2.ps1 -channel Production -IISRoot C:\inetpub\wwwroot -IisFolder maucache -TempShare C:\Temp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
<# Must supply channel. Values accaptable are "Production", "External", "InsiderFast" .EXAMPLE powershell.exe .\psMacUpdatesOFFICEv2.ps1 -channel Production -IISRoot C:\inetpub\wwwroot -IisFolder maucache -TempShare C:\temp .EXAMPLE powershell.exe .\psMacUpdatesOFFICEv2.ps1 -channel Production -IISRoot C:\inetpub\wwwroot -IisFolder maucache -TempShare C:\temp -verbose #> [cmdletbinding()] Param( [Parameter(Mandatory=$true,HelpMessage='Must supply channel. Values accaptable are "Production", "External", "InsiderFast"')] [ValidateSet("Production", "External", "InsiderFast")] [string] $channel, [Parameter(Mandatory=$true,HelpMessage='Default IIS LOcation EG c:\iinetpub\wwwroot')] [Validatescript({if (test-path ($_)){$true} Else {Throw "$_ doesnt exist. Must be a valid Path"}})] [string] $IISRoot, [Parameter(Mandatory=$true,HelpMessage='IIS Shared Folder Name. Also used in the temp folder eg. MAUCache')] [string] $IisFolder, [Parameter(Mandatory=$true,HelpMessage='Path to Temp working space eg c:\temp')] [Validatescript({if (test-path ($_)){$true} Else {Throw "$_ doesnt exist. Must be a valid Path"}})] [string] $TempShare ) If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!" Break } $null = New-Object System.Net.webclient $PublishFolderName = $IisFolder $publishBasePath = $IISRoot $tempfolderLocation = $TempShare #Test iis shared folder exists if not make it if (!(test-path "$publishBasePath\$PublishFolderName")){ New-Item -ItemType Directory -Path "$publishBasePath" -Name "$PublishFolderName" } $PublishFolder = "$publishBasePath\$PublishFolderName" $tempFolder = "$tempfolderLocation\$PublishFolderName" $collarteralFolder = $tempFolder #setup TEmp environment If (Test-path -Path $tempFolder) { Remove-Item $tempfolder -Recurse -Force } #Create TEmp Structure New-Item -ItemType Directory -Path $tempfolderlocation -Name "$PublishFolderName" New-Item -ItemType Directory -Path "$tempfolder" -Name "Collateral" $starturl = "https://officecdn-microsoft-com.akamaized.net" switch ($channel) { "Production"{$webUrlDownload = "$starturl/pr/C1297A47-86C4-4C1F-97FA-950631F94777/OfficeMac/"} "External"{$webUrlDownload = "$starturl/pr/1ac37578-5a24-40fb-892e-b89d85b6dfaa/OfficeMac/"} "InsiderFast"{$webUrlDownload = "$starturl/pr/4B2D7701-0A4F-49C8-B4CB-0C2D4043F51F/OfficeMac/"} } [io.file]::WriteAllbytes("$collarteralFolder\builds.txt",(Invoke-WebRequest -URI "$webUrlDownload/builds.txt").content) #compare temp build.txt with prod build.txt #Initalise $origcontent array incase of first run #if change detected then continue else stop $origContent = @("") if (test-path "$PublishFolder\builds.txt"){$origContent = Get-Content "$PublishFolder\builds.txt"} $newContent = Get-Content "$collarteralFolder\builds.txt" If ((compare-object $origContent $newContent).count -eq 0){ Write-Verbose "No Change" Break } $MAUID_MAU3X="0409MSAU03" $MAUID_WORD2016="0409MSWD15" $MAUID_EXCEL2016="0409XCEL15" $MAUID_POWERPOINT2016="0409PPT315" $MAUID_OUTLOOK2016="0409OPIM15" $MAUID_ONENOTE2016="0409ONMC15" $MAUID_OFFICE2011="0409MSOF14" $MAUID_LYNC2011="0409UCCP14" $MAUID_SKYPE2016="0409MSFB16" $MAUID_INTUNECP="0409IMCP01" $MAUID_REMOTEDESKTOP10="0409MSRD10" # xml version 16 $MAUID_WORD2016_1="0409MSWD16" $MAUID_EXCEL2016_1="0409XCEL16" $MAUID_POWERPOINT2016_1="0409PPT316" $MAUID_OUTLOOK2016_1="0409OPIM16" $MAUID_ONENOTE2016_1="0409ONMC16" function BuildApplicationArray() { # Builds an array of all the MAU-enabled applications that we care about $MAUAPP=@() $MAUAPP+="$MAUID_MAU3X" $MAUAPP+="$MAUID_WORD2016" $MAUAPP+="$MAUID_EXCEL2016" $MAUAPP+="$MAUID_POWERPOINT2016" $MAUAPP+="$MAUID_OUTLOOK2016" $MAUAPP+="$MAUID_ONENOTE2016" $MAUAPP+="$MAUID_OFFICE2011" $MAUAPP+="$MAUID_LYNC2011" $MAUAPP+="$MAUID_SKYPE2016" $MAUAPP+="$MAUID_INTUNECP" $MAUAPP+="$MAUID_REMOTEDESKTOP10" $MAUAPP+="$MAUID_WORD2016_1" $MAUAPP+="$MAUID_EXCEL2016_1" $MAUAPP+="$MAUID_POWERPOINT2016_1" $MAUAPP+="$MAUID_OUTLOOK2016_1" $MAUAPP+="$MAUID_ONENOTE2016_1" return $MAUAPP } function DownloadUPdate ([Parameter(Mandatory=$true)]$Payload, [Parameter(Mandatory=$true)]$location) { #Test-WritePath Write-Verbose "Starting $location - $collateral\$payload" #TEST BASELINE $collateral = "$collarteralFolder" $wc = New-Object System.Net.WebClient $wc.DownloadFile($($location), "$collateral\$payload") } function DownloadCollateralFiles ([Parameter(Mandatory=$true)]$downloadarray,[Parameter(Mandatory=$true)]$weburldown){ # Downloads XML/CAT collateral files foreach ($Down in $DownloadArray){ $payload ="" $locationstring = "" $UpdateVersions = "" Write-Verbose "$down" [io.file]::WriteAllbytes("$collarteralFolder\$down.xml",(Invoke-WebRequest -URI "$weburldown$down.xml").content) [io.file]::WriteAllbytes("$collarteralFolder\$down.cat",(Invoke-WebRequest -URI "$weburldown$down.cat").content) #get xml and find updateversion $log = "$collarteralFolder\$down.xml" $collateral = $collarteralFolder $patt = "<key>Update Version" $indx = Select-String $patt $log | ForEach-Object {$_.LineNumber} if ($indx.count -ge 2){ $UpdateVersions= @((Get-Content $log)[$indx]) $UpdateVersions=$UpdateVersions -replace "</String>", "" $UpdateVersions=$UpdateVersions -replace "<String>", "" $UpdateVersions=$UpdateVersions.trim() $pathtoput = "$($updateversions[0])" } elseif ($indx.count -eq 1){ $UpdateVersions= @((Get-Content $log)[$indx]) $UpdateVersions=$UpdateVersions -replace "</String>", "" $UpdateVersions=$UpdateVersions -replace "<String>", "" $UpdateVersions=$UpdateVersions.trim() $pathtoput="$($UpdateVersions)" } else { $pathtoput="Legacy" } #TEST COLLATERAL PATH EXISTS if (!(Test-Path "$collateral\$pathtoput")){ new-item -ItemType Directory -Path $collateral -Name $pathtoput -Verbose } write-verbose "$collateral\$pathtoput\$down.xml" Copy-Item -Path "$collarteralFolder\$down.xml" -Destination "$collateral\$pathtoput\$down.xml" -Verbose Copy-Item -Path "$collarteralFolder\$down.cat" -Destination "$collateral\$pathtoput\$down.cat" -Verbose #PAYLOAD NAME $log = "$collarteralFolder\$down.xml" $patt = "<KEY>Payload" $indxp = Select-String $patt $log | ForEach-Object {$_.LineNumber} write-verbose "$indx $($down)" $payload=@((Get-Content $log)[$indxp]) $payload=$payload -replace "</String>", "" $payload=$payload -replace "<String>", "" $payload=$payload.trim() #DOWNLOAD FILE $patt = "<KEY>Location" $indx = Select-String $patt $log | ForEach-Object {$_.LineNumber} $locationstring= @((Get-Content $log)[$indx]) $locationstring=$locationstring -replace "</String>", "" $locationstring=$locationstring -replace "<String>", "" $locationstring=$locationstring.trim() if ($indxp.count -le 1){ write-verbose "One Detected $payload $locationstring" DownloadUPdate -Payload $payload -location $locationstring } else { for ($x = 0; $x -le ($($indxp.count)-1); $x += 1) { write-verbose "One Detected $x" $pay = $($payload[$x]) $loc = $($locationstring[$x]) DownloadUPdate -Payload $pay -location $loc } } } } $mauApp = BuildApplicationArray DownloadCollateralFiles -downloadarray $mauApp -weburldown $webUrlDownload #rename Folders #Sainity check of folder before renaming if ((Get-ChildItem $tempfolder).count -ge 10){ Remove-Item $PublishFolder -recurse -Force start-sleep -Seconds 30 Move-Item -Path $tempfolder -Destination "$publishBasePath" } |
Скачали? Молодцы.
Первым делом нам надо раскатить на клиентов новую версию MAU, чтобы полностью контролировать ситуацию. Для тех, у кого управление маками нативное через ConfigMgr, то развлекайтесь сами с конвертацией pkg пакета, у меня всё это давно перестало работать на High Sierra. В Parallels все ставится через пакет: installer -pkg Microsoft_AutoUpdate_4.0.18061000_Updater.pkg -target /
И всё, раскатили MAU.
Далее, нам же нужно куда-то сложить 25 гигов, чтобы клиенты могли забрать. Вариантов несколько.
Вариант первый — используем нашу точку распространения на ConfigMgr, а точнее наш IIS. Только нужно чуточку подшаманить.
В Default Web Site создадим директорию, которая будет слинкована на UNC путь, где у нас будут лежать апдейты. Не на диск же Ц всё это складывать. ж) И не забываем добавить MIME types, а то IIS ничего не знает про файлы форматов dmg/pkg и будет возвращать 404.
.dmg — file/download
.pkg — file/download
Вариант два — засунуть всё это в докер образ и с выходом очередных апдейтов для офиса просто делать ребилд докер контейнера.
За основу возьмем контейнер nginx из докерхаба и в две строчки в dockerfile соберём контейнер с апдейтами.
1 2 |
FROM nginx COPY maucache /usr/share/nginx/html |
docker build -t office-updates .
Запуск docker run —name office-updates -d -p 8080:80 office-updates
После запуска nginx отдаёт нам всё, что надо.
Вариант два с половинкой — маунтить волюмом в докер шару с апдейтами. Здесь билд уже будет проходить за 3 секунды.
Так, теперь клиенты у нас по http смогут забрать апдейты. Пора разобраться, как им объяснить, где же их забирать и как контролировать этот процесс.
В ConfigMgr у нас есть замечательная штука под названием Configuration Items. Все настройки MAU берёт из пользовательского com.microsoft.autoupdate2. Читаем его: defaults read com.microsoft.autoupdate2 и видим что-то подобное.
В плист надо добавить несколько настроек. Я думаю, что из названия самих настроек всё понятно, кто и что делает. Если нет, то самый подробный документ, I’ve ever seen, лежит здесь и тут. Частота проверки указывается в минутах. Для разных коллекций, могут быть разные настройки и разные каналы обновлений офиса, чтобы мы могли проверить новый релиз на тестовых клиентах.
Нас интересуют эти настройки:
HowToCheck -string AutomaticDownload
DisableInsiderCheckbox -bool TRUE
ChannelName -string ‘Custom’
ManifestServer -string ‘http://srv-sccm-ps/officeupdates/’
UpdateCache -string ‘http://srv-sccm-ps/officeupdates/’
UpdateCheckFrequency -int 240
SendAllTelemetryEnabled -int 0
Но, как выяснилось в процессе тестирования для того, чтобы MAU начал обновлять, то его необходимо ассоциировать по версиям с приложениями Офиса. Значит перед тем, как раздать все настройки в плист, требуется на клиентах сделать всё красиво.
Мы возьмём текущую сессию пользователя, прогоним все приложения Офиса по версии и запишем всё, что нам надо в пользовательский com.microsoft.autoupdate2 вот таким скриптом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#! /bin/bash CurrentloggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");') # applications=" Word.app Excel.app PowerPoint.app OneNote.app Outlook.app" for application in $applications do domain="com.microsoft.autoupdate2" defaults_cmd="/usr/bin/sudo -u $CurrentloggedInUser /usr/bin/defaults" application_info_plist="/Applications/Microsoft $application/Contents/Info.plist" lcid="1033" if /bin/test -f "$application_info_plist" then application_bundle_signature=$($defaults_cmd read "$application_info_plist" CFBundleSignature) application_bundle_version=$($defaults_cmd read "$application_info_plist" CFBundleVersion) application_id=$(printf "%s%02s" "$application_bundle_signature" "${application_bundle_version%%.*}") # application_id="${application_id/%16/15}" $defaults_cmd write $domain Applications -dict-add "/Applications/Microsoft $application" "{ 'Application ID' = $application_id; LCID = $lcid ; }" fi done # # /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 HowToCheck -string AutomaticDownload /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 DisableInsiderCheckbox -bool TRUE /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 ChannelName -string 'Custom' /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 ManifestServer -string 'http://srv-sccm-ps/officeupdates/' /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 UpdateCache -string 'http://srv-sccm-ps/officeupdates/' /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 UpdateCheckFrequency -int 240 /usr/bin/defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 SendAllTelemetryEnabled -int 0 # /Library/Application\ Support/Microsoft/MAU2.0/Microsoft\ AutoUpdate.app/Contents/MacOS/msupdate --install echo "Success" exit 0 |
Скрипт для проверки настроек.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# echo "Check Office 2016 settings..." #! /bin/bash CurrentloggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");') # OfficeUpdatesServer="/usr/bin/defaults read /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 ManifestServer -string 'http://srv-sccm-ps/officeupdates/'" OfficeUpdatesServer=$(defaults read /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 UpdateCache) AutoCheck=$(defaults read /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2 HowToCheck) RESULT=$? # echo "Confirming configuration..." if [ $RESULT -eq 0 ] && [ -e "/Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2.plist" ] && [ "$OfficeUpdatesServer" == "http://srv-sccm-ps/officeupdates/" ] && [ "$AutoCheck" == "AutomaticDownload" ]; then echo "Success" else exit -1 fi |
Что нам с этим всем делать? Надо создать Configuration Items и всё. ж)
И поймать Success на выходе.
Все логи MAU смотрим в /Library/Logs/Microsoft/autoupdate.log
Как мы видим, клиент успешно забирает xml, если MAU видит, что билд Офиса сменился, там же в логе вы увидите и процесс установки.
Несколько лайфхаков. ж)
Если в stdout вернется еще что-нибудь кроме Success, то клиент будет все равно Non-Compliant. Возможно, что это такое поведение самого агента Parallels. Про ConfigMgr уже не знаю.
Если записываем через su $CurrentloggedInUser -c defaults write /Users/$CurrentloggedInUser/Library/Preferences/com.microsoft.autoupdate2, то плист поломается. Что агент ConfigMgr, что агент Parallels уже всё выполняют с повышенными привилегиями. Не забывайте делать бэкапы плистов, просто поднимать интерактивную сессию sudo -i и там уже проверять, что всё работает так, как надо.
Главный лайфхак. В Windows 10 инсайдер билд notepad уже должен нормально работать с файлами, созданными в Unix, Linux и macOS, но если вы залили шелл скрипт на свою точку распространения, добавили в пакет, поправили файл в блокноте на WS2012 R2, например, то всё, всё сломалось. ;) Помните про это, т.к. после многочасовых траблшутингов можно долго бить себе фейспалмы.