<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Zoltan Toma</title>
    <description>The latest articles on Forem by Zoltan Toma (@leeshan87).</description>
    <link>https://forem.com/leeshan87</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F908988%2F503affb5-f0a5-47a2-a778-2ceb90b12df4.png</url>
      <title>Forem: Zoltan Toma</title>
      <link>https://forem.com/leeshan87</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/leeshan87"/>
    <language>en</language>
    <item>
      <title>Implementing Missing Integration Tests: Provisioners and Docker</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/implementing-missing-integration-tests-provisioners-and-docker-177i</link>
      <guid>https://forem.com/leeshan87/implementing-missing-integration-tests-provisioners-and-docker-177i</guid>
      <description>&lt;h2&gt;
  
  
  Finishing What We Started
&lt;/h2&gt;

&lt;p&gt;The Vagrant WSL2 provider had a &lt;code&gt;test/integration_old/todos/&lt;/code&gt; folder with unimplemented tests. Two big ones: &lt;code&gt;test_provisioners.ps1&lt;/code&gt; and &lt;code&gt;test_docker.ps1&lt;/code&gt;. Both just had TODO comments saying what they should test.&lt;/p&gt;

&lt;p&gt;We’d already migrated the working tests to Pester 5.x format. The old homegrown PowerShell scripts worked fine - they just weren’t using a framework. Now we needed to implement these missing tests using the same Pester patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provisioners: One Monolith, Five Tests
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;examples/provisioners/&lt;/code&gt; directory had one massive Vagrantfile testing shell, file, ansible, chef, and salt provisioners all at once. To test them individually, we needed to split them up.&lt;/p&gt;

&lt;p&gt;Created a subfolder for each provisioner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;examples/provisioners/
├── shell/
│ └── Vagrantfile
├── file/
│ ├── Vagrantfile
│ └── test-file.txt
├── ansible/
│ ├── Vagrantfile
│ └── test-playbook.yml
├── chef/
│ ├── Vagrantfile
│ └── cookbooks/
└── salt/
    ├── Vagrantfile
    └── states/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each subfolder has its own self-contained Vagrantfile that demonstrates one provisioner. This makes them usable as examples and testable individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provisioners.Tests.ps1
&lt;/h2&gt;

&lt;p&gt;The Pester test defines separate &lt;code&gt;Describe&lt;/code&gt; blocks for each provisioner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Provisioners.Tests.ps1
param([switch]$Full)

Describe "Vagrant WSL2 Provider - Shell Provisioner" {
    BeforeAll {
        $script:ExampleDir = Join-Path $PSScriptRoot "..\..\examples\provisioners\shell"
        $script:DistributionName = "vagrant-wsl2-shell-test"
        Push-Location $script:ExampleDir
        vagrant destroy -f 2&amp;gt;$null | Out-Null
    }

    AfterAll {
        vagrant destroy -f 2&amp;gt;$null | Out-Null
        Pop-Location
    }

    Context "When provisioning with shell scripts" {
        It "Should successfully run 'vagrant up --provider=wsl2'" {
            vagrant up --provider=wsl2
            $LASTEXITCODE | Should -Be 0
        }

        It "Should have created shell-test.txt file" {
            $result = vagrant ssh -c "test -f /home/vagrant/shell-test.txt &amp;amp;&amp;amp; echo 'exists'" 2&amp;gt;&amp;amp;1
            $result -join "`n" | Should -Match "exists"
        }
    }
}

# Separate Describe blocks for file, ansible, chef, salt...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick mode runs shell, file, and ansible (the working provisioners). Full mode adds chef and salt, which have known issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Describe "Vagrant WSL2 Provider - Chef Solo Provisioner" -Tag "KnownIssue" -Skip:(-not $Full) {
    # Tests that we expect to fail due to chef symlink issues
}

Describe "Vagrant WSL2 Provider - SaltStack Provisioner" -Tag "KnownIssue" -Skip:(-not $Full) {
    # Tests that we expect to fail due to bootstrap URL changes
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-Skip:(-not $Full)&lt;/code&gt; means these only run when you pass &lt;code&gt;-Full&lt;/code&gt;. Chef and Salt are documented to fail, but the tests are there to track the issues.&lt;/p&gt;

&lt;p&gt;Rakefile tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rake test_provisioners # Test shell, file, and ansible
rake test_provisioners_full # Test all including chef and salt

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker: 22 Distributions, Individual Machines
&lt;/h2&gt;

&lt;p&gt;The Docker test validates that Docker installs and runs on different WSL distributions. The old &lt;code&gt;examples/docker-test/Vagrantfile&lt;/code&gt; used a loop that auto-created all VMs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Old approach
DISTRIBUTIONS.each do |distro|
  config.vm.define "docker-#{distro}" do |node|
    # auto-created on vagrant up
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn’t work for selective testing. We refactored to individual machine definitions with &lt;code&gt;autostart: false&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# New approach
config.vm.define "ubuntu2404", autostart: false do |node|
  node.vm.box = "Ubuntu-24.04"
  node.vm.provider "wsl2" do |wsl|
    wsl.distribution_name = "vagrant-docker-ubuntu2404"
    wsl.version = 2
    wsl.systemd = true
  end
  node.vm.provision "ansible_local" do |ansible|
    ansible.playbook = "provisioning/docker-debian.yml"
  end
end

config.vm.define "debian", autostart: false do |node|
  # ...
end

# ... 22 machines total

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run &lt;code&gt;vagrant up ubuntu2404&lt;/code&gt; to test just one, or the Pester test can selectively spin up what it needs.&lt;/p&gt;

&lt;p&gt;The test uses the same quick/full pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Docker.Tests.ps1
param([switch]$Full)

BeforeAll {
    $script:QuickMachines = @(
        @{ Name = "ubuntu2404"; Distro = "vagrant-docker-ubuntu2404" },
        @{ Name = "debian"; Distro = "vagrant-docker-debian" },
        @{ Name = "almalinux8"; Distro = "vagrant-docker-almalinux8" }
    )

    $script:AllMachines = @(
        # All 22 distributions
    )

    $script:TestMachines = if ($Full) {
        Write-Host "Running FULL Docker test (all $($script:AllMachines.Count) distributions)"
        $script:AllMachines
    } else {
        Write-Host "Running QUICK Docker test ($($script:QuickMachines.Count) distributions)"
        $script:QuickMachines
    }
}

Describe "Vagrant WSL2 Provider - Docker Support" {
    Context "When testing Docker installation across distributions" {
        It "Should successfully bring up &amp;lt;Name&amp;gt; with Docker" -ForEach $script:TestMachines {
            vagrant up $Name --provider=wsl2
            $LASTEXITCODE | Should -Be 0
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rakefile tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rake test_docker # 3 distros
rake test_docker_full # 22 distros

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Parameter Passing Bug
&lt;/h2&gt;

&lt;p&gt;To pass the &lt;code&gt;-Full&lt;/code&gt; parameter from &lt;code&gt;Invoke-PesterTests.ps1&lt;/code&gt; to the test scripts, we had broken code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# This doesn't work - ContainerParameters doesn't exist in Pester 5.x
$config.Run.ContainerParameters = @{ Full = $true }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix uses &lt;code&gt;New-PesterContainer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Invoke-PesterTests.ps1
if ($Full) {
    $container = New-PesterContainer -Path $TestPath -Data @{ Full = $true }
    $config.Run.Container = $container
} else {
    $config.Run.Path = $TestPath
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the correct way to pass parameters to Pester 5.x test scripts. Works for AllDistributions, Provisioners, and Docker tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Still Broken
&lt;/h2&gt;

&lt;p&gt;The Docker test isn’t discovering tests yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Discovery found 0 tests in 211ms.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-ForEach $script:TestMachines&lt;/code&gt; parameter binding isn’t working. Something’s wrong with how we’re structuring the test data or the &lt;code&gt;Describe&lt;/code&gt; block. That’s next session’s problem - we’re out of context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Shipped
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Provisioners.Tests.ps1&lt;/strong&gt; - Tests all 5 provisioners (shell, file, ansible, chef, salt)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactored provisioner examples&lt;/strong&gt; - Individual Vagrantfiles for each provisioner&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker.Tests.ps1&lt;/strong&gt; - Tests Docker across distributions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactored docker-test Vagrantfile&lt;/strong&gt; - Individual machine definitions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fixed parameter passing&lt;/strong&gt; - &lt;code&gt;Invoke-PesterTests.ps1&lt;/code&gt; now uses &lt;code&gt;New-PesterContainer&lt;/code&gt; properly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Updated Rakefile&lt;/strong&gt; - New test tasks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Updated CLAUDE.md&lt;/strong&gt; - Documented the new tests and structure&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The TODO tests are no longer TODO. They’re implemented - just need to fix the Docker test discovery issue.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Vagrant WSL2 Provider is an MIT-licensed open source project. Check it out on &lt;a href="https://github.com/leemac/vagrant-wsl2-provider" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pester</category>
      <category>powershell</category>
      <category>testing</category>
      <category>vagrant</category>
    </item>
    <item>
      <title>Migrating Integration Tests to Pester: A PowerShell Testing Journey</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/migrating-integration-tests-to-pester-a-powershell-testing-journey-4cn1</link>
      <guid>https://forem.com/leeshan87/migrating-integration-tests-to-pester-a-powershell-testing-journey-4cn1</guid>
      <description>&lt;h2&gt;
  
  
  The Testing Problem
&lt;/h2&gt;

&lt;p&gt;The vagrant-wsl2-provider had integration tests. They worked. They ran actual Vagrant commands and verified WSL2 distributions got created properly. But they were all custom PowerShell scripts with manual error handling, inconsistent output, and no real test framework.&lt;/p&gt;

&lt;p&gt;Here’s what a typical test looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try {
    Write-Host "Test: Starting VM..." -ForegroundColor Yellow
    vagrant up
    if ($LASTEXITCODE -ne 0) {
        throw "vagrant up failed"
    }
    Write-Host "[PASS] VM started" -ForegroundColor Green
} catch {
    Write-Host "[FAIL] Test failed" -ForegroundColor Red
    exit 1
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked, but it was painful to maintain. Each test file had its own error handling, cleanup logic, and output formatting. And when something failed, you got a wall of red text with no clear indication of which specific assertion broke.&lt;/p&gt;

&lt;p&gt;Time to modernize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Pester?
&lt;/h2&gt;

&lt;p&gt;Pester is PowerShell’s native testing framework. It’s been around since 2011, and version 5.x brought major improvements. More importantly, it’s what PowerShell developers actually use.&lt;/p&gt;

&lt;p&gt;The benefits were clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured tests&lt;/strong&gt; - &lt;code&gt;Describe&lt;/code&gt;, &lt;code&gt;Context&lt;/code&gt;, &lt;code&gt;It&lt;/code&gt; blocks instead of ad-hoc scripts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better assertions&lt;/strong&gt; - &lt;code&gt;Should -Be&lt;/code&gt; instead of manual &lt;code&gt;if ($LASTEXITCODE -ne 0)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proper output&lt;/strong&gt; - Test results with timing, pass/fail counts, and detailed failure info&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BeforeAll/AfterAll&lt;/strong&gt; - Consistent setup and cleanup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip support&lt;/strong&gt; - Conditionally skip tests (critical for admin-required tests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, Pester integrates with CI/CD tools, has good VS Code support, and produces JUnit XML reports if you need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration Strategy
&lt;/h2&gt;

&lt;p&gt;I didn’t want to rewrite everything from scratch. The existing tests worked and covered real functionality. So the plan was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move to test/pester/&lt;/strong&gt; directory - Keep legacy tests in test/integration/ for reference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert one test at a time&lt;/strong&gt; - Start simple, learn patterns, iterate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the same examples/&lt;/strong&gt; structure - Tests still run against actual Vagrantfiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add proper cleanup&lt;/strong&gt; - BeforeAll/AfterAll for consistent environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle Windows quirks&lt;/strong&gt; - Admin privileges, path issues, PowerShell redirection&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Test Structure: The Pester Way
&lt;/h2&gt;

&lt;p&gt;Here’s what the basic test structure looked like after conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeforeAll {
    $script:ExampleDir = Join-Path $PSScriptRoot "..\..\examples\basic"
    Push-Location $script:ExampleDir

    # Cleanup before tests
    vagrant destroy -f 2&amp;gt;$null | Out-Null
}

AfterAll {
    # Cleanup after all tests
    vagrant destroy -f 2&amp;gt;$null | Out-Null
    Pop-Location
}

Describe "Vagrant WSL2 Provider - Basic Operations" {
    Context "When creating a new VM" {
        It "Should successfully run 'vagrant up --provider=wsl2'" {
            vagrant up --provider=wsl2
            $LASTEXITCODE | Should -Be 0
        }

        It "Should create WSL distribution" {
            $wslList = (wsl -l -v | Out-String) -replace '\0', ''
            $wslList | Should -Match "vagrant-wsl2-basic"
        }
    }

    Context "When destroying the VM" {
        It "Should successfully run 'vagrant destroy -f'" {
            vagrant destroy -f
            $LASTEXITCODE | Should -Be 0
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Readable. Each &lt;code&gt;It&lt;/code&gt; block is a single assertion. Context blocks group related tests. And &lt;code&gt;BeforeAll&lt;/code&gt;/&lt;code&gt;AfterAll&lt;/code&gt; handle setup and cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 1: Administrator Privileges
&lt;/h2&gt;

&lt;p&gt;Several tests require administrator privileges - creating VHDs for data disks, configuring network adapters, mounting disks in WSL2. The old tests just checked permissions and exited with a message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (-not $isAdmin) {
    Write-Host "SKIPPED: Administrator required"
    exit 0
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But with Pester, you want the tests to show up in the results, not just silently exit. The solution was Pester’s &lt;code&gt;-Skip&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Check admin rights early for informative message
$isAdminCheck = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if (-not $isAdminCheck) {
    Write-Host ""
    Write-Host "=== Data Disk Test SKIPPED ===" -ForegroundColor Yellow
    Write-Host "Reason: Administrator privileges required" -ForegroundColor Yellow
    Write-Host ""
    Write-Host "Data disk features require:" -ForegroundColor Gray
    Write-Host " - VHD creation (New-VHD cmdlet)" -ForegroundColor Gray
    Write-Host " - WSL disk mounting (wsl --mount)" -ForegroundColor Gray
    Write-Host ""
    Write-Host "To run this test, please restart PowerShell as Administrator" -ForegroundColor Cyan
    Write-Host ""
}

Describe "Vagrant WSL2 Provider - Data Disk" -Skip:(-not $isAdminCheck) -Tag @('RequiresAdmin') {
    # Tests run only if admin
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Non-admin users see a clear message explaining why tests are skipped&lt;/li&gt;
&lt;li&gt;Tests appear in Pester output as skipped (not hidden)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;-Tag @('RequiresAdmin')&lt;/code&gt; lets you filter tests by privilege level&lt;/li&gt;
&lt;li&gt;Running as admin? Tests execute normally&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; The admin check happens at file scope before Pester even loads, so the message appears immediately. Then Pester sees the &lt;code&gt;-Skip&lt;/code&gt; flag and marks all tests as skipped. Best of both worlds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Challenge 2: Directory Management Hell
&lt;/h2&gt;

&lt;p&gt;This one was subtle. The tests need to run in the example directory (where the Vagrantfile lives). Easy enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeforeAll {
    Push-Location $script:ExampleDir
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here’s the trap: if the test is skipped due to admin privileges, &lt;code&gt;BeforeAll&lt;/code&gt; still runs. And if you put the &lt;code&gt;Push-Location&lt;/code&gt; after the admin check with an early &lt;code&gt;return&lt;/code&gt;, the directory never changes, but the tests try to run anyway.&lt;/p&gt;

&lt;p&gt;Result? &lt;code&gt;vagrant&lt;/code&gt; commands fail with “A Vagrant environment or target machine is required” because you’re in the wrong directory.&lt;/p&gt;

&lt;p&gt;The fix was to always &lt;code&gt;Push-Location&lt;/code&gt;, even for skipped tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeforeAll {
    $script:ExampleDir = Join-Path $PSScriptRoot "..\..\examples\data-disk"

    # Store admin status for skip condition
    $script:isAdmin = $isAdminCheck

    # Always push location, even if skipping (for proper cleanup)
    Push-Location $script:ExampleDir

    if (-not $script:isAdmin) {
        return # Skip setup, but directory is already changed
    }

    # Cleanup before tests (only if admin)
    vagrant destroy -f 2&amp;gt;$null
}

AfterAll {
    if ($script:isAdmin) {
        # Cleanup after all tests
        vagrant destroy -f 2&amp;gt;$null
    }

    # Always pop location
    Pop-Location
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the directory state is always consistent, whether tests run or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 3: PowerShell Output Redirection
&lt;/h2&gt;

&lt;p&gt;I’ve been writing PowerShell for years and I still get this wrong.&lt;/p&gt;

&lt;p&gt;The old tests had things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$output = vagrant up --provider=wsl2 2&amp;gt;&amp;amp;1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable, right? Redirect stderr to stdout, capture everything. Except in PowerShell, &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; at the end of a command doesn’t work the way Bash users expect.&lt;/p&gt;

&lt;p&gt;For cleanup commands piped to &lt;code&gt;Out-Null&lt;/code&gt;, you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant destroy -f 2&amp;gt;$null | Out-Null

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant destroy -f 2&amp;gt;&amp;amp;1 | Out-Null # Wrong!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;2&amp;gt;$null&lt;/code&gt; redirects stderr to null, and &lt;code&gt;| Out-Null&lt;/code&gt; discards stdout. Together they suppress all output.&lt;/p&gt;

&lt;p&gt;And for capturing multi-line output to match against patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ipCheck = (wsl -d vagrant-wsl2-networking-demo -- ip addr show eth0) -join "`n"
$ipCheck | Should -Match "192.168.33.10"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-join "&lt;/code&gt;n"` is critical. Without it, you’re matching against an array, and Pester only shows the first element in error messages. With the join, you get a single string with all lines, and regex matching works across the entire output.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; We spent a solid 20 minutes debugging why IP address checks were failing, only to discover the output was there, just on line 4 instead of line 1. Classic PowerShell array behavior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Fast/Slow Distribution Test
&lt;/h2&gt;

&lt;p&gt;The AllDistributions test validates which WSL distributions work with the provider. There are 22 distributions available from &lt;code&gt;wsl -l -o&lt;/code&gt;. Testing all of them takes forever.&lt;/p&gt;

&lt;p&gt;So I added a parameter to control test scope:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
param(&lt;br&gt;
    [switch]$Full&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;BeforeAll {&lt;br&gt;
    # Quick subset for fast testing (different distro families)&lt;br&gt;
    $script:QuickDistributions = @(&lt;br&gt;
        "Ubuntu-24.04", # Debian-based&lt;br&gt;
        "Debian", # Pure Debian&lt;br&gt;
        "AlmaLinux-8" # RHEL-based&lt;br&gt;
    )&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# All distributions
$script:AllDistributions = @(
    "Ubuntu-24.04",
    "Debian",
    "AlmaLinux-8",
    # ... 19 more distributions
)

# Select which to test based on parameter
$script:WslDistributions = if ($Full) {
    $script:AllDistributions
} else {
    $script:QuickDistributions
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now you can run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
rake test_all_distributions # Quick: 3 distributions&lt;br&gt;
rake test_all_distributions_full # Full: 22 distributions&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The quick test covers the major distro families (Debian, Ubuntu, RHEL) and runs in minutes. The full test is for pre-release validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Invoke-PesterTests Runner
&lt;/h2&gt;

&lt;p&gt;To make all this work, I needed a centralized test runner that could pass parameters through:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;br&gt;
param(&lt;br&gt;
    [string]$TestFile = $null,&lt;br&gt;
    [ValidateSet('Detailed', 'Normal', 'Minimal')]&lt;br&gt;
    [string]$Output = 'Detailed',&lt;br&gt;
    [switch]$Full # For AllDistributions: run all distributions&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;$ErrorActionPreference = "Stop"&lt;/p&gt;

&lt;h1&gt;
  
  
  Ensure Pester 5.x is imported
&lt;/h1&gt;

&lt;p&gt;Import-Module Pester -MinimumVersion 5.0 -ErrorAction Stop&lt;/p&gt;

&lt;p&gt;$TestPath = $PSScriptRoot&lt;/p&gt;

&lt;p&gt;if ($TestFile) {&lt;br&gt;
    $TestPath = Join-Path $TestPath "$TestFile.Tests.ps1"&lt;br&gt;
}&lt;/p&gt;

&lt;h1&gt;
  
  
  Build Pester configuration
&lt;/h1&gt;

&lt;p&gt;$config = New-PesterConfiguration&lt;br&gt;
$config.Run.Path = $TestPath&lt;br&gt;
$config.Output.Verbosity = $Output&lt;br&gt;
$config.Run.PassThru = $true&lt;/p&gt;

&lt;h1&gt;
  
  
  Pass parameters to test scripts
&lt;/h1&gt;

&lt;p&gt;if ($Full) {&lt;br&gt;
    $config.Run.ContainerParameters = @{ Full = $true }&lt;br&gt;
}&lt;/p&gt;

&lt;h1&gt;
  
  
  Run tests
&lt;/h1&gt;

&lt;p&gt;$result = Invoke-Pester -Configuration $config&lt;br&gt;
exit $result.FailedCount&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This runner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loads Pester 5.x (ensuring compatibility)&lt;/li&gt;
&lt;li&gt;Accepts a &lt;code&gt;-TestFile&lt;/code&gt; parameter to run specific tests&lt;/li&gt;
&lt;li&gt;Passes the &lt;code&gt;-Full&lt;/code&gt; flag through to the test script&lt;/li&gt;
&lt;li&gt;Returns proper exit codes for CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Got Converted
&lt;/h2&gt;

&lt;p&gt;All the integration tests are now Pester-based:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Basic.Tests.ps1&lt;/strong&gt; - vagrant up, ssh, destroy (11 tests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DataDisk.Tests.ps1&lt;/strong&gt; - VHD creation, mounting, persistence (15 tests, requires admin)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot.Tests.ps1&lt;/strong&gt; - save, restore, list, delete, push/pop (10 tests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking.Tests.ps1&lt;/strong&gt; - static IPs, port forwarding (4 tests, requires admin)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MultiVmNetwork.Tests.ps1&lt;/strong&gt; - multi-VM networking, VM isolation (9 tests, requires admin)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AllDistributions.Tests.ps1&lt;/strong&gt; - distribution compatibility (3 quick / 22 full)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: 62 test cases (quick mode) or 81 test cases (full mode).&lt;/p&gt;

&lt;p&gt;The legacy tests are still in &lt;code&gt;test/integration/&lt;/code&gt; for reference, but all new work uses Pester.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Windows testing is different.&lt;/strong&gt; Admin privileges, path handling, output encoding - you can’t just port Unix testing patterns. PowerShell has its own quirks, and fighting them is pointless. Learn the PowerShell way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pester’s skip functionality is crucial.&lt;/strong&gt; Tests that require specific conditions (admin rights, specific OS, etc.) should show up as skipped, not silently disappear. It makes it clear what’s being tested and what’s not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always cleanup, even on failure.&lt;/strong&gt; The &lt;code&gt;AfterAll&lt;/code&gt; block runs even if tests fail. Use it. Clean up VMs, delete temp files, reset state. Future you will thank present you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parameters make tests flexible.&lt;/strong&gt; The fast/slow distribution test pattern could apply to other scenarios - integration vs. smoke tests, with/without network, etc. Parameters let you control test scope without duplicating code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real output matters.&lt;/strong&gt; Pester’s output shows you exactly what failed, with timing information and clear pass/fail counts. It’s a huge improvement over custom &lt;code&gt;Write-Host&lt;/code&gt; messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;The tests are working, but there’s cleanup to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove the legacy &lt;code&gt;test/integration/test_*.ps1&lt;/code&gt; files (keeping only &lt;code&gt;run_all_tests.ps1&lt;/code&gt; for compatibility)&lt;/li&gt;
&lt;li&gt;Add more Pester tests for edge cases (network failures, disk full, WSL2 not installed)&lt;/li&gt;
&lt;li&gt;Integrate with GitHub Actions for automated testing on commits&lt;/li&gt;
&lt;li&gt;Maybe add performance benchmarks using Pester’s timing data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for now, the integration tests are solid. They run fast (quick mode), cover all major functionality, and produce clear results.&lt;/p&gt;

&lt;p&gt;And they’re actually maintainable, which is the whole point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The vagrant-wsl2-provider is &lt;a href="https://github.com/your-repo/vagrant-wsl2-provider" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;. If you’re working on Windows tooling and need inspiration for testing WSL2 integrations, the Pester tests might help.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pester</category>
      <category>testing</category>
      <category>powershell</category>
      <category>vagrant</category>
    </item>
    <item>
      <title>Testing 22 WSL Distributions Live on Twitch (13 Passed, 9 Failed)</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sun, 16 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/testing-22-wsl-distributions-live-on-twitch-13-passed-9-failed-2acl</link>
      <guid>https://forem.com/leeshan87/testing-22-wsl-distributions-live-on-twitch-13-passed-9-failed-2acl</guid>
      <description>&lt;h2&gt;
  
  
  The Question Nobody Asked
&lt;/h2&gt;

&lt;p&gt;Which WSL distributions from the Windows Store actually work with the Vagrant WSL2 Provider?&lt;/p&gt;

&lt;p&gt;I didn’t know. And I realized - that’s a problem.&lt;/p&gt;

&lt;p&gt;The provider uses &lt;code&gt;wsl --install --distribution &amp;lt;name&amp;gt;&lt;/code&gt; to download and install distributions directly from the Windows Store. But there are 22 distributions available, and I’d only tested Ubuntu-24.04.&lt;/p&gt;

&lt;p&gt;Time to find out what works and what doesn’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Test
&lt;/h2&gt;

&lt;p&gt;The goal was simple: iterate through every distribution from &lt;code&gt;wsl -l -o&lt;/code&gt;, try to create a Vagrant environment with it, and document what fails.&lt;/p&gt;

&lt;p&gt;This is a sanity check, not a certification test. We expect failures. We want to know &lt;em&gt;which&lt;/em&gt; distributions fail and &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Test Structure
&lt;/h3&gt;

&lt;p&gt;Pester makes this straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeforeAll {
    # WSL distributions available from Windows Store
    $script:WslDistributions = @(
        "AlmaLinux-8",
        "AlmaLinux-9",
        "Debian",
        "Ubuntu-24.04",
        # ... 22 total
    )

    $script:TestBaseDir = Join-Path $PSScriptRoot "..\..\examples\test-distributions"
    $script:TestResults = @{}
}

Describe "WSL Distribution Compatibility Tests" {
    Context "Testing each distribution" {
        It "Should test all distributions" {
            foreach ($distribution in $script:WslDistributions) {
                # Step 1: vagrant init
                vagrant init $distribution

                # Step 2: Configure WSL2 provider
                # (modify Vagrantfile to add wsl2 provider config)

                # Step 3: vagrant up --provider=wsl2
                $upOutput = vagrant up --provider=wsl2 2&amp;gt;&amp;amp;1

                # Step 4: vagrant destroy -f
                vagrant destroy -f

                # Track results
                $script:TestResults[$distribution] = @{
                    Success = ($LASTEXITCODE -eq 0)
                    Error = $errorMsg
                }
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each distribution gets its own folder in &lt;code&gt;examples/test-distributions/&lt;/code&gt;, runs through the full lifecycle (&lt;code&gt;vagrant init&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;destroy&lt;/code&gt;), and gets cleaned up afterward.&lt;/p&gt;

&lt;p&gt;The test runs to completion even if distributions fail. We capture all the errors and display a summary at the end.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Fun fact - we initially tried using Pester’s &lt;code&gt;-TestCases&lt;/code&gt; with dynamic distribution lists, but Pester evaluates test cases at discovery time, not runtime. So we went with a single test that loops through distributions manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Streaming the Run
&lt;/h2&gt;

&lt;p&gt;I ran this live on Twitch. Because why not? Watching 22 distributions install, provision, and clean up takes time. Might as well make it public.&lt;/p&gt;

&lt;p&gt;Each test took anywhere from 30 seconds (for distributions that failed fast) to 2-3 minutes (for successful ones that needed to download, install, and configure systemd).&lt;/p&gt;

&lt;p&gt;Total runtime: &lt;strong&gt;17 minutes, 33 seconds&lt;/strong&gt; (1053.89s reported by Pester).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Distribution Compatibility Summary ===

Supported Distributions (13):
  [PASS] AlmaLinux-8
  [PASS] AlmaLinux-9
  [PASS] AlmaLinux-Kitten-10
  [PASS] AlmaLinux-10
  [PASS] Debian
  [PASS] FedoraLinux-43
  [PASS] FedoraLinux-42
  [PASS] SUSE-Linux-Enterprise-16.0
  [PASS] Ubuntu
  [PASS] Ubuntu-24.04
  [PASS] kali-linux
  [PASS] openSUSE-Tumbleweed
  [PASS] openSUSE-Leap-16.0

Unsupported/Failed Distributions (9):
  [FAIL] archlinux
         Error: tee: /etc/sudoers.d/vagrant: No such file or directory

  [FAIL] SUSE-Linux-Enterprise-15-SP7
         Error: chown: invalid group: 'vagrant:vagrant'

  [FAIL] Ubuntu-20.04
  [FAIL] OracleLinux_7_9
  [FAIL] OracleLinux_8_10
  [FAIL] OracleLinux_9_5
         Error: Distribution not found after installation completed

  [FAIL] Ubuntu-22.04
  [FAIL] openSUSE-Leap-15.6
  [FAIL] SUSE-Linux-Enterprise-15-SP6
         Error: Using legacy distribution registration

Total: 22 distributions tested
Success Rate: 13/22 (59.1%)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Worked
&lt;/h3&gt;

&lt;p&gt;The AlmaLinux family (all 4 versions), Fedora, Debian, current Ubuntu versions, Kali Linux, and openSUSE’s latest releases all passed.&lt;/p&gt;

&lt;p&gt;That’s 13 distributions spanning multiple Linux families. Not bad for a provider that started as “let’s just get Ubuntu working.”&lt;/p&gt;

&lt;h3&gt;
  
  
  What Failed - And Why
&lt;/h3&gt;

&lt;p&gt;The failures break down into 4 categories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Legacy Distribution Registration (4 distros)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Using legacy distribution registration.
Consider using a tar based distribution instead.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ubuntu-22.04, openSUSE-Leap-15.6, and SUSE Linux Enterprise 15-SP6 all hit this. WSL is warning that these distributions use an older registration format.&lt;/p&gt;

&lt;p&gt;The provider handles tar-based imports fine (that’s how we clone distributions for snapshots). But &lt;code&gt;wsl --install&lt;/code&gt; for these specific distributions triggers the legacy path, and something in our provisioning flow doesn’t handle it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Distribution Not Found After Installation (4 distros)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Distribution 'Ubuntu-20.04' not found after installation completed.
Check if installation was successful.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ubuntu-20.04 and all three OracleLinux versions hit this. The &lt;code&gt;wsl --install&lt;/code&gt; command completes, reports success, but when we check &lt;code&gt;wsl -l -v&lt;/code&gt; to verify the distribution is there, it’s not listed.&lt;/p&gt;

&lt;p&gt;This could be a timing issue (installation takes longer than we expect), a naming mismatch (we search for the exact distribution name but it registers under a different one), or these distributions genuinely fail to install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Missing &lt;code&gt;/etc/sudoers.d/&lt;/code&gt; Directory (1 distro)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: tee: /etc/sudoers.d/vagrant: No such file or directory

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arch Linux doesn’t have &lt;code&gt;/etc/sudoers.d/&lt;/code&gt; by default. Our provisioning script tries to add the vagrant user’s sudo config there and fails.&lt;/p&gt;

&lt;p&gt;This is a simple fix - check if the directory exists first, or fall back to appending directly to &lt;code&gt;/etc/sudoers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Invalid Group Error (1 distro)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: chown: invalid group: 'vagrant:vagrant'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SUSE Linux Enterprise 15-SP7 doesn’t let us create the &lt;code&gt;vagrant:vagrant&lt;/code&gt; group the way we expect. Could be SELinux, could be enterprise defaults, needs investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bugs We Found Along the Way
&lt;/h2&gt;

&lt;p&gt;While building and running this test, we discovered some provider bugs that needed documenting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation vs Reality&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CLAUDE.md file listed &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;cpus&lt;/code&gt;, &lt;code&gt;gui_support&lt;/code&gt;, &lt;code&gt;swap&lt;/code&gt;, and &lt;code&gt;kernel_command_line&lt;/code&gt; as supported configuration options. Turns out they’re only &lt;em&gt;defined&lt;/em&gt; in config.rb but never actually implemented in the provider.&lt;/p&gt;

&lt;p&gt;Fixed the docs to be honest: “Additional configuration options are defined but not yet implemented.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naming Convention Bug&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The provider uses &lt;code&gt;distribution_name&lt;/code&gt; for the WSL distribution name, but doesn’t follow Vagrant’s standard naming conventions. If the box isn’t named correctly, &lt;code&gt;vagrant destroy&lt;/code&gt; won’t remove it from WSL distributions.&lt;/p&gt;

&lt;p&gt;This leaves orphaned WSL distributions sitting around after you think you’ve cleaned up. Added to the bug list.&lt;/p&gt;

&lt;p&gt;These aren’t critical (the provider works), but they’re the kind of rough edges you find when you actually test things properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means
&lt;/h2&gt;

&lt;p&gt;We have a 59% success rate across all available WSL distributions. That’s higher than I expected, honestly.&lt;/p&gt;

&lt;p&gt;The failures aren’t random. They fall into clear categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Legacy formats&lt;/strong&gt; - Need to handle older distribution registration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Installation timing&lt;/strong&gt; - Verify distributions exist before proceeding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem assumptions&lt;/strong&gt; - Stop assuming &lt;code&gt;/etc/sudoers.d/&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group creation&lt;/strong&gt; - Handle enterprise distributions differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are dealbreakers. They’re just edge cases we haven’t coded for yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This test doesn’t just tell us what works. It tells us &lt;em&gt;what to fix next&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Before this, I had no idea which distributions were broken or why. Now I have a reproducible test that documents exactly which features fail on which distributions.&lt;/p&gt;

&lt;p&gt;More importantly: &lt;strong&gt;This test runs fast&lt;/strong&gt; (17 minutes for 22 distributions). I can run it after every major change to catch regressions.&lt;/p&gt;

&lt;p&gt;And when someone opens an issue saying “Provider doesn’t work with OracleLinux,” I can point to the test results and say: “Yeah, I know. Here’s the error. Want to help fix it?”&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Also worth noting - we caught a documentation bug during this session. CLAUDE.md listed &lt;code&gt;memory&lt;/code&gt; and &lt;code&gt;cpus&lt;/code&gt; as supported configuration options, but they’re only defined in the config, not actually implemented. Fixed that too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;The test is in. The results are documented. Now comes the fun part: fixing the failures.&lt;/p&gt;

&lt;p&gt;Priority targets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Arch Linux&lt;/strong&gt; - Should be a 5-minute fix (check if directory exists)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy distributions&lt;/strong&gt; - Figure out what “tar based distribution” actually means for our flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle/Ubuntu older versions&lt;/strong&gt; - Debug why installations report success but don’t register&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test stays in the repo. Future releases will include updated compatibility matrices.&lt;/p&gt;

&lt;p&gt;And yeah, I’ll probably stream the fixes too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The test is in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/LeeShan87/vagrant-wsl2-provider
cd vagrant-wsl2-provider
rake test_all_distributions

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: This will download and test 22 WSL distributions. Budget ~20 minutes and some bandwidth.&lt;/p&gt;

&lt;p&gt;Results will vary based on your Windows version, WSL version, and what distributions you already have installed.&lt;/p&gt;

&lt;p&gt;If you find different results, open an issue. This is how we make the provider better.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Lessons from this session:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sanity checks are worth the time investment&lt;/li&gt;
&lt;li&gt;Live streaming makes long test runs more tolerable&lt;/li&gt;
&lt;li&gt;59% success rate beats 0% knowledge&lt;/li&gt;
&lt;li&gt;Document what’s broken before trying to fix it&lt;/li&gt;
&lt;li&gt;Pester makes it easy to iterate through test cases cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next post: Actually fixing the failures. Stay tuned.&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>testing</category>
      <category>pester</category>
    </item>
    <item>
      <title>Why I Chose Pester Over Custom Test Scripts (And Why Now)</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Mon, 10 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/why-i-chose-pester-over-custom-test-scripts-and-why-now-35de</link>
      <guid>https://forem.com/leeshan87/why-i-chose-pester-over-custom-test-scripts-and-why-now-35de</guid>
      <description>&lt;h2&gt;
  
  
  The Testing Journey So Far
&lt;/h2&gt;

&lt;p&gt;When I started this Vagrant WSL2 Provider project, I knew unit tests would be a waste of tokens. My requirements change daily. AI pair programming with Claude means rapid iteration, constant refactoring, and features that evolve as I discover what WSL2 can and can’t do.&lt;/p&gt;

&lt;p&gt;So I skipped unit tests entirely and went straight to integration tests. Simple PowerShell scripts that do the real thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant up --provider=wsl2
if ($LASTEXITCODE -ne 0) { throw "failed" }
Write-Host "[PASS] vagrant up succeeded" -ForegroundColor Green

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. It was fast to write. And for a while, it was enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Custom Framework Started Hurting
&lt;/h2&gt;

&lt;p&gt;The project grew. We shipped v0.3.0 with snapshot support and data disks. Then came networking support, and that’s where things got interesting (read: broken).&lt;/p&gt;

&lt;p&gt;I started noticing weird behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vagrant status&lt;/code&gt; always showed “running” even after &lt;code&gt;vagrant halt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SSH commands with background processes didn’t work&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;amp;&lt;/code&gt; character in &lt;code&gt;vagrant ssh -c "python3 -m http.server &amp;amp;"&lt;/code&gt; just… disappeared&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The networking feature needed these to work. Start a web server on one VM, connect to it from another. Basic stuff. Except it didn’t work.&lt;/p&gt;

&lt;p&gt;And I had no tests for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem
&lt;/h2&gt;

&lt;p&gt;I could write more custom PowerShell test scripts. That wasn’t the issue. The issue was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token waste&lt;/strong&gt; - Claude and I were spending time debugging test output formatting instead of fixing bugs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No structure&lt;/strong&gt; - Every test script had its own cleanup logic, error handling, and output style&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing coverage&lt;/strong&gt; - We had tests for features that worked, not for bugs we needed to fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual everything&lt;/strong&gt; - Want better output? Write more &lt;code&gt;Write-Host&lt;/code&gt; calls&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I started thinking: “What if this custom testing framework became an open-source project?”&lt;/p&gt;

&lt;p&gt;Then I realized: Nobody would use it. Because &lt;strong&gt;Pester already exists&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Can confirm. We literally googled “PowerShell testing framework” and found Pester in about 10 seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Pester, Why Now
&lt;/h2&gt;

&lt;p&gt;Pester is the standard PowerShell testing framework. It’s:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built into Windows 10/11 (though we needed to upgrade to 5.x)&lt;/li&gt;
&lt;li&gt;BDD-style syntax (&lt;code&gt;Describe&lt;/code&gt;, &lt;code&gt;Context&lt;/code&gt;, &lt;code&gt;It&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Proper assertions (&lt;code&gt;Should -Be&lt;/code&gt;, &lt;code&gt;Should -Match&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Automatic setup/teardown (&lt;code&gt;BeforeAll&lt;/code&gt;, &lt;code&gt;AfterAll&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Better reporting (colored output, timing, clear pass/fail)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly: &lt;strong&gt;I’m about to write tests that document bugs&lt;/strong&gt;. Not tests that pass. Tests that fail, on purpose, to show exactly what’s broken.&lt;/p&gt;

&lt;p&gt;For that, I need a real framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration
&lt;/h2&gt;

&lt;p&gt;The old way (119 lines):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try {
    Push-Location $ExampleDir
    vagrant destroy -f 2&amp;gt;$null

    vagrant up --provider=wsl2
    if ($LASTEXITCODE -ne 0) {
        throw "vagrant up failed with exit code $LASTEXITCODE"
    }
    Write-Host "[PASS] vagrant up succeeded" -ForegroundColor Green

    # ... more tests ...

    exit 0
} catch {
    Write-Host "=== Test FAILED ===" -ForegroundColor Red
    vagrant destroy -f 2&amp;gt;$null
    exit 1
} finally {
    Pop-Location
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new way (67 lines):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BeforeAll {
    $script:ExampleDir = Join-Path $PSScriptRoot "..\..\examples\basic"
    Push-Location $script:ExampleDir
    vagrant destroy -f 2&amp;gt;$null | Out-Null
}

AfterAll {
    vagrant destroy -f 2&amp;gt;$null | Out-Null
    Pop-Location
}

Describe "Vagrant WSL2 Provider - Basic Operations" {
    Context "When creating a new VM" {
        It "Should successfully run 'vagrant up --provider=wsl2'" {
            vagrant up --provider=wsl2
            $LASTEXITCODE | Should -Be 0
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleaner. More structure. Automatic cleanup. And when something fails, I get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-] Should show correct state after halt (not running) 6.27s (6.27s|1ms)
    Expected regular expression 'running' to not match
    'Current machine states:

    default running (wsl2)

    The WSL2 distribution is running', but it did match.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s way better than parsing my custom &lt;code&gt;[PASS]&lt;/code&gt; / &lt;code&gt;[FAIL]&lt;/code&gt; output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documenting Bugs with Tests
&lt;/h2&gt;

&lt;p&gt;Here’s the important part. I didn’t just migrate existing tests. I added new ones that &lt;strong&gt;fail on purpose&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Context "When managing VM state" {
    It "Should successfully halt the VM" {
        vagrant halt
        $LASTEXITCODE | Should -Be 0
    }

    It "Should show correct state after halt (not running)" {
        Start-Sleep -Seconds 2
        $status = (vagrant status) -join "`n"

        # After halt, should NOT show "running"
        $status | Should -Not -Match "running"
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test fails. As it should. Because &lt;code&gt;vagrant halt&lt;/code&gt; doesn’t actually change the status output. That’s the bug.&lt;/p&gt;

&lt;p&gt;Another one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;It "Should start a background process (Python web server)" {
    $output = vagrant ssh -c "python3 -m http.server 8888 &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;" 2&amp;gt;&amp;amp;1
    $LASTEXITCODE | Should -Be 0

    Start-Sleep -Seconds 2

    $psOutput = vagrant ssh -c "ps aux | grep 'http.server' | grep -v grep" 2&amp;gt;&amp;amp;1
    $psOutput | Should -Match "http.server"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also fails. The background process doesn’t start. SSH command escaping is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Results
&lt;/h2&gt;

&lt;p&gt;After the migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tests Passed: 11, Failed: 3

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;11 passing tests&lt;/strong&gt; - The features that work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 failing tests&lt;/strong&gt; - The bugs I need to fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the state of the project right now. Green for confidence, red for work to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;I’m not writing tests for test coverage. I’m writing tests because I don’t want to pull my hair out debugging regressions.&lt;/p&gt;

&lt;p&gt;When someone opens a PR, I want them to run &lt;code&gt;rake test_pester&lt;/code&gt; and see what they broke (or fixed). When I refactor the SSH action, I want to know immediately if I broke ansible provisioner support.&lt;/p&gt;

&lt;p&gt;Unit tests wouldn’t catch that. Integration tests do.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Approach Works
&lt;/h2&gt;

&lt;p&gt;This testing strategy isn’t universal. It works specifically because:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The context is right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hobbyist/MVP project with changing requirements&lt;/li&gt;
&lt;li&gt;AI-assisted development (not wasting tokens on test boilerplate)&lt;/li&gt;
&lt;li&gt;Open source (contributors need to understand what’s safe to change)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The motivation is right:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not “we need tests because best practices say so”&lt;/li&gt;
&lt;li&gt;But “I found bugs, I need to document them, and I don’t want to break working features while fixing them”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The migration is pragmatic:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not “throw away all tests and rewrite”&lt;/li&gt;
&lt;li&gt;But “keep legacy tests running, migrate gradually, remove old ones when confident”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the opposite of cargo cult programming. No testing pyramids, no TDD dogma, no “you must have 80% coverage” rules. Just: what does this project actually need right now?&lt;/p&gt;

&lt;p&gt;And right now, it needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confidence that basic features work (11 green tests)&lt;/li&gt;
&lt;li&gt;Documentation of known bugs (3 red tests)&lt;/li&gt;
&lt;li&gt;A framework that won’t waste our time when we add more tests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pester gives us that. A custom framework wouldn’t scale. Unit tests would miss the real issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Getting Pester working was surprisingly smooth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Rakefile
desc "Ensure Pester 5.x is installed (&amp;gt;= 5.0, &amp;lt; 6.0)"
task :ensure_pester do
  pester_check = &amp;lt;&amp;lt;~POWERSHELL
    $pester = Get-Module -ListAvailable -Name Pester |
      Where-Object { $_.Version -ge '5.0' -and $_.Version -lt '6.0' } |
      Select-Object -First 1
    if (-not $pester) {
      Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
      Install-Module -Name Pester -RequiredVersion 5.7.1 -Force -Scope CurrentUser
    }
  POWERSHELL
  sh "powershell -Command \"#{pester_check.gsub("\n", "; ")}\""
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;rake test_pester&lt;/code&gt; automatically checks for Pester 5.x and installs it if needed. Version locked to avoid the 6.x alpha. One less thing to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;Now comes the fun part: fixing the bugs.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;vagrant halt&lt;/code&gt; status issue is probably in the state tracking logic. The SSH background process issue is definitely in how we escape shell commands. Both have tests that document exactly what should happen.&lt;/p&gt;

&lt;p&gt;This is reverse TDD. Write the tests after you find the bugs, then fix the code until the tests pass.&lt;/p&gt;

&lt;p&gt;Not ideal, but way better than:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fixing a bug&lt;/li&gt;
&lt;li&gt;Manually testing it&lt;/li&gt;
&lt;li&gt;Breaking it again three commits later&lt;/li&gt;
&lt;li&gt;Discovering it broke when trying to demo the feature&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Been there. Done that. Got the T-shirt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The Vagrant WSL2 Provider is on GitHub. The Pester tests are in &lt;code&gt;test/integration/&lt;/code&gt;. If you want to see a real-world example of integration testing a Vagrant provider, check it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/LeeShan87/vagrant-wsl2-provider.git
cd vagrant-wsl2-provider
rake ensure_pester
rake test_pester

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll see the same 11 passing, 3 failing tests I see. And when I fix those bugs, you’ll see 14 passing tests.&lt;/p&gt;

&lt;p&gt;That’s the plan, anyway.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Bold of you to assume the fixes won’t reveal more bugs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair point.&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>testing</category>
      <category>pester</category>
    </item>
    <item>
      <title>When Your Quick Sunday Feature Takes All Day: WSL2 Multi-VM Networking</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Mon, 10 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/when-your-quick-sunday-feature-takes-all-day-wsl2-multi-vm-networking-453i</link>
      <guid>https://forem.com/leeshan87/when-your-quick-sunday-feature-takes-all-day-wsl2-multi-vm-networking-453i</guid>
      <description>&lt;h2&gt;
  
  
  “This Should Be Quick”
&lt;/h2&gt;

&lt;p&gt;Famous last words.&lt;/p&gt;

&lt;p&gt;After implementing &lt;a href="https://dev.to/posts/2025/2025-11-08-vagrant-wsl2-networking-reality-check/"&gt;basic networking support&lt;/a&gt; and learning about WSL2’s shared network architecture, I wanted to make it work reliably across multiple distributions. The plan: add integration tests, support more distros (Debian, Fedora, AlmaLinux, Kali, openSUSE), and document the limitations properly.&lt;/p&gt;

&lt;p&gt;What I thought would be a quick Sunday afternoon turned into a deep dive into systemd services, DNS resolution, and why background processes in SSH are surprisingly hard.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Can confirm. We went from “let’s add a test” to “why is DNS broken on Debian” to “let’s refactor everything to use systemd services” in about 4 hours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Multi-Distro Challenge
&lt;/h2&gt;

&lt;p&gt;Ubuntu was easy - it has netplan. But what about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debian&lt;/strong&gt; - Uses systemd-networkd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fedora/AlmaLinux/Kali&lt;/strong&gt; - Use NetworkManager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;openSUSE&lt;/strong&gt; - Uses wicked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each has its own network configuration system. My first instinct: support each one natively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def write_netplan_config
  # Ubuntu with netplan
  netplan_config = &amp;lt;&amp;lt;~YAML
    network:
      version: 2
      ethernets:
        eth0:
          dhcp4: true
          addresses:
            - #{ip}/#{prefix}
  YAML

  @machine.communicate.sudo("netplan apply")
end

def write_networkmanager_config
  # Fedora/AlmaLinux/Kali
  @machine.communicate.sudo(
    "nmcli connection modify 'eth0' +ipv4.addresses #{ip}/#{prefix}"
  )
  @machine.communicate.sudo("nmcli connection up 'eth0'")
end

def write_systemd_networkd_config
  # Debian
  # ... and so on
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seemed reasonable, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  The DNS Problem
&lt;/h2&gt;

&lt;p&gt;Debian was the first to break.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vm2: Warning: Failed to fetch http://deb.debian.org/debian/dists/trixie/InRelease
vm2: Temporary failure resolving 'deb.debian.org'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The static IP was configured, but DNS stopped working after the provision script ran. Why?&lt;/p&gt;

&lt;p&gt;After some debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant ssh vm2 -c "ip a show eth0"
2: eth0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500
    inet 192.168.50.11/24 scope global eth0
    # Where's the WSL2 DHCP IP (172.x.x.x)?

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh. The &lt;code&gt;systemd-networkd restart&lt;/code&gt; command wiped out the WSL2 DHCP IP, which also provides DNS resolution and the default route. No DHCP IP = no DNS = broken apt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; This is the WSL2 version of “I deleted production.” Except you’re deleting your own network stack.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Ubuntu Wasn’t Better
&lt;/h2&gt;

&lt;p&gt;Tried Ubuntu with netplan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==&amp;gt; vm1: Netplan configuration written with 1 static IP(s)
systemd-networkd is not running, output might be incomplete.
Failed to reload network settings: Unit dbus-org.freedesktop.network1.service not found.
Falling back to a hard restart of systemd-networkd.service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same problem. &lt;code&gt;netplan apply&lt;/code&gt; tries to restart &lt;code&gt;systemd-networkd&lt;/code&gt;, which breaks WSL2’s network management.&lt;/p&gt;

&lt;h2&gt;
  
  
  NetworkManager: Different Problem
&lt;/h2&gt;

&lt;p&gt;Fedora seemed promising - NetworkManager should handle multiple IPs gracefully, right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Connection activation failed: No suitable device found for this connection
(device eth0 not available because profile is not compatible with device
(permanent MAC address doesn't match)).

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ah. WSL2’s MAC address &lt;strong&gt;changes on every restart&lt;/strong&gt;. NetworkManager stores the MAC in the connection profile and refuses to work when it doesn’t match.&lt;/p&gt;

&lt;p&gt;Great.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Systemd Service Revelation
&lt;/h2&gt;

&lt;p&gt;At this point I had three different broken approaches for three different distros. Time to step back.&lt;/p&gt;

&lt;p&gt;What do we actually need?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add static IP to eth0&lt;/li&gt;
&lt;li&gt;Keep WSL2’s DHCP IP intact (for DNS/routing)&lt;/li&gt;
&lt;li&gt;Persist across reboots&lt;/li&gt;
&lt;li&gt;Work on all distros&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What if… we just don’t touch the native network config systems at all?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def write_systemd_static_ip_service(after_services, distro_name)
  # Universal method for ALL distros
  ip_commands = @static_ips.map { |ip_info|
    "ip addr add #{ip_info[:ip]}/#{ip_info[:prefix]} dev eth0 || true"
  }.join("\n")

  service_config = &amp;lt;&amp;lt;~SERVICE
    [Unit]
    Description=Vagrant Static IP Configuration
    After=#{after_services}
    Wants=network-online.target

    [Service]
    Type=oneshot
    RemainAfterExit=yes
    ExecStart=/bin/bash -c '#{ip_commands}'

    [Install]
    WantedBy=multi-user.target
  SERVICE

  # Write service file
  @machine.communicate.sudo("mv #{service_path} /etc/systemd/system/vagrant-static-ip.service")
  @machine.communicate.sudo("systemctl daemon-reload")
  @machine.communicate.sudo("systemctl enable vagrant-static-ip.service")
  @machine.communicate.sudo("systemctl start vagrant-static-ip.service")
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A systemd oneshot service that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs after network is up&lt;/li&gt;
&lt;li&gt;Adds static IPs with &lt;code&gt;ip addr add&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;|| true&lt;/code&gt; so it’s idempotent&lt;/li&gt;
&lt;li&gt;Doesn’t restart anything&lt;/li&gt;
&lt;li&gt;Doesn’t touch WSL2’s DHCP configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here’s the beautiful part - &lt;strong&gt;it works on every distro&lt;/strong&gt; because they all use systemd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Refactor
&lt;/h2&gt;

&lt;p&gt;Wait, why are we even detecting distros? The whole point of the systemd service is that it’s universal. And &lt;code&gt;network-online.target&lt;/code&gt; works on all of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def write_netplan_config
  # Universal solution for all distros - systemd service
  # No need to detect distro or network manager
  write_systemd_static_ip_service
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. No detection. No branching. One implementation.&lt;/p&gt;

&lt;p&gt;From ~160 lines of distro-specific code to ~45 lines of universal code. DRY for the win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing: The SSH Background Process Bug
&lt;/h2&gt;

&lt;p&gt;Integration test time. Need to test VM-to-VM communication. Python HTTP server seems perfect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Start HTTP server on vm1
vagrant ssh vm1 -c "python3 -m http.server 8080 --bind 192.168.50.10 &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;"

# Test from vm2
vagrant ssh vm2 -c "curl http://192.168.50.10:8080/"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except… it doesn’t work. The &lt;code&gt;&amp;amp;&lt;/code&gt; background process never starts.&lt;/p&gt;

&lt;p&gt;Why? Because of how we encode commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def encode_command(command)
  encoded = Base64.strict_encode64(command)
  "echo '#{encoded}' | base64 -d | bash"
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pipe to &lt;code&gt;bash&lt;/code&gt; is &lt;strong&gt;blocking&lt;/strong&gt;. Even with &lt;code&gt;&amp;amp;&lt;/code&gt; at the end of the command, the SSH session waits for bash to finish. And bash waits for the backgrounded process because… pipes.&lt;/p&gt;

&lt;p&gt;Tried &lt;code&gt;eval&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"eval \"$(echo '#{encoded}' | base64 -d)\""

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that breaks redirect parsing because of the double quotes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Leave it as a known bug for now, use PowerShell jobs in the test to work around it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$serverJob = Start-Job -ScriptBlock {
    vagrant ssh vm1 -c "python3 -m http.server 8080 --bind 192.168.50.10"
}
Start-Sleep -Seconds 3
$http_result = vagrant ssh vm2 -c "curl http://192.168.50.10:8080/"
Stop-Job $serverJob

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not elegant, but it works. The SSH command encoding is a problem for another day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Translation: “I’ll fix this later” = “This will ship as-is”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The README: Managing Expectations
&lt;/h2&gt;

&lt;p&gt;After all this, I wrote probably the most important documentation - the limitations README. Because this feature &lt;strong&gt;works&lt;/strong&gt; , but it has constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## WSL2 Networking Limitations

⚠️ **Important:** Private network support in WSL2 is experimental.

### Shared Network Infrastructure
- All WSL2 VMs share the same virtual network switch
- Same MAC address - every VM gets the same MAC on each WSL restart
- Shared base IP - all VMs share the same WSL2 DHCP IP
- IP visibility - you may see other VMs' static IPs on a single VM

### What This Means
**VM-to-VM Communication:** ⚠️ LIMITED
- VMs share the same physical NIC and MAC address
- Ping between VMs using static IPs does not work reliably
- TCP/UDP application traffic may work if routing is configured correctly

**Windows Host Access:** ❌ LIMITED
- Use port forwarding instead

**Process Isolation:** ✅ WORKS
- Each distribution runs in its own PID namespace

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being honest about limitations is better than users discovering them the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Don’t Fight the Platform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first instinct was to use each distro’s native network config system. But WSL2 isn’t a traditional VM - it has its own quirks. Fighting those quirks with netplan/NetworkManager/etc. just created more problems.&lt;/p&gt;

&lt;p&gt;The systemd service approach works &lt;strong&gt;with&lt;/strong&gt; WSL2’s design instead of against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sometimes “Good Enough” Is the Win&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Perfect VM-to-VM networking in WSL2? Not possible. The architecture doesn’t support it.&lt;/p&gt;

&lt;p&gt;But adding static IPs that survive reboots and work across distros? That’s achievable. And for development/testing use cases, it’s useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Documentation &amp;gt; Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most important code I wrote today was the README explaining why things don’t work perfectly. Setting expectations upfront saves everyone frustration later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Integration Tests Reveal Real Problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writing the test exposed the SSH background process bug, the DNS issues, and the MAC address problem. All things that wouldn’t show up in manual testing.&lt;/p&gt;

&lt;p&gt;Even though the test needed workarounds, it was still valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;The networking feature works across 6 distributions (Ubuntu, Debian, Fedora, AlmaLinux, Kali, openSUSE). It’s documented. It has tests (mostly).&lt;/p&gt;

&lt;p&gt;But there are TODOs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fix the SSH command encoding for background processes&lt;/li&gt;
&lt;li&gt;Maybe explore WSL2 mirrored networking mode (Windows 11 22H2+)&lt;/li&gt;
&lt;li&gt;Test with more complex network scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now though, it’s good enough. Users can run multi-VM setups for testing, the limitations are clear, and the code is maintainable.&lt;/p&gt;

&lt;p&gt;That’s a win.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Started the session thinking “quick feature add.” Ended it having refactored the entire networking implementation and written a philosophical README about WSL2’s limitations. Classic Sunday.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The multi-VM networking example is in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/LeeShan87/vagrant-wsl2-provider
cd examples/multi-vm-network
vagrant up

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the README for the full list of limitations and workarounds. And maybe don’t expect VirtualBox-level networking - this is WSL2, after all.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Actual time spent:&lt;/strong&gt; 4+ hours &lt;strong&gt;Lines of code written:&lt;/strong&gt; ~300 &lt;strong&gt;Lines of code deleted:&lt;/strong&gt; ~100 &lt;strong&gt;Times I questioned my life choices:&lt;/strong&gt; Several &lt;strong&gt;Would I do it again:&lt;/strong&gt; Probably&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>networking</category>
      <category>systemd</category>
    </item>
    <item>
      <title>Vagrant WSL2 Networking: A Reality Check</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sat, 08 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/vagrant-wsl2-networking-a-reality-check-2j48</link>
      <guid>https://forem.com/leeshan87/vagrant-wsl2-networking-a-reality-check-2j48</guid>
      <description>&lt;h2&gt;
  
  
  Starting with High Hopes
&lt;/h2&gt;

&lt;p&gt;After adding Docker support, snapshots, and data disk management to the Vagrant WSL2 Provider, networking felt like the obvious next step. The goal was simple: make &lt;code&gt;config.vm.network&lt;/code&gt; work just like it does in VirtualBox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.vm.network "private_network", ip: "192.168.33.10"
config.vm.network "forwarded_port", guest: 80, host: 8080

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How hard could it be?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Narrator: It was harder than expected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The VirtualBox Mental Model
&lt;/h2&gt;

&lt;p&gt;When you use VirtualBox, each VM gets its own network interfaces. Private networks create host-only adapters, each VM has its own IP, and everything just works. I assumed WSL2 would be similar - after all, it’s running on Hyper-V, right?&lt;/p&gt;

&lt;p&gt;Wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reality: WSL2’s Architecture
&lt;/h2&gt;

&lt;p&gt;Here’s what I learned (painfully) about WSL2 networking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All WSL2 distributions run on a SINGLE Hyper-V VM.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not separate VMs - one VM running multiple distributions in separate namespaces. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every distribution shares the same base IP address (something like 172.26.143.58)&lt;/li&gt;
&lt;li&gt;Each distribution has its own network namespace&lt;/li&gt;
&lt;li&gt;They all use the same &lt;code&gt;eth0&lt;/code&gt; interface in the WSL utility VM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I created two test VMs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.vm.define "vm1" do |vm1|
  vm1.vm.network "private_network", ip: "192.168.50.10"
end

config.vm.define "vm2" do |vm2|
  vm2.vm.network "private_network", ip: "192.168.50.11"
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both got configured with their static IPs. Both showed up in &lt;code&gt;ip addr&lt;/code&gt;. But when I checked from Windows - &lt;strong&gt;both had the same WSL base IP: 172.26.143.58&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IP Alias Disaster
&lt;/h2&gt;

&lt;p&gt;My first approach: “I’ll just add the static IP as an alias to the Windows WSL network adapter!”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Add IP to Windows WSL adapter
cmd = "New-NetIPAddress -InterfaceAlias 'vEthernet (WSL)' " +
      "-IPAddress #{ip_address} -PrefixLength 24"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;em&gt;kind of&lt;/em&gt; worked. The IP showed up on Windows. But then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; Get-NetIPAddress -InterfaceAlias "vEthernet (WSL)"

IPAddress : 192.168.33.10
AddressState : Duplicate # OH NO

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Duplicate Address Detection&lt;/strong&gt; kicked in. Why? Because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;WSL VM’s eth0 has 192.168.33.10&lt;/li&gt;
&lt;li&gt;Windows adapter also has 192.168.33.10&lt;/li&gt;
&lt;li&gt;Same L2 network segment&lt;/li&gt;
&lt;li&gt;Two different MAC addresses claiming the same IP&lt;/li&gt;
&lt;li&gt;Windows: “Nope, that’s a duplicate, shutting it down”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The IP went into “Duplicate” state and stopped working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows Routing to the Rescue
&lt;/h2&gt;

&lt;p&gt;Instead of trying to put the same IP in two places, use routing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def add_windows_route(static_ip, wsl_ip)
  cmd = "route add #{static_ip} mask 255.255.255.255 #{wsl_ip}"
  result = Vagrant::Util::Subprocess.execute("powershell", "-Command", cmd)
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Windows: “When you see traffic for 192.168.33.10, send it to 172.26.143.58 (the actual WSL IP).”&lt;/p&gt;

&lt;p&gt;No duplicate IPs, no conflicts, just routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admin Privileges Required
&lt;/h2&gt;

&lt;p&gt;Of course, &lt;code&gt;route add&lt;/code&gt; requires administrator privileges. So I added a check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def has_admin_privileges?
  cmd = "([Security.Principal.WindowsPrincipal]" +
        "[Security.Principal.WindowsIdentity]::GetCurrent())" +
        ".IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"

  result = Vagrant::Util::Subprocess.execute("powershell", "-Command", cmd)
  result.exit_code == 0 &amp;amp;&amp;amp; result.stdout.strip.downcase == "true"
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re not running as admin, you get a clear warning and network configuration is skipped. The VM still starts, you just don’t get the fancy networking features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistence Problem
&lt;/h2&gt;

&lt;p&gt;Network configuration in Linux needs to survive reboots. The &lt;code&gt;ip addr add&lt;/code&gt; command is immediate but temporary. I needed to write actual config files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plot twist:&lt;/strong&gt; Different distributions use different network managers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu&lt;/strong&gt; : Uses netplan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debian&lt;/strong&gt; : Uses systemd-networkd (no netplan)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solution: Detect which one is available and write the appropriate config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def write_netplan_config
  has_netplan = @machine.communicate.test("command -v netplan")

  if has_netplan
    write_ubuntu_netplan_config
  else
    write_systemd_networkd_config
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ubuntu netplan config:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;network:
  version: 2
  ethernets:
    eth0:
      dhcp4: true
      addresses:
        - 192.168.33.10/24
        - 1.2.3.4/24

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Debian systemd-networkd config:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Match]
Name=eth0

[Network]
DHCP=yes
Address=192.168.33.10/24
Address=1.2.3.4/24

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Multi-VM Reality
&lt;/h2&gt;

&lt;p&gt;Here’s the part that took me longest to accept: &lt;strong&gt;Windows host cannot distinguish between multiple WSL2 VMs using only their static IPs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because they all share the same underlying WSL IP. When you add routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;route add 192.168.50.10 -&amp;gt; 172.26.143.58 # vm1
route add 192.168.50.11 -&amp;gt; 172.26.143.58 # vm2 - same target!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both routes point to the same WSL IP. Windows can’t tell them apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What DOES work:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VM-to-VM communication via static IPs (they’re in separate namespaces)&lt;/li&gt;
&lt;li&gt;Process isolation (completely separate PID spaces)&lt;/li&gt;
&lt;li&gt;Different services listening on the same ports in different VMs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What DOESN’T work:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows host accessing VMs via their static IPs&lt;/li&gt;
&lt;li&gt;Each VM having a truly independent IP from Windows perspective&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; Use port forwarding for Windows host access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.vm.define "web" do |web|
  web.vm.network "forwarded_port", guest: 80, host: 8080
end

config.vm.define "api" do |api|
  api.vm.network "forwarded_port", guest: 80, host: 8081
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different ports on localhost, clear separation, works perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Port Forwarding Implementation
&lt;/h2&gt;

&lt;p&gt;Port forwarding uses Windows &lt;code&gt;netsh&lt;/code&gt; portproxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def setup_port_forward(listen_address, listen_port, connect_address, connect_port)
  listen_addresses = listen_address == "0.0.0.0" ? ["127.0.0.1"] : [listen_address]

  listen_addresses.each do |addr|
    cmd = "netsh interface portproxy add v4tov4 " +
          "listenaddress=#{addr} listenport=#{listen_port} " +
          "connectaddress=#{connect_address} connectport=#{connect_port}"

    Vagrant::Util::Subprocess.execute("powershell", "-Command", cmd)
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a proper TCP proxy on Windows that forwards traffic to the WSL VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Reality
&lt;/h2&gt;

&lt;p&gt;I created a multi-VM example to document the behavior:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From vm1:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant ssh vm1
sudo python3 -m http.server 80 --bind 192.168.50.10

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From vm2:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant ssh vm2
curl 192.168.50.10 # Works! Returns directory listing

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VM-to-VM communication via static IPs works perfectly. Each VM is isolated, has its own processes, its own network namespace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From Windows:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl 192.168.50.10 # Timeout - can't distinguish which VM

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doesn’t work. The route points to the shared WSL IP, and Windows can’t tell which VM should handle the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation Over Pretending
&lt;/h2&gt;

&lt;p&gt;Instead of trying to make WSL2 behave like VirtualBox (impossible), I documented the reality in the README:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;WSL2 has a unique networking architecture that differs from traditional hypervisors. All distributions run on a single Hyper-V VM with shared networking. Static IPs work for inter-VM communication but not for Windows host access. Use port forwarding for host-to-VM connectivity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No marketing speak. No “it’s a feature not a bug.” Just: here’s how it works, here’s what you can do with it, here are the limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;The networking implementation is done and working within WSL2’s constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Static IP configuration (Ubuntu netplan + Debian systemd-networkd)&lt;/li&gt;
&lt;li&gt;✅ Port forwarding via netsh portproxy&lt;/li&gt;
&lt;li&gt;✅ Admin privilege checking&lt;/li&gt;
&lt;li&gt;✅ Multi-VM inter-VM communication&lt;/li&gt;
&lt;li&gt;✅ Clear documentation of limitations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: Cleaning up error handling, writing integration tests, and preparing for v0.4.0 release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The networking support is in the &lt;code&gt;feature/networking-support&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Clone the repo
git clone https://github.com/LeeShan87/vagrant-wsl2-provider
cd vagrant-wsl2-provider
git checkout feature/networking-support

# Install locally (requires admin)
rake install_local

# Try the networking example
cd examples/networking
vagrant up # Requires administrator privileges

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out &lt;code&gt;examples/multi-vm-network/README.md&lt;/code&gt; for the full explanation of WSL2 networking behavior and limitations.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Sometimes you start building a feature and discover the platform doesn’t work the way you thought. Instead of fighting reality, document it honestly and work within the constraints. The result might not match your original vision, but it’s better than pretending limitations don’t exist.&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>networking</category>
      <category>hyperv</category>
    </item>
    <item>
      <title>Three VM Crashes Later, I Built Data Disk Support</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sat, 01 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/three-vm-crashes-later-i-built-data-disk-support-1d77</link>
      <guid>https://forem.com/leeshan87/three-vm-crashes-later-i-built-data-disk-support-1d77</guid>
      <description>&lt;h2&gt;
  
  
  Three Times
&lt;/h2&gt;

&lt;p&gt;My colleague lost his VM three times. Not in a year - in the last few months we’ve been working together.&lt;/p&gt;

&lt;p&gt;Each time it happened, we’d shrug it off. That’s what Vagrant is for, right? Just run &lt;code&gt;vagrant up&lt;/code&gt; and you’re back in business. The development environment is code - it’s reproducible.&lt;/p&gt;

&lt;p&gt;Except it wasn’t.&lt;/p&gt;

&lt;p&gt;All three times, he had uncommitted work on that VM. Database migrations half-done. Configuration files tweaked just right but not yet committed. Local test data. You know, the stuff you’re “definitely going to commit tomorrow.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Everything Lives on the VM
&lt;/h2&gt;

&lt;p&gt;We work on Windows with VirtualBox VMs. But here’s the thing - we clone everything into the VM, not to shared folders. Why?&lt;/p&gt;

&lt;p&gt;Linux filesystem compatibility. Case-sensitive paths. Symlinks that actually work. File permissions that make sense. Performance that doesn’t crawl to a halt when node_modules has 50,000 files.&lt;/p&gt;

&lt;p&gt;So yeah, everything lives inside the VM. Which means when the VM dies, everything dies with it.&lt;/p&gt;

&lt;p&gt;After the third time, I decided: data disk support isn’t a nice-to-have. It’s more important than networking, more important than fancy features. My colleague shouldn’t lose work because we didn’t have a way to separate persistent data from ephemeral VMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;p&gt;I’d been building a Vagrant provider for WSL2 (because VirtualBox and WSL2 don’t play nice together, but that’s another story). Version 0.2.0 had snapshots and Docker support. Version 0.3.0 was going to be data disks.&lt;/p&gt;

&lt;p&gt;The idea was simple: mount a VHD as a data disk, store your code there, blow away the VM whenever you want. The data persists.&lt;/p&gt;

&lt;p&gt;Looked at how VirtualBox and VMware do it. Both support multiple data disks, VHD and VHDX formats. WSL2 has &lt;code&gt;wsl --mount --vhd&lt;/code&gt; for mounting VHD files. Should be straightforward, right?&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Problem: VHD Format
&lt;/h2&gt;

&lt;p&gt;Started implementing. First disk worked - created a VHDX, mounted it, formatted it. Great.&lt;/p&gt;

&lt;p&gt;Then I tried VHD format (for VirtualBox compatibility) and hit this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New-VHD : A parameter cannot be found that matches parameter name 'VHDType'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, what? The PowerShell docs said… oh. The format isn’t determined by a parameter. It’s determined by the file extension. &lt;code&gt;.vhd&lt;/code&gt; vs &lt;code&gt;.vhdx&lt;/code&gt;. The &lt;code&gt;-VHDType Dynamic&lt;/code&gt; parameter doesn’t exist.&lt;/p&gt;

&lt;p&gt;Fixed it. Removed the parameter, let the extension do the work. Both formats working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Second Problem: Provisioning Ate Everything
&lt;/h2&gt;

&lt;p&gt;Got multiple disks working. Example Vagrantfile had three disks - two default, one persistent. Ran &lt;code&gt;vagrant up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Checked inside the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -la /mnt/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four data directories. &lt;code&gt;/mnt/data1&lt;/code&gt;, &lt;code&gt;/mnt/data2&lt;/code&gt;, &lt;code&gt;/mnt/data3&lt;/code&gt;, &lt;code&gt;/mnt/data4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What? I only configured three disks.&lt;/p&gt;

&lt;p&gt;The provisioning script was mounting every &lt;code&gt;/dev/sd[b-z]&lt;/code&gt; device it found. Turns out WSL2 has some system disks (sda-sdd), and then our data disks start at &lt;code&gt;sde&lt;/code&gt;. My script was finding an extra disk somewhere and mounting it.&lt;/p&gt;

&lt;p&gt;Quick fix: change the loop to only process &lt;code&gt;/dev/sd[e-z]&lt;/code&gt; and limit it to the expected number of disks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXPECTED_DISKS=3
disk_count=0
for device in /dev/sd[e-z]; do
  if [$disk_count -ge $EXPECTED_DISKS]; then
    break
  fi
  # ... rest of the mounting logic
done

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have exactly three disks. No more, no less.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Third Problem: Admin Rights
&lt;/h2&gt;

&lt;p&gt;Ran the tests. Green across the board when running as admin. Great.&lt;/p&gt;

&lt;p&gt;Then tried &lt;code&gt;vagrant up&lt;/code&gt; as a regular user. Boom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to mount VHD. Administrator privileges are required.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course. Both &lt;code&gt;New-VHD&lt;/code&gt; (creating VHD files) and &lt;code&gt;wsl --mount&lt;/code&gt; (mounting them) need admin rights. That’s… annoying.&lt;/p&gt;

&lt;p&gt;But wait. After &lt;code&gt;vagrant halt&lt;/code&gt;, if you run &lt;code&gt;vagrant up&lt;/code&gt; again as a regular user, does it work?&lt;/p&gt;

&lt;p&gt;Tested it. The VM started fine. No errors.&lt;/p&gt;

&lt;p&gt;Looked closer. When the VM halts, the VHD files stay mounted at the host level (the &lt;code&gt;/dev/sde&lt;/code&gt;, &lt;code&gt;/dev/sdf&lt;/code&gt; devices are still there). When the VM starts again, those devices are already present. No need to re-mount. No need for admin rights.&lt;/p&gt;

&lt;p&gt;Added a check: before trying to mount, count how many &lt;code&gt;/dev/sd[e-z]&lt;/code&gt; devices are already in the distribution. If we have enough, skip the mounting step entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def data_disk_already_mounted?(expected_disk_count)
  result = Vagrant::Util::Subprocess.execute(
    "wsl", "-d", @config.distribution_name, "--", "lsblk", "-nd", "-o", "NAME"
  )
  device_count = result.stdout.lines.count { |line| line.match?(/^sd[e-z]$/) }
  device_count &amp;gt;= expected_disk_count
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the workflow is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First &lt;code&gt;vagrant up&lt;/code&gt; (admin required) - creates and mounts VHDs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vagrant halt&lt;/code&gt; - VM stops, VHDs stay mounted&lt;/li&gt;
&lt;li&gt;Second &lt;code&gt;vagrant up&lt;/code&gt; (no admin required) - disks already there, just start the VM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for my use case. You only need admin once to set things up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fourth Problem: Integration Tests
&lt;/h2&gt;

&lt;p&gt;Integration tests need admin rights too. But what if someone runs the test suite without admin?&lt;/p&gt;

&lt;p&gt;Added a check at the start of the data disk test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if (-not $isAdmin) {
    Write-Host "=== $TestName Test SKIPPED ===" -ForegroundColor Yellow
    Write-Host "Reason: Administrator privileges required for data disk tests"
    exit 0
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test gracefully skips instead of failing with cryptic errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works Now
&lt;/h2&gt;

&lt;p&gt;Version 0.3.0 ships with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple data disks per VM:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wsl.data_disk do |disk|
  disk.size = 10
  disk.format = 'vhdx'
end

wsl.data_disk do |disk|
  disk.size = 5
  disk.format = 'vhd'
end

wsl.data_disk do |disk|
  disk.path = '../persistent-data.vhdx' # Custom path, survives destroy
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Smart mounting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks if disks are already accessible before trying to mount&lt;/li&gt;
&lt;li&gt;Skips admin-requiring operations when possible&lt;/li&gt;
&lt;li&gt;Clear error messages when admin is actually needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Persistence:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default disks (in &lt;code&gt;.vagrant/&lt;/code&gt; directory) get cleaned up on &lt;code&gt;vagrant destroy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Custom-path disks survive destroy - your data is safe&lt;/li&gt;
&lt;li&gt;After destroy/up cycle, data on the persistent disk is intact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Testing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integration tests verify all scenarios&lt;/li&gt;
&lt;li&gt;Graceful skip when admin rights aren’t available&lt;/li&gt;
&lt;li&gt;Tests cover VHD/VHDX formats, persistence, cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PowerShell Integration Tests
&lt;/h2&gt;

&lt;p&gt;Since this is Windows-only functionality, I wrote PowerShell-based integration tests. Each test creates a real Vagrant environment, performs operations, and verifies the results.&lt;/p&gt;

&lt;p&gt;The data disk test verifies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Test 1: Creating VM with data disks
vagrant up --provider=wsl2

# Test 2: Verifying VHD files exist
# Checks for 2 default + 1 persistent disk

# Test 3-6: Mount verification and data writes
# Writing test data to each disk

# Test 7: VHD cleanup on destroy
vagrant destroy
# Default VHDs should be deleted
# Persistent VHD should survive

# Test 8: VM recreation
vagrant up
# New default disks (clean, no old data)
# Persistent disk retained data

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the full test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; rake test

Running: test_data_disk.ps1

Test 1: Creating VM with data disks
[PASS] VM created with data disks

Test 2: Verifying VHD files exist
[PASS] All VHD files created (2 default + 1 persistent)
  - data-disk-0.vhdx: 516 MB (default)
  - data-disk-1.vhd: 56.06 MB (default)
  - test-data-disk.vhdx: 740 MB (persistent)

Test 3: Verifying data disks are mounted
[PASS] Data disks are mounted

Test 4-6: Writing data to disks
[PASS] Data written to first disk
[PASS] Data written to second disk
[PASS] Data written to persistent disk

Test 7: Testing VHD cleanup on destroy
[PASS] Default VHD files cleaned up after destroy
[PASS] Persistent VHD survived destroy

Test 8: Testing VM recreation
[PASS] New default VHD files created on up
[PASS] New default disks are clean (no old data)
[PASS] Persistent disk retained data across destroy/up cycle

=== DataDisk Test PASSED ===

Test Summary
Passed: 6
Failed: 0

OVERALL: PASSED

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests run the actual Vagrant commands, create real VHD files, mount them in WSL2, write data, destroy the VM, and verify persistence. No mocks. Real integration testing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Claude:&lt;/strong&gt; Writing these tests caught several bugs before they shipped. The disk counting issue? Found it during test development. The admin rights check? Added after the test failed on a non-admin console. Integration tests are tedious to write but worth it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Real Win
&lt;/h2&gt;

&lt;p&gt;My colleague can now put his working directory on a persistent data disk. The VM is ephemeral. The data isn’t.&lt;/p&gt;

&lt;p&gt;VM got corrupted? &lt;code&gt;vagrant destroy &amp;amp;&amp;amp; vagrant up&lt;/code&gt;. Code is still there.&lt;/p&gt;

&lt;p&gt;Want to try a different distro? Switch VMs, mount the same data disk. Code is still there.&lt;/p&gt;

&lt;p&gt;Accidentally broke the system? Doesn’t matter. The stuff that matters is on a disk that survives.&lt;/p&gt;

&lt;p&gt;This is what I should have built first. Not snapshots, not networking, not fancy features. The ability to separate “the environment” from “the work.”&lt;/p&gt;

&lt;p&gt;Because losing code sucks. And it shouldn’t happen just because a VM died.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;The data disk support is solid. Next up: actually figuring out networking so VMs can talk to each other. But that can wait.&lt;/p&gt;

&lt;p&gt;Right now, my colleague’s code is safe. That’s worth more than any feature.&lt;/p&gt;

&lt;p&gt;Code’s at &lt;a href="https://github.com/LeeShan87/vagrant-wsl2-provider" rel="noopener noreferrer"&gt;github.com/LeeShan87/vagrant-wsl2-provider&lt;/a&gt;. Version 0.3.0 is tagged and ready. Admin rights required for setup, but after that you’re good.&lt;/p&gt;

&lt;p&gt;Try it. Mount your code on a persistent disk. Stop worrying about losing work.&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>vhd</category>
      <category>datapersistence</category>
    </item>
    <item>
      <title>Publishing My First Vagrant Plugin - v0.2.0 Goes Live</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Thu, 30 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/publishing-my-first-vagrant-plugin-v020-goes-live-e64</link>
      <guid>https://forem.com/leeshan87/publishing-my-first-vagrant-plugin-v020-goes-live-e64</guid>
      <description>&lt;h2&gt;
  
  
  One Star and a Decision
&lt;/h2&gt;

&lt;p&gt;So the Vagrant WSL2 Provider got its first GitHub star. From someone I don’t know. Just one random person finding the project useful enough to click that button.&lt;/p&gt;

&lt;p&gt;That’s when I realized - okay, maybe it’s time to actually publish this properly.&lt;/p&gt;

&lt;p&gt;Until now, this was just a tool I built for myself and my team. We needed to migrate from VirtualBox to WSL2, and Vagrant’s existing providers didn’t cut it. So I built one. It worked. We used it. End of story.&lt;/p&gt;

&lt;p&gt;But that star made me think - if someone else is interested, maybe I should make it actually installable. Not just “clone the repo and build it yourself” but proper &lt;code&gt;vagrant plugin install&lt;/code&gt; support.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Publishing Process
&lt;/h2&gt;

&lt;p&gt;I’ve never published a Vagrant plugin before. Hell, I’ve never published anything to RubyGems.org. So naturally, I asked Claude what the process looks like.&lt;/p&gt;

&lt;p&gt;Turns out it’s surprisingly simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make sure your gemspec is correct&lt;/li&gt;
&lt;li&gt;Update the CHANGELOG&lt;/li&gt;
&lt;li&gt;Register on RubyGems.org&lt;/li&gt;
&lt;li&gt;Build the gem&lt;/li&gt;
&lt;li&gt;Push it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No approval process. No waiting. No HashiCorp permission needed. Just build and push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemspec Cleanup
&lt;/h3&gt;

&lt;p&gt;The gemspec needed a few tweaks. Nothing major - just adding &lt;code&gt;required_ruby_version&lt;/code&gt; to avoid warnings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spec.required_ruby_version = "&amp;gt;= 2.7.0"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vagrant 2.2+ needs Ruby 2.7+ anyway, so this is safe.&lt;/p&gt;

&lt;p&gt;The metadata was already good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spec.metadata["allowed_push_host"] = "https://rubygems.org"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CHANGELOG for v0.2.0
&lt;/h3&gt;

&lt;p&gt;Updated the CHANGELOG to move everything from &lt;code&gt;[Unreleased]&lt;/code&gt; to &lt;code&gt;[0.2.0]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## [0.2.0] - 2025-10-30

### Added
- Full snapshot support (save, restore, list, delete)
- Support for `vagrant snapshot push/pop` commands
- `vagrant ssh -c` command execution support
- PowerShell-based integration test suite
- Docker support and systemd enablement on distribution start
- Comprehensive testing for various Linux distributions
- wsl.conf configuration support

### Features
- Snapshots stored as `.tar` files
- Complete distribution state preservation and restoration
- Direct command execution via `vagrant ssh -c`
- Automated integration tests
- Docker-in-WSL2 workflows with systemd support

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a solid release. Snapshot support was the big one - being able to save/restore WSL2 distribution state is huge for development workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  RubyGems.org Registration
&lt;/h3&gt;

&lt;p&gt;Went to rubygems.org, signed up. Email, password, done.&lt;/p&gt;

&lt;p&gt;And yes, I typo’d my username. Classic. But turns out it doesn’t matter - gem ownership is tied to email, not username. You can rename your profile anytime.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Push
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem build vagrant-wsl2-provider.gemspec

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got some warnings about URIs being the same (not a problem) and the Ruby version constraint (already fixed). Build succeeded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successfully built RubyGem
Name: vagrant-wsl2-provider
Version: 0.2.0

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the moment of truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem push vagrant-wsl2-provider-0.2.0.gem

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First time, so it asked for credentials. Email and password. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pushing gem to https://rubygems.org...
Successfully registered gem: vagrant-wsl2-provider (0.2.0)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Published. Live.&lt;/p&gt;

&lt;p&gt;Now anyone can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant plugin install vagrant-wsl2-provider

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a Roadmap
&lt;/h2&gt;

&lt;p&gt;While we were at it, we added a roadmap to the README. People should know what’s coming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v0.3 - Data Disk Support&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mount VirtualBox data disks in WSL2&lt;/li&gt;
&lt;li&gt;VHD/VMDK to WSL2 format conversion&lt;/li&gt;
&lt;li&gt;Support for development backup workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the next big thing. Our team has development environments backed up as VirtualBox data disks. We need a way to convert and mount those in WSL2. That’s the migration path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v0.4 - Legacy Distribution Support&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Non-interactive setup for Ubuntu 20.04, 22.04&lt;/li&gt;
&lt;li&gt;Support for Oracle Linux distributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some WSL distributions use the legacy registration system and require interactive setup. Need to work around that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v0.5 - Network Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fixed network support for multiple WSL2 distributions&lt;/li&gt;
&lt;li&gt;Network isolation and custom IP configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WSL2’s dynamic networking is… interesting. Multiple distributions with predictable networking would be nice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means
&lt;/h2&gt;

&lt;p&gt;The plugin is now officially public and installable. Version 0.2.0 is solid - snapshot support, SSH command execution, Docker/systemd integration, and comprehensive testing across multiple Linux distributions.&lt;/p&gt;

&lt;p&gt;It went from “internal tool” to “published open-source project” in about an hour. RubyGems makes it stupidly easy to publish.&lt;/p&gt;

&lt;p&gt;Next up: v0.3 and that VirtualBox data disk conversion. That’s the real migration blocker for our team.&lt;/p&gt;

&lt;p&gt;But for now - we’re live. The plugin is out there. And hopefully that one star becomes a few more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you’re on Windows with WSL2 and use Vagrant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant plugin install vagrant-wsl2-provider

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a Vagrantfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vagrant.configure("2") do |config|
  config.vm.box = "Ubuntu-24.04"

  config.vm.provider "wsl2" do |wsl|
    wsl.distribution_name = "my-dev-env"
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant up --provider=wsl2
vagrant snapshot save clean-install
vagrant ssh

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you’ve got a WSL2-based Vagrant environment with snapshot support.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/LeeShan87/vagrant-wsl2-provider" rel="noopener noreferrer"&gt;github.com/LeeShan87/vagrant-wsl2-provider&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>rubygems</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Docker Support for Vagrant WSL2 Provider - A Development Journey</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/building-docker-support-for-vagrant-wsl2-provider-a-development-journey-bim</link>
      <guid>https://forem.com/leeshan87/building-docker-support-for-vagrant-wsl2-provider-a-development-journey-bim</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Over the past few days, I’ve been working on adding Docker support to the &lt;a href="https://github.com/LeeShan87/vagrant-wsl2-provider" rel="noopener noreferrer"&gt;vagrant-wsl2-provider&lt;/a&gt; plugin. This turned out to be more challenging than expected, but the journey taught me a lot about WSL2’s internals, systemd initialization, and multi-distribution package management.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through the development process, the challenges we faced, and how we solved them to get Docker running on 8 different Linux distributions in WSL2 through Vagrant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge: Docker Needs systemd
&lt;/h2&gt;

&lt;p&gt;The first major hurdle was enabling systemd support in WSL2 distributions. Docker relies on systemd to manage the container daemon, but WSL2 doesn’t enable systemd by default. This is controlled by the &lt;code&gt;/etc/wsl.conf&lt;/code&gt; file, which must be created and configured before systemd will start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing wsl.conf Support
&lt;/h3&gt;

&lt;p&gt;We needed a clean API for users to configure WSL2 distributions. After some experimentation, we settled on a dotted syntax that feels natural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.vm.provider "wsl2" do |wsl|
  wsl.systemd = true # Enable systemd
  wsl.default_user = "vagrant" # Set default user
  wsl.hostname = "my-docker-vm" # Set hostname

  # Or use the longer form for advanced options:
  wsl.wsl_conf.boot.command = "service docker start"
  wsl.wsl_conf.network.generate_hosts = false
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this generates a proper &lt;code&gt;/etc/wsl.conf&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[boot]
systemd=true

[user]
default=vagrant

[network]
hostname=my-docker-vm

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Restart Problem
&lt;/h3&gt;

&lt;p&gt;Here’s where things got tricky. After writing the wsl.conf file, the distribution must be fully restarted for systemd to initialize. A simple &lt;code&gt;wsl --terminate&lt;/code&gt; wasn’t enough - we needed &lt;code&gt;wsl --shutdown&lt;/code&gt;, which stops &lt;strong&gt;all&lt;/strong&gt; WSL2 distributions and restarts the entire WSL2 backend.&lt;/p&gt;

&lt;p&gt;Initially, I overthought this and added complex systemd polling logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Over-engineered version (don't do this!)
halt
start
sleep 5
max_retries = 30
retry_count = 0
loop do
  result = check_systemd_status
  break if result == "running"
  retry_count += 1
  sleep 1
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it turned out we only needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Simple and effective
Vagrant::Util::Subprocess.execute("wsl", "--shutdown")
sleep 2
start

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real issue wasn’t systemd initialization time - it was missing the &lt;code&gt;iptables&lt;/code&gt; package!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing iptables Mystery
&lt;/h2&gt;

&lt;p&gt;After getting systemd running, Docker still failed to start with a cryptic error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to start daemon: Error initializing network controller:
failed to register "bridge" driver: failed to create NAT chain DOCKER:
iptables not found

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interestingly, running &lt;code&gt;vagrant reload&lt;/code&gt; after the initial &lt;code&gt;vagrant up&lt;/code&gt; would make Docker work perfectly. This led us down a rabbit hole trying to fix the “restart problem” when the real issue was simple: &lt;strong&gt;iptables wasn’t installed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;iptables&lt;/code&gt; to the package list solved it immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: Install required packages
  apt:
    name:
      - ca-certificates
      - curl
      - gnupg
      - iptables # This was the missing piece!
    state: present

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-Distribution Support: The Right Way
&lt;/h2&gt;

&lt;p&gt;Initially, we tried cramming all distribution support into a single Ansible playbook with numerous conditionals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: Install Docker
  package:
    name: "{{ 'moby-engine' if ansible_os_family == 'Debian' else 'docker' }}"
  when: # complex conditions...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This quickly became unmaintainable. We refactored to use &lt;strong&gt;OS-family-specific playbooks&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provisioning/
├── docker-debian.yml # Ubuntu, Debian, Kali (Moby)
├── docker-redhat.yml # AlmaLinux, Fedora (Podman)
└── docker-suse.yml # openSUSE (Docker)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Vagrantfile automatically selects the right playbook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;playbook = case distro
           when /Ubuntu|Debian|kali/i then "provisioning/docker-debian.yml"
           when /AlmaLinux|Fedora/i then "provisioning/docker-redhat.yml"
           when /openSUSE/i then "provisioning/docker-suse.yml"
           end

node.vm.provision "ansible_local" do |ansible|
  ansible.playbook = playbook
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much cleaner! Each playbook focuses on one OS family without conditional spaghetti.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Microsoft Moby for Ubuntu?
&lt;/h2&gt;

&lt;p&gt;You might wonder why we use Microsoft’s Moby packages for Ubuntu instead of the standard &lt;code&gt;docker.io&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Ubuntu: Microsoft Moby
- name: Install Moby Docker
  apt:
    name:
      - moby-engine
      - moby-cli
      - moby-containerd
      - moby-runc

# Debian/Kali: Standard docker.io
- name: Install Docker
  apt:
    name:
      - docker.io
      - containerd

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is &lt;strong&gt;Docker Desktop licensing&lt;/strong&gt;. Some enterprises are cautious about using &lt;code&gt;docker.io&lt;/code&gt; in WSL2 because of Docker Desktop’s commercial licensing restrictions on Windows. While this likely only applies to the GUI (not the engine itself), using Microsoft’s Moby distribution provides a clearer licensing path for commercial use.&lt;/p&gt;

&lt;p&gt;For Debian and Kali, we fall back to &lt;code&gt;docker.io&lt;/code&gt; since Microsoft doesn’t provide Moby packages for all Debian versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  RedHat Family: Podman as Docker
&lt;/h2&gt;

&lt;p&gt;On AlmaLinux and Fedora, installing the &lt;code&gt;docker&lt;/code&gt; package actually gives you Podman with Docker CLI compatibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: Install Podman (Docker compatible)
  dnf:
    name:
      - podman
      - podman-docker # Provides 'docker' command

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is RedHat’s preferred approach - Podman is daemonless and rootless by default, which is more secure. The &lt;code&gt;podman-docker&lt;/code&gt; package provides a &lt;code&gt;docker&lt;/code&gt; symlink, so users can run &lt;code&gt;docker run hello-world&lt;/code&gt; and it “just works” using Podman under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Across 8 Distributions
&lt;/h2&gt;

&lt;p&gt;The final test suite provisions Docker on 8 different Linux distributions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vagrant status
docker-almalinux8 running (wsl2)
docker-almalinux9 running (wsl2)
docker-debian running (wsl2)
docker-fedoralinux42 running (wsl2)
docker-ubuntu running (wsl2)
docker-ubuntu2404 running (wsl2)
docker-kalilinux running (wsl2)
docker-opensusetumbleweed running (wsl2)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one successfully runs the hello-world container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vagrant ssh docker-ubuntu2404 -c "docker run hello-world"

Hello from Docker!
This message shows that your installation appears to be working correctly.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What’s Next for v0.2.0
&lt;/h2&gt;

&lt;p&gt;Docker support is just the beginning. Here are additional features planned for the v0.2.0 release:&lt;/p&gt;

&lt;h3&gt;
  
  
  WSL Mount Support
&lt;/h3&gt;

&lt;p&gt;Enable mounting Windows directories and VHD/VHDX data disks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.vm.provider "wsl2" do |wsl|
  wsl.mount "D:\\data", "/mnt/data"
  wsl.attach_vhdx "D:\\backups\\linux-data.vhdx"
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will allow users to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share data between Windows and WSL2&lt;/li&gt;
&lt;li&gt;Back up Linux data to portable VHDX files&lt;/li&gt;
&lt;li&gt;Separate system and data for easier snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vagrant Snapshot Support
&lt;/h3&gt;

&lt;p&gt;Implement Vagrant’s snapshot functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant snapshot save docker-vm baseline
vagrant snapshot restore docker-vm baseline
vagrant snapshot list

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will leverage WSL2’s export/import functionality to create point-in-time snapshots of entire distributions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start simple&lt;/strong&gt; - My complex systemd polling was unnecessary. The real issue was a missing package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test early, test often&lt;/strong&gt; - Running &lt;code&gt;vagrant destroy &amp;amp;&amp;amp; vagrant up&lt;/code&gt; repeatedly helped identify the iptables issue that only appeared on fresh installations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Separate concerns&lt;/strong&gt; - OS-specific playbooks are much easier to maintain than one giant conditional playbook.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read error messages carefully&lt;/strong&gt; - “iptables not found” was right there in the logs, but I focused on the wrong problem initially.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WSL2 is not a VM&lt;/strong&gt; - It has unique behaviors (like needing &lt;code&gt;wsl --shutdown&lt;/code&gt; for systemd) that require understanding its architecture.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The vagrant-wsl2-provider with Docker support is available on GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/LeeShan87/vagrant-wsl2-provider.git
cd vagrant-wsl2-provider/test/docker-test
vagrant up docker-ubuntu2404
vagrant ssh docker-ubuntu2404 -c "docker run hello-world"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Adding Docker support to the Vagrant WSL2 provider was a journey through WSL2 internals, systemd initialization, and cross-distribution package management. The final solution is elegant: a simple configuration API, clean OS-specific playbooks, and reliable Docker/Podman installation across 8 Linux distributions.&lt;/p&gt;

&lt;p&gt;The v0.2.0 release will bring even more functionality with VHD mounting and snapshot support. Stay tuned!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions or suggestions? Open an issue on &lt;a href="https://github.com/LeeShan87/vagrant-wsl2-provider/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or reach out!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>docker</category>
      <category>systemd</category>
    </item>
    <item>
      <title>Snapshots and Testing: Building Real Tests for a Vagrant Provider</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/snapshots-and-testing-building-real-tests-for-a-vagrant-provider-ojm</link>
      <guid>https://forem.com/leeshan87/snapshots-and-testing-building-real-tests-for-a-vagrant-provider-ojm</guid>
      <description>&lt;h2&gt;
  
  
  The Testing Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;After getting Docker support working, I wanted to add snapshot functionality to the vagrant-wsl2-provider. But there was a nagging problem I’d been avoiding: how do you actually test a Vagrant provider plugin?&lt;/p&gt;

&lt;p&gt;The “proper” way would be to write Ruby unit tests with RSpec, mock all of Vagrant’s internals, and test each component in isolation. But here’s the thing - Vagrant is a massive gem with heavy dependencies. Just getting the test environment set up requires pulling in the entire Vagrant gem, which then requires native extensions, specific Ruby versions, and a whole dependency chain that’s… painful.&lt;/p&gt;

&lt;p&gt;I tried. I really did. Created a &lt;code&gt;spec/&lt;/code&gt; directory, added RSpec, started writing tests. Got errors about missing Vagrant classes. Added Vagrant as a dev dependency. Got compilation errors for native extensions. Spent an hour on dependency hell before I stopped and asked myself: &lt;strong&gt;what am I actually trying to test here?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration Tests: The Pragmatic Choice
&lt;/h2&gt;

&lt;p&gt;I don’t care if my Driver class can be instantiated in isolation. I care if &lt;code&gt;vagrant snapshot save&lt;/code&gt; actually works when a user runs it. I care if &lt;code&gt;vagrant ssh -c "command"&lt;/code&gt; returns output. I care if &lt;code&gt;vagrant up&lt;/code&gt; creates a working WSL2 distribution.&lt;/p&gt;

&lt;p&gt;So I made a decision that probably horrifies some people: &lt;strong&gt;PowerShell-based integration tests.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# test/integration/test_basic.ps1
vagrant up --provider=wsl2
if ($LASTEXITCODE -eq 0) {
    Write-Host "[PASS] vagrant up succeeded" -ForegroundColor Green
} else {
    throw "vagrant up failed"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dead simple. No mocks. No stubs. Just run the actual command and check if it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Makes Sense
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests real behavior&lt;/strong&gt; - What users actually experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No mocking infrastructure&lt;/strong&gt; - No dependency on Vagrant’s internal APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast to write&lt;/strong&gt; - PowerShell is native to Windows where WSL2 runs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to debug&lt;/strong&gt; - When a test fails, you can literally copy-paste the command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Ruby dependency hell&lt;/strong&gt; - Just PowerShell and Vagrant installed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test suite structure is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test/integration/
├── test_basic.ps1 # vagrant up, status, ssh, destroy
├── test_snapshot.ps1 # snapshot lifecycle
└── run_all_tests.ps1 # runs everything

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;rake test&lt;/code&gt;. That’s it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Snapshot Implementation
&lt;/h2&gt;

&lt;p&gt;WSL2 already has everything we need for snapshots: &lt;code&gt;wsl --export&lt;/code&gt; and &lt;code&gt;wsl --import&lt;/code&gt;. The challenge was wrapping this in Vagrant’s provider capability system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Driver Methods
&lt;/h3&gt;

&lt;p&gt;The Driver class handles the actual WSL2 operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def save_snapshot(snapshot_name)
  snapshot_file = snapshot_path(snapshot_name)

  @machine.ui.info "Saving snapshot: #{snapshot_name}"
  execute("wsl", "--export", @config.distribution_name, snapshot_file)
  @machine.ui.success "Snapshot saved: #{snapshot_name}"
end

def restore_snapshot(snapshot_name)
  snapshot_file = snapshot_path(snapshot_name)

  # Unregister current distribution
  halt if state == :running
  execute("wsl", "--unregister", @config.distribution_name)

  # Import the snapshot
  dist_dir = distribution_path
  execute("wsl", "--import", @config.distribution_name,
          dist_dir, snapshot_file, "--version", @config.version.to_s)
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Snapshots are just tar files stored in &lt;code&gt;.vagrant/machines/{name}/wsl2/snapshots/&lt;/code&gt;. Nothing fancy, which is exactly what we want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Provider Capabilities
&lt;/h3&gt;

&lt;p&gt;Vagrant expects providers to register capabilities for snapshot operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# lib/vagrant-wsl2-provider/plugin.rb
provider_capability "wsl2", "snapshot_list" do
  require_relative "cap/snapshot_list"
  Cap::SnapshotList
end

provider_capability "wsl2", "snapshot_save" do
  require_relative "cap/snapshot_save"
  Cap::SnapshotSave
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each capability is just a thin wrapper that calls the driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# lib/vagrant-wsl2-provider/cap/snapshot_save.rb
module VagrantPlugins
  module WSL2
    module Cap
      class SnapshotSave
        def self.snapshot_save(machine, snapshot_name)
          driver = machine.provider.instance_variable_get(:@driver)
          driver.save_snapshot(snapshot_name)
        end
      end
    end
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us full Vagrant snapshot support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vagrant snapshot save before-experiment
vagrant snapshot restore before-experiment
vagrant snapshot list
vagrant snapshot delete old-snapshot

# Bonus: push/pop work automatically!
vagrant snapshot push
vagrant snapshot pop

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The &lt;code&gt;vagrant ssh -c&lt;/code&gt; Rabbit Hole
&lt;/h2&gt;

&lt;p&gt;While testing snapshots, I noticed &lt;code&gt;vagrant ssh -c "echo test"&lt;/code&gt; would execute successfully (exit code 0) but show no output. This was… not great.&lt;/p&gt;

&lt;p&gt;The problem was that Vagrant’s built-in &lt;code&gt;SSHRun&lt;/code&gt; action wasn’t compatible with the WSL2 communicator. It expected SSH-style communication, but we use direct WSL command execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom SSHRun Action
&lt;/h3&gt;

&lt;p&gt;The solution was a custom action that properly streams output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# lib/vagrant-wsl2-provider/action/ssh_run.rb
def call(env)
  command = env[:ssh_run_command]

  if command
    exit_status = env[:machine].communicate.execute(command, error_check: false) do |type, data|
      case type
      when :stdout
        $stdout.print(data)
        $stdout.flush
      when :stderr
        $stderr.print(data)
        $stderr.flush
      end
    end

    env[:ssh_run_exit_status] = exit_status
  end

  @app.call(env)
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this required updating the communicator to accept a block parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# lib/vagrant-wsl2-provider/communicator.rb
def execute(command, opts = {}, &amp;amp;block)
  result = Vagrant::Util::Subprocess.execute(
    "wsl", "-d", distribution_name, "-u", "vagrant", "--",
    "bash", "-l", "-c", encoded_command,
    :notify =&amp;gt; [:stdin, :stdout, :stderr]
  ) do |type, data|
    # Call the block if provided
    block.call(type, data) if block_given?

    # Default output handling
    puts data if type == :stdout &amp;amp;&amp;amp; !block_given?
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;vagrant ssh -c&lt;/code&gt; works as expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vagrant ssh -c "uname -a"
Linux vagrant-wsl2-basic 5.15.133.1-microsoft-standard-WSL2 ...

$ vagrant ssh -c "docker ps"
CONTAINER ID IMAGE COMMAND ...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect for scripting!&lt;/p&gt;

&lt;h2&gt;
  
  
  WSL2 Output Encoding Hell
&lt;/h2&gt;

&lt;p&gt;One weird issue that bit me during testing: WSL commands on Windows return text with null bytes (&lt;code&gt;\0&lt;/code&gt;) scattered throughout. This breaks PowerShell’s string matching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# This doesn't work
$wslList = wsl -l -v
if ($wslList -match "vagrant-wsl2-basic") { # Never matches!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to strip null bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# This works
$wslList = (wsl -l -v | Out-String) -replace '\0', ''
if ($wslList -match "vagrant-wsl2-basic") { # Works!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is now documented in the test template so future tests don’t hit this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Infrastructure as Documentation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;test/&lt;/code&gt; directory felt wrong. These weren’t just tests - they were working examples. So I renamed it to &lt;code&gt;examples/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;examples/
├── basic/ # Minimal Vagrantfile
├── snapshot/ # Snapshot demo
├── provisioners/ # Shell/file/ansible examples
├── docker-test/ # Docker with systemd
└── test-distros/ # Various Linux distributions

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directory has a working Vagrantfile that serves three purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User documentation&lt;/strong&gt; - “Here’s how to use feature X”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration test fixture&lt;/strong&gt; - Tests use these examples&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual testing&lt;/strong&gt; - Quick &lt;code&gt;vagrant up&lt;/code&gt; to try something&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The integration tests just reference these examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ExampleDir = Join-Path $PSScriptRoot "..\..\examples\snapshot"
Push-Location $ExampleDir

vagrant up --provider=wsl2
vagrant snapshot save test-snapshot
vagrant snapshot restore test-snapshot

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No duplication. The docs and tests stay in sync automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Results
&lt;/h2&gt;

&lt;p&gt;The test suite validates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic lifecycle: up, status, ssh-config, destroy&lt;/li&gt;
&lt;li&gt;WSL distribution creation and cleanup&lt;/li&gt;
&lt;li&gt;SSH command execution with output&lt;/li&gt;
&lt;li&gt;Snapshot save/restore/list/delete&lt;/li&gt;
&lt;li&gt;Snapshot push/pop (auto-generated names)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Current status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;========================================
Test Summary
========================================
Passed: 2
Failed: 0

OVERALL: PASSED

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a huge test suite, but it covers the core workflows users actually care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Integration tests &amp;gt; Unit tests for infrastructure tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you’re wrapping external commands (WSL, Docker, systemd), integration tests give you more confidence. Mock-heavy unit tests just test your mocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use the platform’s native tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PowerShell on Windows is fine. Don’t fight it by trying to force Ruby/RSpec everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Examples should be runnable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your documentation includes code, make it actual working code that’s tested. “Docs that lie” is worse than no docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Block parameters are powerful&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ruby blocks for streaming callbacks are elegant. Don’t be afraid to use them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Exit codes matter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Always check &lt;code&gt;$LASTEXITCODE&lt;/code&gt; in PowerShell. A command can “succeed” but do nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;With snapshots and testing infrastructure in place, v0.2.0 is almost ready. The last major feature is &lt;strong&gt;WSL mount support&lt;/strong&gt; for VHD/VHDX data disks, but that’s for the next iteration.&lt;/p&gt;

&lt;p&gt;For now, I’m happy that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapshots work reliably&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vagrant ssh -c&lt;/code&gt; returns output&lt;/li&gt;
&lt;li&gt;Tests actually test real behavior&lt;/li&gt;
&lt;li&gt;The code is properly documented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not bad for a plugin nobody asked for that I’m building because I’m stuck on Windows. 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;The snapshot support is now merged into &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/LeeShan87/vagrant-wsl2-provider.git
cd vagrant-wsl2-provider
rake install_local

cd examples/snapshot
vagrant up --provider=wsl2
vagrant snapshot save clean
# ... experiment ...
vagrant snapshot restore clean

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rake test # All tests
rake test_basic # Just basic functionality
rake test_snapshot # Just snapshot tests

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Questions? Open an issue on &lt;a href="https://github.com/LeeShan87/vagrant-wsl2-provider/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I’m always curious how people are (or aren’t) using this thing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>testing</category>
      <category>snapshots</category>
    </item>
    <item>
      <title>Building a Vagrant WSL2 Provider: Testing Journey and Unexpected Discoveries</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Mon, 29 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/building-a-vagrant-wsl2-provider-testing-journey-and-unexpected-discoveries-3i8l</link>
      <guid>https://forem.com/leeshan87/building-a-vagrant-wsl2-provider-testing-journey-and-unexpected-discoveries-3i8l</guid>
      <description>&lt;h2&gt;
  
  
  The Final Push to v0.1.0
&lt;/h2&gt;

&lt;p&gt;After testing the provider on my corporate machine, I discovered a bug related to the missing Windows HOME environment variable. The provider was trying to cache files on a NAS drive (mapped as U:) instead of the local user directory. After fixing this by switching to Vagrant.user_data_path, I felt confident that v0.1.0 was nearly ready.&lt;/p&gt;

&lt;p&gt;All that remained was a comprehensive test run across all available WSL distributions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The –name Flag Mystery
&lt;/h2&gt;

&lt;p&gt;While reading through the WSL help documentation, I noticed the –name flag for wsl –install. I was puzzled - what’s the point of this flag?&lt;/p&gt;

&lt;p&gt;I had a distant memory from years ago: WSL would reuse existing VM disks when creating multiple instances. If you didn’t create a clean snapshot immediately after installation, the second VM would inherit changes from the first one. This was exactly why my provider prevents creating distributions from already-in-use WSL instances.&lt;/p&gt;

&lt;p&gt;But if that’s true, what does the –name flag actually do?&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing the –name Flag
&lt;/h3&gt;

&lt;p&gt;Time to test this assumption. I created two Ubuntu 24.04 instances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# First instance
wsl --install Ubuntu-24.04 --name Ubuntu-First --no-launch
wsl -d Ubuntu-First bash -c "echo 'Test file from first' &amp;gt; ~/testfile.txt"

# Second instance
wsl --install Ubuntu-24.04 --name Ubuntu-Second --no-launch
wsl -d Ubuntu-Second bash -c "ls -la ~/"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: The second instance was completely empty. No testfile.txt. Each instance has its own independent virtual disk.&lt;/p&gt;

&lt;p&gt;Wow… I remembered wrong?&lt;/p&gt;

&lt;h3&gt;
  
  
  When Did This Flag Appear?
&lt;/h3&gt;

&lt;p&gt;A quick search revealed the –name flag was added to WSL around 2023 (version 2.4.4), documented in microsoft/WSL#9210. The last time I worked extensively with WSL was around 2021-2022, when this flag didn’t exist yet - and Ubuntu 24.04 wasn’t available either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Legacy Distributions
&lt;/h3&gt;

&lt;p&gt;But wait - what about older distributions like Ubuntu 20.04 and 22.04?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wsl --install Ubuntu-20.04 --name Ubuntu2004-Test --no-launch
# Result: Installation completes but distribution doesn't appear in wsl -l

wsl --install Ubuntu-20.04 # Interactive installation
# Now it appears in wsl -l

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discovery: Legacy distributions (Ubuntu 20.04/22.04, Oracle Linux) don’t properly register with the –no-launch flag. They require interactive setup with username/password prompts. Only modern distributions support the new registration system.&lt;/p&gt;

&lt;p&gt;So I was right - but only for legacy distributions!&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Purpose: Caching
&lt;/h3&gt;

&lt;p&gt;Even for modern distributions with the –name flag, there’s still value in caching. While each named instance creates an independent VM, the installation always downloads from the Microsoft Store.&lt;/p&gt;

&lt;p&gt;My provider’s clean distribution cache speeds this up significantly - instead of repeated downloads, it creates one clean tar file locally and imports it with different names. Much faster for creating multiple instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Great Multi-Distribution Test
&lt;/h2&gt;

&lt;p&gt;Now for the marathon: testing all available WSL distributions with shell, file, and Ansible provisioners.&lt;/p&gt;

&lt;h3&gt;
  
  
  Success Stories
&lt;/h3&gt;

&lt;p&gt;8 distributions working flawlessly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AlmaLinux-8, AlmaLinux-9&lt;/li&gt;
&lt;li&gt;Debian&lt;/li&gt;
&lt;li&gt;FedoraLinux-42&lt;/li&gt;
&lt;li&gt;Ubuntu, Ubuntu-24.04&lt;/li&gt;
&lt;li&gt;Kali-Linux&lt;/li&gt;
&lt;li&gt;openSUSE-Tumbleweed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Problem Children
&lt;/h3&gt;

&lt;p&gt;Legacy Distributions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu-20.04, Ubuntu-22.04&lt;/li&gt;
&lt;li&gt;OracleLinux (all versions: 7.9, 8.10, 9.5)&lt;/li&gt;
&lt;li&gt;Issue: –no-launch flag doesn’t register them properly&lt;/li&gt;
&lt;li&gt;Require interactive setup (username/password)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Guest Detection Failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SUSE-Linux-Enterprise-15-SP6/SP7&lt;/li&gt;
&lt;li&gt;Error: “The guest operating system of the machine could not be detected!”&lt;/li&gt;
&lt;li&gt;File provisioner fails without guest capability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bleeding Edge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AlmaLinux-10, AlmaLinux-Kitten-10&lt;/li&gt;
&lt;li&gt;Ansible provisioner fails: EPEL repositories don’t exist yet for these versions&lt;/li&gt;
&lt;li&gt;Error: curl: (22) The requested URL returned error: 404 for EPEL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Minimal Distributions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;archlinux&lt;/li&gt;
&lt;li&gt;Missing sudo by default - Ansible provisioner fails&lt;/li&gt;
&lt;li&gt;Would need pacman -Sy –noconfirm sudo pre-provisioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shared Folder Issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;openSUSE-Leap-15.6&lt;/li&gt;
&lt;li&gt;Silent failure creating shared folders&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  v0.1.0: Good Enough for a PoC
&lt;/h2&gt;

&lt;p&gt;I think this is sufficient for a proof-of-concept v0.1.0 release.&lt;/p&gt;

&lt;p&gt;Working Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create and manage WSL2 distributions through Vagrant&lt;/li&gt;
&lt;li&gt;Standard commands: up, status, halt, destroy, ssh&lt;/li&gt;
&lt;li&gt;Provisioners: shell, file, ansible_local&lt;/li&gt;
&lt;li&gt;Synced folders&lt;/li&gt;
&lt;li&gt;Clean distribution caching for faster deployment&lt;/li&gt;
&lt;li&gt;8 tested and working distributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still Untested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU and memory limit configuration&lt;/li&gt;
&lt;li&gt;Docker installation and container execution&lt;/li&gt;
&lt;li&gt;Snapshot creation and restore&lt;/li&gt;
&lt;li&gt;Network configuration&lt;/li&gt;
&lt;li&gt;GUI application support (WSLg)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These would be good scope for v0.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Production-Ready, But a Solid Foundation
&lt;/h2&gt;

&lt;p&gt;I wouldn’t recommend this for production yet, but it’s already a solid starting point for developers who need Vagrant-like workflows on WSL2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Build This?
&lt;/h3&gt;

&lt;p&gt;I’ll be honest: I’m not a fan of Microsoft-centric solutions. I’m building this plugin because developers - myself included - are increasingly forced to use WSL on Windows. Corporate environments, client requirements, and the Windows ecosystem all push us in this direction.&lt;/p&gt;

&lt;p&gt;My hope is that this provider offers a smoother transition path for developers who find themselves in this situation. If you’re used to Vagrant workflows with VirtualBox or VMware, but now need to work with WSL2, this plugin aims to make that transition less painful.&lt;/p&gt;

&lt;p&gt;The code is available on GitHub at &lt;a href="https://github.com/LeeShan87/vagrant-wsl2-provider" rel="noopener noreferrer"&gt;https://github.com/LeeShan87/vagrant-wsl2-provider&lt;/a&gt;. Contributions, bug reports, and feedback welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;v0.2 roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VirtualBox VM import support (convert existing Vagrant boxes to WSL2)&lt;/li&gt;
&lt;li&gt;Resource limit testing (CPU/memory)&lt;/li&gt;
&lt;li&gt;Docker-in-WSL2 workflows&lt;/li&gt;
&lt;li&gt;Snapshot/restore functionality&lt;/li&gt;
&lt;li&gt;Better error handling for legacy distributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>windows</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Vagrant WSL2 Provider: Clean Development Environments on Windows</title>
      <dc:creator>Zoltan Toma</dc:creator>
      <pubDate>Sat, 27 Sep 2025 00:00:00 +0000</pubDate>
      <link>https://forem.com/leeshan87/building-a-vagrant-wsl2-provider-clean-development-environments-on-windows-4hff</link>
      <guid>https://forem.com/leeshan87/building-a-vagrant-wsl2-provider-clean-development-environments-on-windows-4hff</guid>
      <description>&lt;h1&gt;
  
  
  Building a Vagrant WSL2 Provider: Clean Development Environments on Windows
&lt;/h1&gt;

&lt;p&gt;Working with development environments on Windows has always been a challenge. While VirtualBox and VMware provide excellent virtualization, they come with resource overhead and performance penalties. Windows Subsystem for Linux 2 (WSL2) offers a compelling alternative with near-native performance, but lacks the standardized workflow that Vagrant provides.&lt;/p&gt;

&lt;p&gt;This led me to an interesting project: &lt;strong&gt;building a custom Vagrant provider for WSL2&lt;/strong&gt; that combines the best of both worlds - the familiar Vagrant workflow with WSL2’s performance benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;While traditional virtualization solutions like VirtualBox and VMware remain excellent choices for development, many enterprise Windows environments face significant constraints that make WSL2 the only viable high-performance option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise Windows Constraints
&lt;/h3&gt;

&lt;p&gt;Modern corporate Windows deployments often enforce security policies that create development challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtualization-Based Security (VBS)&lt;/strong&gt; - When enabled, VirtualBox becomes unusable or extremely slow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device Guard/Credential Guard&lt;/strong&gt; - Prevents traditional hypervisors from accessing hardware virtualization features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nested virtualization overhead&lt;/strong&gt; - VMware Workstation suffers significant performance penalties under Hyper-V&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security compliance&lt;/strong&gt; - Many organizations require these security features, leaving no choice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a &lt;strong&gt;forced migration&lt;/strong&gt; scenario where developers lose access to their familiar Vagrant + VirtualBox/VMware workflows, precisely when they need reproducible development environments most.&lt;/p&gt;

&lt;h3&gt;
  
  
  WSL2 Technical Challenges
&lt;/h3&gt;

&lt;p&gt;Beyond enterprise constraints, WSL2 distributions behave differently from traditional VMs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No persistent “running” state&lt;/strong&gt; - WSL2 distributions automatically sleep when no processes are active&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No built-in SSH server&lt;/strong&gt; - WSL2 uses direct command execution instead of SSH&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different lifecycle management&lt;/strong&gt; - WSL2 uses &lt;code&gt;wsl.exe&lt;/code&gt; commands rather than hypervisor APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contamination risk&lt;/strong&gt; - Existing WSL2 distributions may have modifications that break reproducibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Gap
&lt;/h3&gt;

&lt;p&gt;This leaves Windows enterprise developers in a difficult position: they need the &lt;strong&gt;performance and compatibility of WSL2&lt;/strong&gt; but lose the &lt;strong&gt;standardized workflows and tooling ecosystem&lt;/strong&gt; that Vagrant provides. The WSL2 provider bridges this gap, restoring familiar development workflows in constrained environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The WSL2 provider follows Vagrant’s plugin architecture with several key components:&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Components
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plugin Registration&lt;/strong&gt; (&lt;code&gt;plugin.rb&lt;/code&gt;) - Registers the provider and communicator with Vagrant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider Implementation&lt;/strong&gt; (&lt;code&gt;provider.rb&lt;/code&gt;) - Implements Vagrant’s provider interface with WSL2-specific actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Communicator&lt;/strong&gt; (&lt;code&gt;communicator.rb&lt;/code&gt;) - Handles command execution using native WSL commands instead of SSH&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action Classes&lt;/strong&gt; - Handle create, destroy, halt, start, and provisioning operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration&lt;/strong&gt; (&lt;code&gt;config.rb&lt;/code&gt;) - WSL2-specific configuration options&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Clean Base Distribution Cache
&lt;/h3&gt;

&lt;p&gt;One of the most critical features is the &lt;strong&gt;clean base distribution cache system&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Check if we have a clean cached version
cached_tar_path = File.join(cache_dir, "#{clean_base_name}.tar")

unless File.exist?(cached_tar_path)
  # Check if the original distribution already exists (dirty)
  if wsl_distribution_installed?(box_name)
    raise Errors::DirtyDistributionExists,
          name: box_name,
          clean_name: clean_base_name,
          cache_path: cache_dir
  end

  # Create clean base distribution
  create_clean_base_distribution(env, box_name, clean_base_name, cached_tar_path, cache_dir)
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures every &lt;code&gt;vagrant up&lt;/code&gt; starts with a pristine environment by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detecting dirty distributions&lt;/strong&gt; - Fails if base distribution already exists with modifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creating clean cache&lt;/strong&gt; - Fresh install → export → cache → remove original&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project isolation&lt;/strong&gt; - Each project gets its own distribution from clean cache&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  WSL2 Distribution Lifecycle
&lt;/h2&gt;

&lt;p&gt;WSL2 distributions behave differently from traditional VMs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### Vagrant Status Meanings
- `stopped` = Distribution exists, no active processes (ready to use)
- `running` = Distribution has active processes
- `not created` = Distribution doesn't exist

### Typical Workflow
vagrant up --provider=wsl2 # Creates distribution (shows "stopped")
vagrant ssh # Starts shell (shows "running")
exit # Shell closes (returns to "stopped")
vagrant status # Shows "stopped" - this is normal!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatic sleep behavior is actually beneficial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource efficient&lt;/strong&gt; - No idle processes consuming memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant wake&lt;/strong&gt; - Commands execute immediately from “stopped” state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same experience&lt;/strong&gt; - &lt;code&gt;vagrant ssh&lt;/code&gt; works regardless of state&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Native WSL2 Communication
&lt;/h2&gt;

&lt;p&gt;Instead of setting up SSH servers, the provider uses a custom communicator that executes commands directly through WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def execute(command, opts = {})
  distribution_name = @machine.id

  result = Vagrant::Util::Subprocess.execute(
    "wsl", "-d", distribution_name, "-u", "vagrant", "--",
    "bash", "-l", "-c", command
  )

  result.exit_code
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better performance&lt;/strong&gt; - No SSH overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native integration&lt;/strong&gt; - Direct WSL command execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler setup&lt;/strong&gt; - No SSH key management required&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Vagrant User Setup
&lt;/h2&gt;

&lt;p&gt;For full Vagrant ecosystem compatibility, the provider automatically sets up a standard &lt;code&gt;vagrant&lt;/code&gt; user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def setup_vagrant_user(distribution_name)
  run_in_distribution(distribution_name, [
    "useradd -m -s /bin/bash vagrant",
    "echo 'vagrant:vagrant' | chpasswd",
    "usermod -aG sudo vagrant",
    "echo 'vagrant ALL=(ALL) NOPASSWD:ALL' | tee /etc/sudoers.d/vagrant"
  ])
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures compatibility with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ansible provisioners&lt;/strong&gt; - Expect &lt;code&gt;vagrant&lt;/code&gt; user with sudo access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell provisioners&lt;/strong&gt; - Run as &lt;code&gt;vagrant&lt;/code&gt; user by default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared folders&lt;/strong&gt; - Mount to &lt;code&gt;/home/vagrant&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH forwarding&lt;/strong&gt; - Uses &lt;code&gt;vagrant&lt;/code&gt; user’s home directory&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Provisioning Support
&lt;/h2&gt;

&lt;p&gt;The provider supports full Vagrant provisioning through the custom communicator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.vm.provision "shell", inline: &amp;lt;&amp;lt;-SHELL
  echo "Hello from WSL2 Vagrant provider!"
  apt-get update
  apt-get install -y nginx
SHELL

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provisioning workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Script upload&lt;/strong&gt; - Vagrant uploads script to &lt;code&gt;/tmp/vagrant-shell&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission setup&lt;/strong&gt; - Makes script executable and sets ownership&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution&lt;/strong&gt; - Runs script as &lt;code&gt;vagrant&lt;/code&gt; user with sudo access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup&lt;/strong&gt; - Removes temporary files&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Familiar workflow&lt;/strong&gt; - Same Vagrant commands across platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform flexibility&lt;/strong&gt; - Easy switching between VM and WSL2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced resource usage&lt;/strong&gt; - WSL2 uses fewer system resources than VMs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corporate environment friendly&lt;/strong&gt; - WSL2 alternative when VMs are restricted&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Teams
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistent environments&lt;/strong&gt; - Same configuration across team members&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform choice&lt;/strong&gt; - Teams can choose VM vs WSL2 per member preference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified onboarding&lt;/strong&gt; - Single setup process regardless of platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean, reproducible environments&lt;/strong&gt; - Every &lt;code&gt;vagrant up&lt;/code&gt; starts fresh&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Use This Provider
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ &lt;strong&gt;Ideal Use Cases&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Windows environments&lt;/strong&gt; with VBS/Device Guard requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-constrained environments&lt;/strong&gt; where traditional virtualization is blocked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource-limited machines&lt;/strong&gt; where VMs are too heavy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed teams&lt;/strong&gt; with both Windows and Linux/macOS developers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing Vagrant workflows&lt;/strong&gt; that need to work on WSL2&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚠️ &lt;strong&gt;Consider Alternatives When&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You have full OS choice&lt;/strong&gt; - Native Linux development is still superior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VirtualBox/VMware work fine&lt;/strong&gt; - Traditional virtualization offers better isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy virtualization needs&lt;/strong&gt; - Multiple concurrent VMs, complex networking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel development&lt;/strong&gt; - Need direct hardware access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows-specific development&lt;/strong&gt; - Native Windows tooling might be more appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎯 &lt;strong&gt;Sweet Spot&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This provider excels in &lt;strong&gt;enterprise Windows environments&lt;/strong&gt; where developers need Linux-compatible development workflows but are constrained by corporate security policies. It’s not necessarily the best development environment overall, but it’s often the &lt;strong&gt;best available option&lt;/strong&gt; in Windows-centric organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;p&gt;The provider successfully implements:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Core Vagrant workflow&lt;/strong&gt; - &lt;code&gt;vagrant up/halt/destroy/ssh/status&lt;/code&gt;✅ &lt;strong&gt;Clean base distribution cache&lt;/strong&gt; - Prevents environment contamination ✅ &lt;strong&gt;Native WSL2 communication&lt;/strong&gt; - Direct command execution without SSH ✅ &lt;strong&gt;Vagrant user setup&lt;/strong&gt; - Full ecosystem compatibility ✅ &lt;strong&gt;Shell provisioning&lt;/strong&gt; - Native WSL command execution ✅ &lt;strong&gt;Smart default handling&lt;/strong&gt; - Respects user’s existing WSL configuration&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;Future development will focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ansible provisioner support&lt;/strong&gt; - Testing and optimization for ansible_local&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared folders implementation&lt;/strong&gt; - Project directory mounting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network configuration&lt;/strong&gt; - Port forwarding and advanced networking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Box conversion pipeline&lt;/strong&gt; - VirtualBox → WSL2 automatic conversion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-machine support&lt;/strong&gt; - Multiple WSL2 distributions per project&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a Vagrant WSL2 provider has been an exciting journey into Vagrant’s plugin architecture and WSL2’s capabilities. The combination provides a powerful development environment solution that bridges the gap between traditional VM-based workflows and modern container-like performance.&lt;/p&gt;

&lt;p&gt;The provider demonstrates that with careful architecture and attention to Vagrant’s ecosystem requirements, it’s possible to create native, high-performance alternatives to traditional virtualization while maintaining full compatibility with existing Vagrant workflows and provisioning tools.&lt;/p&gt;

&lt;p&gt;For Windows developers seeking reproducible, performant development environments, this WSL2 provider offers a compelling alternative to traditional VM-based solutions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The vagrant-wsl2-provider project is actively being developed and will be open-sourced once the core features are stable. Stay tuned for the repository announcement!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vagrant</category>
      <category>wsl2</category>
      <category>development</category>
      <category>windows</category>
    </item>
  </channel>
</rss>
