Update answer.sh

This commit is contained in:
Kroese
2026-08-03 06:01:18 +02:00
committed by GitHub
parent 03afab532c
commit e4680fdd64
+452 -287
View File
@@ -120,65 +120,90 @@ generateAnswerFile() {
local index="$4" local index="$4"
local type="$5" local type="$5"
local remove_selector="$6" local remove_selector="$6"
local tmp local ns="urn:schemas-microsoft-com:unattend"
local wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
local directory install_count install_to_count tmp
if [ -n "$index" ] && [[ ! "$index" =~ ^[1-9][0-9]*$ ]]; then if [ -n "$index" ] && [[ ! "$index" =~ ^[1-9][0-9]*$ ]]; then
error "Invalid $type image index: $index" error "Invalid $type image index: $index"
return 1 return 1
fi fi
if ! tmp=$(mktemp -p /run/assets ".${id}.XXXXXX"); then directory=$(dirname "$target") || return 1
if ! tmp=$(mktemp -p "$directory" ".${id}.XXXXXX"); then
error "Failed to create a temporary $type answer file!" error "Failed to create a temporary $type answer file!"
return 1 return 1
fi fi
local expressions if ! cp -L -- "$source" "$tmp"; then
if [ "$type" = "evaluation" ]; then
expressions=(
-e '/<ProductKey>.*<\/ProductKey>/d'
-e '/<ProductKey>/,/<\/ProductKey>/d'
)
else
expressions=(
-e '/<InstallFrom>.*<\/InstallFrom>/d'
-e '/<ProductKey>.*<\/ProductKey>/d'
-e '/<InstallFrom>/,/<\/InstallFrom>/d'
-e '/<ProductKey>/,/<\/ProductKey>/d'
)
fi
if ! sed "${expressions[@]}" "$source" > "$tmp"; then
rm -f "$tmp" rm -f "$tmp"
error "Failed to generate $type answer file from $source!" error "Failed to generate $type answer file from $source!"
return 1 return 1
fi fi
if [ "$type" = "evaluation" ] && [ "$remove_selector" = "Y" ]; then local -a args=(
if ! sed -i \ -L
-e '/<InstallFrom>.*<\/InstallFrom>/d' \ -N "u=$ns"
-e '/<InstallFrom>/,/<\/InstallFrom>/d' \ -d '//u:ProductKey'
"$tmp"; then )
rm -f "$tmp"
error "Failed to replace evaluation image selector!" if [ "$type" != "evaluation" ] || [ "$remove_selector" = "Y" ]; then
return 1 args+=( -d '//u:InstallFrom' )
fi
fi fi
if [ -n "$index" ] && ! grep -q '<InstallFrom>' "$tmp"; then if ! xmlstarlet ed "${args[@]}" "$tmp"; then
if ! sed -i \ rm -f "$tmp"
'0,/<InstallTo>/{ /<InstallTo>/i\ error "Failed to generate $type answer file from $source!"
<InstallFrom>\ return 1
<MetaData wcm:action="add">\ fi
<Key>/IMAGE/INDEX</Key>\
<Value>'"$index"'</Value>\ if [ -n "$index" ]; then
</MetaData>\ install_count=$(xmlstarlet sel \
</InstallFrom> -N "u=$ns" \
}' "$tmp"; then -T -t \
-v 'count(//u:ImageInstall/u:OSImage/u:InstallFrom)' \
"$tmp") || {
rm -f "$tmp"
return 1
}
if [ "$install_count" = "0" ]; then
install_to_count=$(xmlstarlet sel \
-N "u=$ns" \
-T -t \
-v 'count(//u:ImageInstall/u:OSImage/u:InstallTo)' \
"$tmp") || {
rm -f "$tmp"
return 1
}
if [ "$install_to_count" = "0" ]; then
rm -f "$tmp" rm -f "$tmp"
error "Failed to select $type image index $index!" error "Failed to select $type image index $index!"
return 1 return 1
fi fi
if ! xmlstarlet ed -L \
-N "u=$ns" \
-N "wcm=$wcm" \
-i '(//u:ImageInstall/u:OSImage/u:InstallTo)[1]' \
-t elem -n 'InstallFrom' \
-s '//u:ImageInstall/u:OSImage/*[local-name()="InstallFrom"]' \
-t elem -n 'MetaData' \
-i '//u:ImageInstall/u:OSImage/*[local-name()="InstallFrom"]/*[local-name()="MetaData"]' \
-t attr -n 'wcm:action' -v 'add' \
-s '//u:ImageInstall/u:OSImage/*[local-name()="InstallFrom"]/*[local-name()="MetaData"]' \
-t elem -n 'Key' -v '/IMAGE/INDEX' \
-s '//u:ImageInstall/u:OSImage/*[local-name()="InstallFrom"]/*[local-name()="MetaData"]' \
-t elem -n 'Value' -v "$index" \
"$tmp"; then
rm -f "$tmp"
error "Failed to select $type image index $index!"
return 1
fi
fi
fi fi
if ! markGeneratedXML "$tmp"; then if ! markGeneratedXML "$tmp"; then
@@ -339,8 +364,10 @@ updateXML() {
updateLocalAccount "$asset" || return 1 updateLocalAccount "$asset" || return 1
fi fi
sed -i -E \ xmlstarlet ed -L \
"s|<PlainText>[^<]*</PlainText>|<PlainText>false</PlainText>|g" \ -N 'u=urn:schemas-microsoft-com:unattend' \
-u '//u:PlainText' \
-v 'false' \
"$asset" || return 1 "$asset" || return 1
updateMembership \ updateMembership \
@@ -947,157 +974,233 @@ validateDomainName() {
updateWorkgroup() { updateWorkgroup() {
local asset="$1" local asset="$1"
local workgroup arch tmp local workgroup="$2"
local workgroup_xml
local ns="urn:schemas-microsoft-com:unattend"
local specialize='/u:unattend/u:settings[@pass="specialize"]'
local component="$specialize/*[local-name()=\"component\" and @name=\"__QEMUS_UNATTENDED_JOIN__\"]"
local arch counts settings_count join_count unnamed_count tmp
workgroup=$(escapeXML "$2") || return 1 workgroup_xml=$(escapeXML "$workgroup") || return 1
arch=$(getXMLArchitecture "$asset") || return 1 arch=$(getXMLArchitecture "$asset") || return 1
grep -q 'Microsoft-Windows-UnattendedJoin' "$asset" && return 1 counts=$(xmlstarlet sel \
-N "u=$ns" \
-T -t \
-v "count($specialize)" -o '|' \
-v 'count(//u:component[@name="Microsoft-Windows-UnattendedJoin"])' -o '|' \
-v "count($specialize/u:component[not(@name)])" \
"$asset") || return 1
tmp=$(mktemp -d) || return 1 IFS='|' read -r settings_count join_count unnamed_count <<< "$counts"
local result="$tmp/answer.xml"
if ! WORKGROUP_XML="$workgroup" ARCH_XML="$arch" awk ' [ "$settings_count" = "1" ] || return 1
/<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" } [ "$join_count" = "0" ] || return 1
[ "$unnamed_count" = "0" ] || return 1
section == "specialize" && !workgroup_added && if ! tmp=$(mktemp "${asset}.XXXXXX"); then
/^[[:space:]]*<\/settings>[[:space:]]*$/ { return 1
print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ fi
" <Identification>\n" \
" <JoinWorkgroup>" ENVIRON["WORKGROUP_XML"] "</JoinWorkgroup>\n" \ if ! xmlstarlet ed \
" </Identification>\n" \ -N "u=$ns" \
" </component>" -s "$specialize" \
workgroup_added = 1 -t elem -n 'component' \
} -i "$specialize/*[local-name()='component' and not(@name)][last()]" \
-t attr -n 'name' -v '__QEMUS_UNATTENDED_JOIN__' \
{ print } -i "$component" \
-t attr -n 'processorArchitecture' -v "$arch" \
/^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } -i "$component" \
END { exit !workgroup_added } -t attr -n 'publicKeyToken' -v '31bf3856ad364e35' \
' "$asset" > "$result" || -i "$component" \
! mv -f "$result" "$asset"; then -t attr -n 'language' -v 'neutral' \
-i "$component" \
rm -rf "$tmp" || true -t attr -n 'versionScope' -v 'nonSxS' \
-s "$component" \
-t elem -n 'Identification' \
-s "$component/*[local-name()='Identification']" \
-t elem -n 'JoinWorkgroup' -v "$workgroup_xml" \
-u "$component/@name" \
-v 'Microsoft-Windows-UnattendedJoin' \
"$asset" > "$tmp"; then
rm -f "$tmp"
return 1
fi
if ! chmod --reference="$asset" "$tmp" ||
! mv -f "$tmp" "$asset"; then
rm -f "$tmp"
return 1 return 1
fi fi
rm -rf "$tmp" || return 1
return 0 return 0
} }
updateDomain() { updateDomain() {
local asset="$1" local asset="$1"
local domain account auth pass local domain="$2"
local ou arch tmp local account="$3"
local auth="$4"
local pass="$5"
local ou="$6"
local ns="urn:schemas-microsoft-com:unattend"
local wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
local specialize='/u:unattend/u:settings[@pass="specialize"]'
local shell='/u:unattend/u:settings[@pass="oobeSystem"]/u:component[@name="Microsoft-Windows-Shell-Setup"]'
local accounts="$shell/u:UserAccounts"
local autologon="$shell/u:AutoLogon"
local domain_accounts="$accounts/*[local-name()=\"DomainAccounts\"]"
local account_list="$domain_accounts/*[local-name()=\"DomainAccountList\"]"
local domain_account="$account_list/*[local-name()=\"DomainAccount\"]"
local component="$specialize/*[local-name()=\"component\" and @name=\"__QEMUS_UNATTENDED_JOIN__\"]"
local identification="$component/*[local-name()=\"Identification\"]"
local credentials="$identification/*[local-name()=\"Credentials\"]"
local cred_domain="$domain"
local domain_xml account_xml auth_xml pass_xml ou_xml cred_domain_xml
local arch counts tmp
local settings_count join_count unnamed_count shell_count
local accounts_count domain_count admin_count autologon_count
local username_count value_count plaintext_count
domain=$(escapeXML "$2") || return 1 domain_xml=$(escapeXML "$domain") || return 1
account=$(escapeXML "$3") || return 1 account_xml=$(escapeXML "$account") || return 1
auth=$(escapeXML "$4") || return 1 auth_xml=$(escapeXML "$auth") || return 1
pass=$(escapeXML "$5") || return 1 pass_xml=$(escapeXML "$pass") || return 1
ou=$(escapeXML "$6") || return 1 ou_xml=$(escapeXML "$ou") || return 1
arch=$(getXMLArchitecture "$asset") || return 1 arch=$(getXMLArchitecture "$asset") || return 1
local cred_domain="$domain" case "$auth" in
case "$4" in
*@* ) cred_domain="" ;; *@* ) cred_domain="" ;;
esac esac
grep -Eq 'Microsoft-Windows-UnattendedJoin|<DomainAccounts([[:space:]/>])' "$asset" && return 1 cred_domain_xml=$(escapeXML "$cred_domain") || return 1
tmp=$(mktemp -d) || return 1 counts=$(xmlstarlet sel \
local result="$tmp/answer.xml" -N "u=$ns" \
-T -t \
-v "count($specialize)" -o '|' \
-v 'count(//u:component[@name="Microsoft-Windows-UnattendedJoin"])' -o '|' \
-v "count($specialize/u:component[not(@name)])" -o '|' \
-v "count($shell)" -o '|' \
-v "count($accounts)" -o '|' \
-v "count($accounts/u:DomainAccounts)" -o '|' \
-v "count($accounts/u:AdministratorPassword)" -o '|' \
-v "count($autologon)" -o '|' \
-v "count($autologon/u:Username)" -o '|' \
-v "count($autologon/u:Password/u:Value)" -o '|' \
-v "count($autologon/u:Password/u:PlainText)" \
"$asset") || return 1
if ! DOMAIN_XML="$domain" ACCOUNT_XML="$account" \ IFS='|' read -r \
AUTH_XML="$auth" PASS_XML="$pass" \ settings_count join_count unnamed_count shell_count \
CRED_DOMAIN="$cred_domain" OU_XML="$ou" \ accounts_count domain_count admin_count autologon_count \
ARCH_XML="$arch" \ username_count value_count plaintext_count <<< "$counts"
awk '
/<settings[^>]*pass="specialize"[^>]*>/ { section = "specialize" }
/<settings[^>]*pass="oobeSystem"[^>]*>/ { section = "oobeSystem" }
section == "oobeSystem" && /<UserAccounts([[:space:]>])/ { in_accounts = 1 }
section == "oobeSystem" && /<AutoLogon([[:space:]>])/ { in_autologon = 1 }
section == "oobeSystem" && in_accounts && !accounts_added && [ "$settings_count" = "1" ] || return 1
/<AdministratorPassword([[:space:]>])/ { [ "$join_count" = "0" ] || return 1
print " <DomainAccounts>\n" \ [ "$unnamed_count" = "0" ] || return 1
" <DomainAccountList wcm:action=\"add\">\n" \ [ "$shell_count" = "1" ] || return 1
" <DomainAccount wcm:action=\"add\">\n" \ [ "$accounts_count" = "1" ] || return 1
" <Name>" ENVIRON["ACCOUNT_XML"] "</Name>\n" \ [ "$domain_count" = "0" ] || return 1
" <Group>Administrators</Group>\n" \ [ "$admin_count" = "1" ] || return 1
" </DomainAccount>\n" \ [ "$autologon_count" = "1" ] || return 1
" <Domain>" ENVIRON["DOMAIN_XML"] "</Domain>\n" \ [ "$username_count" = "1" ] || return 1
" </DomainAccountList>\n" \ [ "$value_count" = "1" ] || return 1
" </DomainAccounts>" [ "$plaintext_count" = "1" ] || return 1
accounts_added = 1
}
section == "oobeSystem" && in_autologon && if ! tmp=$(mktemp "${asset}.XXXXXX"); then
/^[[:space:]]*<Username>.*<\/Username>[[:space:]]*$/ { return 1
print " <Username>" ENVIRON["ACCOUNT_XML"] "</Username>\n" \ fi
" <Domain>" ENVIRON["DOMAIN_XML"] "</Domain>"
autologon_added = 1 local -a args=(
next -N "u=$ns"
} -N "wcm=$wcm"
-i "$accounts/u:AdministratorPassword"
section == "oobeSystem" && in_autologon && -t elem -n 'DomainAccounts'
/^[[:space:]]*<Domain([[:space:]/>])/ { next } -s "$domain_accounts"
-t elem -n 'DomainAccountList'
section == "oobeSystem" && in_autologon && -i "$account_list"
/^[[:space:]]*<Value>.*<\/Value>[[:space:]]*$/ { -t attr -n 'wcm:action' -v 'add'
print " <Value>" ENVIRON["PASS_XML"] "</Value>" -s "$account_list"
password_added = 1 -t elem -n 'DomainAccount'
next -i "$domain_account"
} -t attr -n 'wcm:action' -v 'add'
-s "$domain_account"
section == "oobeSystem" && in_autologon && -t elem -n 'Name' -v "$account_xml"
/^[[:space:]]*<PlainText([[:space:]/>])/ { -s "$domain_account"
print " <PlainText>true</PlainText>" -t elem -n 'Group' -v 'Administrators'
plaintext_added = 1 -s "$account_list"
next -t elem -n 'Domain' -v "$domain_xml"
} -d "$autologon/u:Domain"
-u "$autologon/u:Username"
section == "specialize" && !join_added && -v "$account"
/^[[:space:]]*<\/settings>[[:space:]]*$/ { -a "$autologon/u:Username"
print " <component name=\"Microsoft-Windows-UnattendedJoin\" processorArchitecture=\"" ENVIRON["ARCH_XML"] "\" publicKeyToken=\"31bf3856ad364e35\" language=\"neutral\" versionScope=\"nonSxS\">\n" \ -t elem -n 'Domain' -v "$domain_xml"
" <Identification>\n" \ -u "$autologon/u:Password/u:Value"
" <Credentials>" -v "$pass"
-u "$autologon/u:Password/u:PlainText"
if (ENVIRON["CRED_DOMAIN"] != "") { -v 'true'
print " <Domain>" ENVIRON["CRED_DOMAIN"] "</Domain>" -s "$specialize"
} -t elem -n 'component'
-i "$specialize/*[local-name()='component' and not(@name)][last()]"
print " <Username>" ENVIRON["AUTH_XML"] "</Username>\n" \ -t attr -n 'name' -v '__QEMUS_UNATTENDED_JOIN__'
" <Password>" ENVIRON["PASS_XML"] "</Password>\n" \ -i "$component"
" </Credentials>\n" \ -t attr -n 'processorArchitecture' -v "$arch"
" <JoinDomain>" ENVIRON["DOMAIN_XML"] "</JoinDomain>" -i "$component"
-t attr -n 'publicKeyToken' -v '31bf3856ad364e35'
if (ENVIRON["OU_XML"] != "") { -i "$component"
print " <MachineObjectOU>" ENVIRON["OU_XML"] "</MachineObjectOU>" -t attr -n 'language' -v 'neutral'
} -i "$component"
-t attr -n 'versionScope' -v 'nonSxS'
print " </Identification>\n" \ -s "$component"
" </component>" -t elem -n 'Identification'
-s "$identification"
join_added = 1 -t elem -n 'Credentials'
} )
{ print } if [ -n "$cred_domain" ]; then
args+=(
section == "oobeSystem" && /<\/AutoLogon>/ { in_autologon = 0 } -s "$credentials"
section == "oobeSystem" && /<\/UserAccounts>/ { in_accounts = 0 } -t elem -n 'Domain' -v "$cred_domain_xml"
/^[[:space:]]*<\/settings>[[:space:]]*$/ { section = "" } )
fi
END { exit !(join_added && accounts_added && autologon_added && password_added && plaintext_added) }
' "$asset" > "$result" || args+=(
! mv -f "$result" "$asset"; then -s "$credentials"
-t elem -n 'Username' -v "$auth_xml"
rm -rf "$tmp" || true -s "$credentials"
-t elem -n 'Password' -v "$pass_xml"
-s "$identification"
-t elem -n 'JoinDomain' -v "$domain_xml"
)
if [ -n "$ou" ]; then
args+=(
-s "$identification"
-t elem -n 'MachineObjectOU' -v "$ou_xml"
)
fi
args+=(
-u "$component/@name"
-v 'Microsoft-Windows-UnattendedJoin'
)
if ! xmlstarlet ed "${args[@]}" "$asset" > "$tmp"; then
rm -f "$tmp"
return 1
fi
if ! chmod --reference="$asset" "$tmp" ||
! mv -f "$tmp" "$asset"; then
rm -f "$tmp"
return 1 return 1
fi fi
rm -rf "$tmp" || return 1
return 0 return 0
} }
@@ -1172,18 +1275,27 @@ prepareDomainAccount() {
updateDisplayXML() { updateDisplayXML() {
local asset="$1" local asset="$1"
local app host local ns="urn:schemas-microsoft-com:unattend"
local app="$APP for $ENGINE"
local -a args=(
-L
-N "u=$ns"
-u '//u:*[text()="Windows for Docker"]'
-v "$app"
-u '//u:VerticalResolution'
-v "$HEIGHT"
-u '//u:HorizontalResolution'
-v "$WIDTH"
)
app=$(escapeXMLSed "$APP for $ENGINE") || return 1 if [ -n "${HOST:-}" ]; then
args+=(
-u '//u:ComputerName'
-v "$HOST"
)
fi
sed -i "s|>Windows for Docker<|>$app<|g" "$asset" || return 1 xmlstarlet ed "${args[@]}" "$asset" || return 1
sed -i -E "s|<VerticalResolution>[^<]*</VerticalResolution>|<VerticalResolution>$HEIGHT</VerticalResolution>|g" "$asset" || return 1
sed -i -E "s|<HorizontalResolution>[^<]*</HorizontalResolution>|<HorizontalResolution>$WIDTH</HorizontalResolution>|g" "$asset" || return 1
[ -n "${HOST:-}" ] || return 0
host=$(escapeXMLSed "$HOST") || return 1
sed -i -E "s|<ComputerName>[^<]*</ComputerName>|<ComputerName>$host</ComputerName>|g" "$asset" || return 1
return 0 return 0
} }
@@ -1192,29 +1304,41 @@ updateLocaleXML() {
local asset="$1" local asset="$1"
local language="$2" local language="$2"
local culture region keyboard value local ns="urn:schemas-microsoft-com:unattend"
local culture region keyboard
local -a args=( -L -N "u=$ns" )
culture=$(getLanguage "$language" "culture") || return 1 culture=$(getLanguage "$language" "culture") || return 1
if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then
value=$(escapeXMLSed "$culture") || return 1 args+=(
sed -i "s|<UILanguage>en-US</UILanguage>|<UILanguage>$value</UILanguage>|g" "$asset" || return 1 -u '//u:UILanguage[text()="en-US"]'
-v "$culture"
)
fi fi
region="${REGION:-$culture}" region="${REGION:-$culture}"
if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then
value=$(escapeXMLSed "$region") || return 1 args+=(
sed -i "s|<UserLocale>en-US</UserLocale>|<UserLocale>$value</UserLocale>|g" "$asset" || return 1 -u '//u:UserLocale[text()="en-US"]'
sed -i "s|<SystemLocale>en-US</SystemLocale>|<SystemLocale>$value</SystemLocale>|g" "$asset" || return 1 -v "$region"
-u '//u:SystemLocale[text()="en-US"]'
-v "$region"
)
fi fi
keyboard="${KEYBOARD:-$culture}" keyboard="${KEYBOARD:-$culture}"
if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then
value=$(escapeXMLSed "$keyboard") || return 1 args+=(
sed -i "s|<InputLocale>en-US</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 -u '//u:InputLocale[text()="en-US" or text()="0409:00000409"]'
sed -i "s|<InputLocale>0409:00000409</InputLocale>|<InputLocale>$value</InputLocale>|g" "$asset" || return 1 -v "$keyboard"
)
fi
if (( ${#args[@]} > 3 )); then
xmlstarlet ed "${args[@]}" "$asset" || return 1
fi fi
return 0 return 0
@@ -1225,17 +1349,11 @@ updateLocalAccount() {
local asset="$1" local asset="$1"
local user="${USERNAME:-}" local user="${USERNAME:-}"
local pass="${PASSWORD:-admin}" local pass="${PASSWORD:-admin}"
local user_xml pw admin local ns="urn:schemas-microsoft-com:unattend"
local pw admin
validateUsername "$user" "local" || return 1 validateUsername "$user" "local" || return 1
if [ -n "$user" ]; then
user_xml=$(escapeXMLSed "$user") || return 1
sed -i "s|<Name>Docker</Name>|<Name>$user_xml</Name>|g" "$asset" || return 1
sed -i "s|<FullName>Docker</FullName>|<FullName>$user_xml</FullName>|g" "$asset" || return 1
sed -i "s|<Username>Docker</Username>|<Username>$user_xml</Username>|g" "$asset" || return 1
fi
pw=$(printf '%s' "${pass}Password" | pw=$(printf '%s' "${pass}Password" |
iconv -f utf-8 -t utf-16le | iconv -f utf-8 -t utf-16le |
base64 -w 0) || return 1 base64 -w 0) || return 1
@@ -1244,13 +1362,27 @@ updateLocalAccount() {
iconv -f utf-8 -t utf-16le | iconv -f utf-8 -t utf-16le |
base64 -w 0) || return 1 base64 -w 0) || return 1
sed -i -z -E \ local -a args=( -L -N "u=$ns" )
"s#(<Password>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$pw</Value>#g" \
"$asset" || return 1
sed -i -z -E \ if [ -n "$user" ]; then
"s#(<AdministratorPassword>[[:space:]]*<Value)([[:space:]]*/>|>[^<]*</Value>)#\1>$admin</Value>#g" \ args+=(
"$asset" || return 1 -u '//u:Name[text()="Docker"]'
-v "$user"
-u '//u:FullName[text()="Docker"]'
-v "$user"
-u '//u:Username[text()="Docker"]'
-v "$user"
)
fi
args+=(
-u '//u:Password/u:Value'
-v "$pw"
-u '//u:AdministratorPassword/u:Value'
-v "$admin"
)
xmlstarlet ed "${args[@]}" "$asset" || return 1
return 0 return 0
} }
@@ -1293,11 +1425,13 @@ updateMembership() {
updateAutologinXML() { updateAutologinXML() {
local asset="$1" local asset="$1"
local ns="urn:schemas-microsoft-com:unattend"
disabled "${AUTOLOGIN:-}" || return 0 disabled "${AUTOLOGIN:-}" || return 0
sed -i -E \ xmlstarlet ed -L \
'/^[[:space:]]*<AutoLogon([[:space:]>])/,/^[[:space:]]*<\/AutoLogon>[[:space:]]*$/d' \ -N "u=$ns" \
-d '//u:AutoLogon' \
"$asset" || return 1 "$asset" || return 1
return 0 return 0
@@ -1306,17 +1440,22 @@ updateAutologinXML() {
updateEditionXML() { updateEditionXML() {
local asset="$1" local asset="$1"
local edition local ns="urn:schemas-microsoft-com:unattend"
local edition xpath expression
[ -n "${EDITION:-}" ] || return 0 [ -n "${EDITION:-}" ] || return 0
edition=$(normalizeServerEdition "$EDITION") || return 1 edition=$(normalizeServerEdition "$EDITION") || return 1
edition="${edition//-/}" edition="${edition//-/}"
edition="${edition^^}" edition="${edition^^}"
edition=$(escapeXMLSed "$edition") || return 1
sed -i \ xpath='//u:InstallFrom/u:MetaData[u:Key="/IMAGE/NAME"]/u:Value[substring(., string-length(.) - string-length("SERVERSTANDARD") + 1) = "SERVERSTANDARD"]'
"s|SERVERSTANDARD</Value>|SERVER$edition</Value>|g" \ expression="concat(substring-before(., 'SERVERSTANDARD'), 'SERVER$edition', substring-after(., 'SERVERSTANDARD'))"
xmlstarlet ed -L \
-N "u=$ns" \
-u "$xpath" \
-x "$expression" \
"$asset" || return 1 "$asset" || return 1
return 0 return 0
@@ -1348,7 +1487,8 @@ updateDiskID() {
local disk_type="${2,,}" local disk_type="${2,,}"
local mode="${3:-setup}" local mode="${3:-setup}"
local target="0" local target="0"
local matches ids current count rc local value current
local -a ids=()
[ -s "$asset" ] || return 1 [ -s "$asset" ] || return 1
@@ -1362,29 +1502,26 @@ updateDiskID() {
* ) return 1 ;; * ) return 1 ;;
esac esac
matches=$(grep -oE '<DiskID>[[:space:]]*[0-9]+[[:space:]]*</DiskID>' "$asset") || { while IFS= read -r value; do
rc=$? [[ "$value" =~ ^[0-9]+$ ]] || continue
if [ "$rc" -eq 1 ]; then ids+=( "$value" )
matches="" done < <(
else xmlstarlet sel \
error "Failed to read DiskID values from answer file: $asset" -T -t \
return 1 -m '//*[local-name()="DiskID"]' \
fi -v 'normalize-space(.)' -n \
} "$asset"
)
# Some custom answer files do not contain a disk configuration. # Some custom answer files do not contain a disk configuration.
[ -n "$matches" ] || return 0 (( ${#ids[@]} > 0 )) || return 0
ids=$(printf '%s\n' "$matches" | mapfile -t ids < <(printf '%s\n' "${ids[@]}" | sort -u)
sed -E 's#.*<DiskID>[[:space:]]*([0-9]+)[[:space:]]*</DiskID>.*#\1#' |
sort -u) || return 1
count=$(printf '%s\n' "$ids" | wc -l) || return 1
# Leave explicit multi-disk configurations untouched. # Leave explicit multi-disk configurations untouched.
[ "$count" -eq 1 ] || return 0 (( ${#ids[@]} == 1 )) || return 0
current="$ids" current="${ids[0]}"
[ "$current" = "$target" ] && return 0 [ "$current" = "$target" ] && return 0
case "$current" in case "$current" in
@@ -1395,8 +1532,11 @@ updateDiskID() {
;; ;;
esac esac
if ! sed -i -E \ if ! xmlstarlet ed -L \
"s#<DiskID>[[:space:]]*${current}[[:space:]]*</DiskID>#<DiskID>$target</DiskID>#g" \ "$asset"; then -u "//*[local-name()='DiskID' and normalize-space(.)='$current']" \
-v "$target" \
"$asset"; then
error "Failed to update DiskID in answer file: $asset" error "Failed to update DiskID in answer file: $asset"
return 1 return 1
fi fi
@@ -1409,8 +1549,9 @@ getXMLArchitecture() {
local asset="$1" local asset="$1"
local arch local arch
arch=$(sed -n -E \ arch=$(xmlstarlet sel \
'0,/processorArchitecture="/s/.*processorArchitecture="([^"]+)".*/\1/p' \ -T -t \
-v 'string((//*[local-name()="component"]/@processorArchitecture)[1])' \
"$asset") || return 1 "$asset") || return 1
[ -n "$arch" ] || return 1 [ -n "$arch" ] || return 1
@@ -1424,89 +1565,125 @@ setConfigurationXML() {
local asset="$1" local asset="$1"
local setup='/*[local-name()="unattend"]/*[local-name()="settings" and @pass="windowsPE"]/*[local-name()="component" and @name="Microsoft-Windows-Setup"]' local setup='/*[local-name()="unattend"]/*[local-name()="settings" and @pass="windowsPE"]/*[local-name()="component" and @name="Microsoft-Windows-Setup"]'
local config="$setup/*[local-name()=\"UseConfigurationSet\"]" local config="$setup/*[local-name()=\"UseConfigurationSet\"]"
local userdata="$setup/*[local-name()=\"UserData\"]" local setup_count config_count result_count tmp
local setup_count config_count userdata_count tmp
[ -s "$asset" ] || return 1 [ -s "$asset" ] || return 1
setup_count=$(xmlstarlet sel -t -v "count($setup)" "$asset") || return 1 setup_count=$(xmlstarlet sel -T -t -v "count($setup)" "$asset") || return 1
if [ "$setup_count" != "1" ]; then if [ "$setup_count" != "1" ]; then
error "Failed to find a unique Microsoft-Windows-Setup component: $asset" error "Failed to find a unique Microsoft-Windows-Setup component: $asset"
return 1 return 1
fi fi
config_count=$(xmlstarlet sel -t -v "count($config)" "$asset") || return 1 config_count=$(xmlstarlet sel -T -t -v "count($config)" "$asset") || return 1
if [ "$config_count" -gt 1 ]; then if [ "$config_count" -gt 1 ]; then
error "Multiple UseConfigurationSet entries found in answer file: $asset" error "Multiple UseConfigurationSet entries found in answer file: $asset"
return 1 return 1
fi fi
if ! tmp=$(mktemp "${asset}.XXXXXX"); then if ! tmp=$(mktemp -d); then
error "Failed to create a temporary answer file!" error "Failed to create a temporary answer file!"
return 1 return 1
fi fi
if [ "$config_count" -eq 1 ]; then local stylesheet="$tmp/configuration.xsl"
local result="$tmp/answer.xml"
if ! xmlstarlet ed \ if ! cat > "$stylesheet" <<'XSL'
-u "$config" \ <?xml version="1.0" encoding="UTF-8"?>
-v "true" \ <xsl:stylesheet version="1.0"
"$asset" > "$tmp"; then xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
rm -f "$tmp" <xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="
*[local-name()='UseConfigurationSet' and
parent::*[
local-name()='component' and
@name='Microsoft-Windows-Setup' and
parent::*[local-name()='settings' and @pass='windowsPE']
]
]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:text>true</xsl:text>
</xsl:copy>
</xsl:template>
<xsl:template match="
*[local-name()='UserData' and
parent::*[
local-name()='component' and
@name='Microsoft-Windows-Setup' and
parent::*[local-name()='settings' and @pass='windowsPE'] and
not(*[local-name()='UseConfigurationSet'])
]
]">
<xsl:element name="UseConfigurationSet" namespace="{namespace-uri()}">
<xsl:text>true</xsl:text>
</xsl:element>
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="
*[local-name()='component' and
@name='Microsoft-Windows-Setup' and
parent::*[local-name()='settings' and @pass='windowsPE'] and
not(*[local-name()='UseConfigurationSet']) and
not(*[local-name()='UserData'])
]">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
<xsl:element name="UseConfigurationSet" namespace="{namespace-uri()}">
<xsl:text>true</xsl:text>
</xsl:element>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
XSL
then
rm -rf "$tmp"
return 1
fi
if ! xmlstarlet tr "$stylesheet" "$asset" > "$result"; then
rm -rf "$tmp"
error "Failed to enable the Windows configuration set!" error "Failed to enable the Windows configuration set!"
return 1 return 1
fi fi
else result_count=$(xmlstarlet sel \
-T -t \
userdata_count=$(xmlstarlet sel -t -v "count($userdata)" "$asset") || { -v "count($config[normalize-space(.)='true'])" \
rm -f "$tmp" "$result") || {
rm -rf "$tmp"
return 1 return 1
} }
if [ "$userdata_count" -gt 0 ]; then if [ "$result_count" != "1" ]; then
rm -rf "$tmp"
if ! xmlstarlet ed \ error "Failed to enable the Windows configuration set!"
-i "${userdata}[1]" \
-t elem \
-n "UseConfigurationSet" \
-v "true" \
"$asset" > "$tmp"; then
rm -f "$tmp"
error "Failed to insert UseConfigurationSet into answer file!"
return 1 return 1
fi fi
else if ! chmod --reference="$asset" "$result" ||
! mv -f "$result" "$asset"; then
if ! xmlstarlet ed \ rm -rf "$tmp"
-s "$setup" \
-t elem \
-n "UseConfigurationSet" \
-v "true" \
"$asset" > "$tmp"; then
rm -f "$tmp"
error "Failed to append UseConfigurationSet to answer file!"
return 1
fi
fi
fi
if ! chmod --reference="$asset" "$tmp" ||
! mv -f "$tmp" "$asset"; then
rm -f "$tmp"
error "Failed to replace the updated answer file!" error "Failed to replace the updated answer file!"
return 1 return 1
fi fi
rm -rf "$tmp" || return 1
return 0 return 0
} }
@@ -1527,10 +1704,11 @@ removeSharedFolder() {
removeLocalAccount() { removeLocalAccount() {
local asset="$1" local asset="$1"
local ns="urn:schemas-microsoft-com:unattend"
if ! sed -i -E \ if ! xmlstarlet ed -L \
-e '/^[[:space:]]*<LocalAccounts([[:space:]>])/,/^[[:space:]]*<\/LocalAccounts>[[:space:]]*$/d' \ -N "u=$ns" \
-e '/^[[:space:]]*<AdministratorPassword([[:space:]>])/,/^[[:space:]]*<\/AdministratorPassword>[[:space:]]*$/d' \ -d '//u:LocalAccounts | //u:AdministratorPassword' \
"$asset"; then "$asset"; then
error "Failed to remove local account configuration from answer file!" error "Failed to remove local account configuration from answer file!"
@@ -1630,19 +1808,6 @@ validateLegacyUsername() {
return 0 return 0
} }
escapeXMLSed() {
local s
s=$(escapeXML "$1") || return 1
s=${s//\\/\\\\}
s=${s//&/\\&}
s=${s//|/\\|}
printf '%s' "$s"
return 0
}
escapeSIFValue() { escapeSIFValue() {
local s="$1" local s="$1"