This post walks through a small, direct libvirt workflow for installing Windows Server from an ISO, enabling SSH during first boot, capturing the result as a reusable qcow2 base image, and creating disposable VM instances from that base.

The examples use libvirt’s QEMU backend through qemu:///system, driven by virt-install and virsh.

The example uses Windows Server 2022 Evaluation, but the shape of the workflow is not tied to that particular release. The moving parts are plain Windows unattended setup, a first-boot PowerShell script, virt-install, and qcow2 overlays.

The goal is intentionally modest: one local Windows Server image that can be used to create disposable VMs quickly, without introducing an image-factory framework.

This is a local development workflow. The example password is intentionally visible because it is baked into the unattended install file; change it before using the process anywhere less disposable.

§Layout

The workspace is rooted at:

1
~/vms

The final workspace looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~/vms/
|-- README.md
|-- isos/
| |-- windows-server-2022-eval.iso
| `-- virtio-win.iso
|-- autoinstall/
| |-- Autounattend.xml
| |-- first-boot.ps1
| `-- autoinstall.iso
|-- scripts/
| |-- winvm-build-autoinstall-iso
| |-- winvm-build-ssh-base
| |-- winvm-create
| `-- winvm-ip
|-- images/
| `-- windows-server-2022-ssh-base.qcow2
`-- instances/
`-- <disposable-vm>.qcow2

The flow is simple: the installer ISOs plus the autoinstall files produce a base image under images/; disposable VMs are qcow2 overlays under instances/.

The base image produced by this process has:

1
2
3
4
5
User: Administrator
Password: qwf123!@#
SSH: enabled, password auth
Disk bus: SATA
NIC model: e1000e

No host SSH public key is baked into the image. SSH access uses the Administrator password unless you add your own key later.

§1. Download The ISOs

The host needs a working libvirt/QEMU setup with virt-install, virsh, qemu-img, and xorriso available.

Create an ISO directory:

1
mkdir -p ~/vms/isos

Download the Windows Server evaluation ISO from Microsoft:

1
https://www.microsoft.com/en-us/evalcenter/download-windows-server-2022

Save it as:

1
~/vms/isos/windows-server-2022-eval.iso

Download the Fedora virtio-win driver/tools ISO:

1
https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso

Save it as:

1
~/vms/isos/virtio-win.iso

The copy used while testing this post was:

1
2
virtio-win-0.1.285
sha256=e14cf2b94492c3e925f0070ba7fdfedeb2048c91eea9c5a5afb30232a3976331

The VM deliberately uses SATA storage and an e1000e NIC so Windows can install and obtain network access without needing VirtIO drivers during setup. The virtio ISO is still attached because first-boot.ps1 installs virtio-win-guest-tools.exe after Windows has booted.

§2. Prepare The Autoinstall Files

The unattended install is driven by:

1
~/vms/autoinstall/Autounattend.xml

This file answers the Windows setup prompts, partitions the disk, sets the Administrator password, enables autologon, and runs a first-logon command:

1
powershell.exe ... first-boot.ps1

The first-boot script is:

1
~/vms/autoinstall/first-boot.ps1

That script is the guest-side provisioning entrypoint. It does three things:

  1. Installs VirtIO guest tools if virtio-win-guest-tools.exe is present.
  2. Installs and starts OpenSSH Server.
  3. Writes readiness markers under C:\setup.

The readiness markers are:

1
2
C:\setup\ssh-ready.txt
C:\setup\ready.txt

§3. Build The Autoinstall ISO

Bundle the unattended setup files into a small ISO:

1
~/vms/scripts/winvm-build-autoinstall-iso

The command creates:

1
~/vms/autoinstall/autoinstall.iso

The ISO intentionally contains only two files:

1
2
Autounattend.xml
first-boot.ps1

§4. Install Windows And Provision SSH

Start a fresh Windows install VM:

1
~/vms/scripts/winvm-build-ssh-base

The script creates a new disk:

1
~/vms/instances/win2022-ssh-base-build.qcow2

and starts a libvirt VM named:

1
win2022-ssh-base-build

The build VM uses:

1
2
3
4
5
machine: q35
firmware: UEFI
disk: SATA qcow2
network: e1000e on libvirt default network
graphics: SPICE/qxl

The script attaches three CD-ROMs to the VM:

1
2
3
Windows Server 2022 ISO
autoinstall.iso
virtio-win.iso

It also sends Enter keypresses for the Windows ISO boot prompt:

1
Press any key to boot from CD or DVD

Without those keypresses, a headless install can sit at that prompt forever.

Windows setup may power off once during installation. If that happens, start the same VM again and let first-boot provisioning continue:

1
virsh -c qemu:///system start win2022-ssh-base-build

A typical full build takes about 15-30 minutes, depending on host load and Windows setup timing.

§5. Verify SSH

Once first-boot provisioning has finished, get the VM IP:

1
2
ip=$(~/vms/scripts/winvm-ip win2022-ssh-base-build)
echo "$ip"

Verify SSH with a real login:

1
ssh Administrator@"$ip" hostname

Password:

1
qwf123!@#

Expected output:

1
WIN-OJDK-BASE

§6. Capture The Base Image

After SSH works, shut down the build VM:

1
virsh -c qemu:///system shutdown win2022-ssh-base-build

After it is shut off, undefine the temporary libvirt domain and move the disk into images:

1
2
3
virsh -c qemu:///system undefine win2022-ssh-base-build --nvram
mv ~/vms/instances/win2022-ssh-base-build.qcow2 \
~/vms/images/windows-server-2022-ssh-base.qcow2

If you are replacing an existing base image, move the old file aside first.

That qcow2 file is now the reusable base image.

§7. Create A Disposable VM From The Base

Create and start a disposable VM from the base image:

1
~/vms/scripts/winvm-create ssh-test

This creates a qcow2 overlay:

1
~/vms/instances/ssh-test.qcow2

It also defines the libvirt domain and starts the VM immediately. You do not need a separate virsh start after winvm-create.

You can see the running VM with:

1
virsh -c qemu:///system list --all

The overlay is backed by the base image:

1
~/vms/images/windows-server-2022-ssh-base.qcow2

Get the VM IP:

1
2
ip=$(~/vms/scripts/winvm-ip ssh-test)
echo "$ip"

Connect with SSH:

1
ssh Administrator@"$ip"

§8. Clean Up A Disposable VM

Gracefully shut down when possible:

1
virsh -c qemu:///system shutdown ssh-test

Force power off only if needed:

1
virsh -c qemu:///system destroy ssh-test

Delete the disposable VM and its overlay disk:

1
2
virsh -c qemu:///system undefine ssh-test --nvram
rm -f ~/vms/instances/ssh-test.qcow2

§Appendix: Source Files

These are the complete source files used by the workflow above. Paths are relative to ~/vms.

§autoinstall/Autounattend.xml

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
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DiskConfiguration>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>EFI</Type>
<Size>100</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order>
<Type>MSR</Type>
<Size>16</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order>
<Type>Primary</Type>
<Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Format>FAT32</Format>
<Label>System</Label>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>3</PartitionID>
<Format>NTFS</Format>
<Label>Windows</Label>
<Letter>C</Letter>
</ModifyPartition>
</ModifyPartitions>
</Disk>
<WillShowUI>OnError</WillShowUI>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/INDEX</Key>
<Value>2</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
<WillShowUI>OnError</WillShowUI>
</OSImage>
</ImageInstall>
<UserData>
<AcceptEula>true</AcceptEula>
<FullName>Administrator</FullName>
<Organization>OpenJDK</Organization>
</UserData>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ComputerName>WIN-OJDK-BASE</ComputerName>
<TimeZone>UTC</TimeZone>
</component>
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<fDenyTSConnections>false</fDenyTSConnections>
</component>
<component name="Microsoft-Windows-ServerManager-SvrMgrNc" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DoNotOpenServerManagerAtLogon>true</DoNotOpenServerManagerAtLogon>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<UserAccounts>
<AdministratorPassword>
<Value>qwf123!@#</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
</UserAccounts>
<AutoLogon>
<Enabled>true</Enabled>
<Username>Administrator</Username>
<LogonCount>5</LogonCount>
<Password>
<Value>qwf123!@#</Value>
<PlainText>true</PlainText>
</Password>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Run first boot provisioning</Description>
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "foreach ($d in 'D','E','F','G','A') { $p = $d + ':\first-boot.ps1'; if (Test-Path $p) { &amp; $p; break } }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>

§autoinstall/first-boot.ps1

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
$ErrorActionPreference = 'Continue'

New-Item -ItemType Directory -Force C:\setup | Out-Null

# Install virtio guest tools if the virtio ISO is attached.
foreach ($drive in Get-PSDrive -PSProvider FileSystem) {
$installer = Join-Path $drive.Root 'virtio-win-guest-tools.exe'
if (Test-Path $installer) {
Start-Process -FilePath $installer -ArgumentList '/quiet', '/norestart' -Wait
break
}
}

$oldErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
try {
try {
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
} catch {
Write-Warning "OpenSSH capability install failed or was already installed: $_"
}

Start-Service sshd
Set-Service -Name sshd -StartupType Automatic

if (-not (Get-NetFirewallRule -Name sshd -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
}

$sshdConfig = 'C:\ProgramData\ssh\sshd_config'
if (Test-Path $sshdConfig) {
$content = Get-Content $sshdConfig
$content = $content -replace '^#?PasswordAuthentication\s+.*$', 'PasswordAuthentication yes'
$content = $content -replace '^#?PubkeyAuthentication\s+.*$', 'PubkeyAuthentication yes'
$content | Set-Content $sshdConfig
}

try { Restart-Service sshd -Force } catch {}

Set-Content C:\setup\ssh-ready.txt (Get-Date -Format o)
} catch {
Set-Content C:\setup\ssh-error.txt $_
throw
} finally {
$ErrorActionPreference = $oldErrorActionPreference
}

try {
New-Item -ItemType Directory -Force 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp.disabled' | Out-Null
New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\ServerManager' -Name DoNotOpenServerManagerAtLogon -PropertyType DWORD -Value 1 -Force | Out-Null
} catch {}

Set-Content C:\setup\ready.txt (Get-Date -Format o)

§scripts/winvm-build-autoinstall-iso

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
root=$(cd -- "${script_dir}/.." && pwd)
out=${1:-${root}/autoinstall/autoinstall.iso}
WORK=$(mktemp -d)
trap 'rm -rf "${WORK}"' EXIT

cp "${root}/autoinstall/first-boot.ps1" "${WORK}/first-boot.ps1"
cp "${root}/autoinstall/Autounattend.xml" "${WORK}/Autounattend.xml"

xorriso -as mkisofs -iso-level 3 -J -r -V AUTOINSTALL -o "${out}" "${WORK}" >/dev/null
echo "${out}"

§scripts/winvm-build-ssh-base

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
#!/usr/bin/env bash
set -euo pipefail

LIBVIRT_URI=${LIBVIRT_URI:-qemu:///system}
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
VM_ROOT=${VM_ROOT:-$(cd -- "${script_dir}/.." && pwd)}
ISO_DIR=${ISO_DIR:-${VM_ROOT}/isos}
INSTANCES_DIR=${INSTANCES_DIR:-${VM_ROOT}/instances}

die() {
echo "error: $*" >&2
exit 1
}

name=${1:-win2022-ssh-base-build}
disk=${INSTANCES_DIR}/${name}.qcow2
win_iso=${ISO_DIR}/windows-server-2022-eval.iso
virtio_iso=${ISO_DIR}/virtio-win.iso
autoinstall_iso=${AUTOINSTALL_ISO:-${VM_ROOT}/autoinstall/autoinstall.iso}

[[ -f "${win_iso}" ]] || die "missing Windows ISO: ${win_iso}"
[[ -f "${virtio_iso}" ]] || die "missing virtio ISO: ${virtio_iso}"
[[ -f "${autoinstall_iso}" ]] || die "missing autoinstall ISO: ${autoinstall_iso}"
if virsh -c "${LIBVIRT_URI}" dominfo "${name}" >/dev/null 2>&1; then
die "VM already exists: ${name}"
fi
[[ ! -e "${disk}" ]] || die "disk already exists: ${disk}"

mkdir -p "${INSTANCES_DIR}"
qemu-img create -f qcow2 "${disk}" 80G

env PYTHONNOUSERSITE=1 PATH="/usr/sbin:/usr/bin:/sbin:/bin" /usr/bin/python3 /usr/bin/virt-install \
--connect "${LIBVIRT_URI}" \
--name "${name}" \
--memory "${WINVM_MEMORY:-12288}" \
--vcpus "${WINVM_CPUS:-6}" \
--cpu host-passthrough \
--machine q35 \
--os-variant win2k22 \
--boot uefi \
--disk "path=${disk},format=qcow2,bus=sata,cache=none,discard=unmap" \
--disk "path=${autoinstall_iso},device=cdrom" \
--disk "path=${virtio_iso},device=cdrom" \
--cdrom "${win_iso}" \
--network "network=default,model=e1000e" \
--graphics spice \
--video qxl \
--channel spicevmc \
--noautoconsole

# Windows ISO boot prompts with "Press any key to boot from CD or DVD".
# Send a few Enter keypresses so unattended installs do not stall there.
for _ in 1 2 3; do
sleep 2
virsh -c "${LIBVIRT_URI}" send-key "${name}" KEY_ENTER >/dev/null 2>&1 || true
done

cat <<EOF
${name}

The unattended Windows install is now running. Typical elapsed time is about
15-30 minutes, depending on host load and Windows setup timing.

This script already sends Enter keypresses for the Windows ISO prompt:
Press any key to boot from CD or DVD

During setup the VM may power off after an installation phase. If that happens,
start it again and let Windows continue first-boot provisioning:
virsh -c ${LIBVIRT_URI} start ${name}

Wait for installation and first-boot provisioning to finish, then verify SSH:
${script_dir}/winvm-ip ${name}
ssh Administrator@"\$(${script_dir}/winvm-ip ${name})" hostname

Readiness markers inside the guest are:
C:\setup\ssh-ready.txt
C:\setup\ready.txt

Administrator password: qwf123!@#
EOF

§scripts/winvm-create

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
#!/usr/bin/env bash
set -euo pipefail

LIBVIRT_URI=${LIBVIRT_URI:-qemu:///system}
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
VM_ROOT=${VM_ROOT:-$(cd -- "${script_dir}/.." && pwd)}
IMAGES_DIR=${IMAGES_DIR:-${VM_ROOT}/images}
INSTANCES_DIR=${INSTANCES_DIR:-${VM_ROOT}/instances}

usage() {
cat >&2 <<'EOF'
usage: winvm-create <vm-name> [base-image-name-or-path] [disk-size]

Defaults:
base image: windows-server-2022-ssh-base.qcow2
disk bus: sata
NIC model: e1000e

Override devices with WINVM_DISK_BUS=... and WINVM_NIC_MODEL=...
EOF
exit 2
}

die() {
echo "error: $*" >&2
exit 1
}

[[ $# -ge 1 && $# -le 3 ]] || usage

name=$1
base=${2:-windows-server-2022-ssh-base.qcow2}
disk_size=${3:-}
disk_bus=${WINVM_DISK_BUS:-sata}
nic_model=${WINVM_NIC_MODEL:-e1000e}

[[ "${base}" = /* ]] || base="${IMAGES_DIR}/${base}"
[[ -f "${base}" ]] || die "base image not found: ${base}"

if virsh -c "${LIBVIRT_URI}" dominfo "${name}" >/dev/null 2>&1; then
die "VM already exists: ${name}"
fi

mkdir -p "${INSTANCES_DIR}"
disk="${INSTANCES_DIR}/${name}.qcow2"
[[ ! -e "${disk}" ]] || die "disk already exists: ${disk}"

qemu-img create -f qcow2 -F qcow2 -b "${base}" "${disk}" ${disk_size:+"${disk_size}"}

env PYTHONNOUSERSITE=1 PATH="/usr/sbin:/usr/bin:/sbin:/bin" /usr/bin/python3 /usr/bin/virt-install \
--connect "${LIBVIRT_URI}" \
--name "${name}" \
--memory "${WINVM_MEMORY:-12288}" \
--vcpus "${WINVM_CPUS:-6}" \
--cpu host-passthrough \
--machine q35 \
--os-variant win2k22 \
--boot uefi \
--disk "path=${disk},format=qcow2,bus=${disk_bus},cache=none,discard=unmap" \
--network "network=default,model=${nic_model}" \
--graphics spice \
--video qxl \
--channel spicevmc \
--import \
--noautoconsole

echo "${name}"

§scripts/winvm-ip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash
set -euo pipefail

LIBVIRT_URI=${LIBVIRT_URI:-qemu:///system}

die() {
echo "error: $*" >&2
exit 1
}

[[ $# -eq 1 ]] || die "usage: winvm-ip <vm-name>"
name=$1

mac=$(virsh -c "${LIBVIRT_URI}" domiflist "${name}" \
| awk '$2 == "network" { mac=$5 } END { print mac }')
[[ -n "${mac}" ]] || exit 1

ip=$(virsh -c "${LIBVIRT_URI}" net-dhcp-leases default \
| awk -v mac="${mac}" '$0 ~ mac { ip=$5 } END { sub(/\/.*$/, "", ip); print ip }')
[[ -n "${ip}" ]] || exit 1

echo "${ip}"