Hetzner.ps1
function New-Hetzner { param( [Parameter(Mandatory = $true)] [String] $name, [String] $type = "cx11" # cx32 ) if (-Not(Get-Command "hcloud.exe" -ErrorAction SilentlyContinue)) { Install-Scoop scoop install hcloud hcloud completion powershell | Out-String | Invoke-Expression } #### Activate context $active = hcloud context active if (-Not($active)) { Write-Host "Active context not found" Write-Host "Visit the Hetzner Cloud Console at https://console.hetzner.cloud/, select your project, and create a new API token" $project = Read-Host -Prompt "Project" Write-Host "Copy and paste your token (it will not be showed) and press enter" hcloud context create $project } ##### Create server $servers = hcloud.exe server list -o json | ConvertFrom-Json $server = $servers | Where-Object { $_.name -eq $name } if (-Not($server)) { Write-Host "Creating server ${name}" $key_path = Get-SshKey -Public hcloud ssh-key create --name dev --public-key-from-file $key_path hcloud server create --name $name --image ubuntu-24.04 --type $type --ssh-key dev # -o json } ##### Config server $ip = hcloud server ip $name $command = "adduser box; usermod -aG sudo box; mkdir -p /home/box/.ssh; cp .ssh/authorized_keys /home/box/.ssh/authorized_keys; chown -R box:box /home/box/.ssh;" # TODO wait server responsive; pwsh ask password or ... ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR root@$ip $command | Write-Host ##### SSH # https://dev.to/kaiwalter/manage-ssh-config-entries-with-a-powershell-module-77b $sshKey = "${env:USERPROFILE}\.ssh\id_ed25519" $configFile = "${env:USERPROFILE}\.ssh\config" function Get-ConfigKeyWords { return @("Match", "AddressFamily", "BatchMode", "BindAddress", "ChallengeResponseAuthentication", "CheckHostIP", "Cipher", "Ciphers", "ClearAllForwardings", "Compression", "CompressionLevel", "ConnectionAttempts", "ConnectTimeout", "ControlMaster", "ControlPath", "DynamicForward", "EscapeChar", "ExitOnForwardFailure", "ForwardAgent", "ForwardX11", "ForwardX11Trusted", "GatewayPorts", "GlobalKnownHostsFile", "GSSAPIAuthentication", "GSSAPIKeyExchange", "GSSAPIClientIdentity", "GSSAPIDelegateCredentials", "GSSAPIRenewalForcesRekey", "GSSAPITrustDns", "HashKnownHosts", "HostbasedAuthentication", "HostKeyAlgorithms", "HostKeyAlias", "HostName", "IdentitiesOnly", "IdentityFile", "KbdInteractiveAuthentication", "KbdInteractiveDevices", "LocalCommand", "LocalForward", "LogLevel", "MACs", "NoHostAuthenticationForLocalhost", "PreferredAuthentications", "Protocol", "ProxyCommand", "PubkeyAuthentication", "RemoteForward", "RhostsRSAAuthentication", "RSAAuthentication", "SendEnv", "ServerAliveCountMax", "ServerAliveInterval", "SmartcardDevice", "StrictHostKeyChecking", "TCPKeepAlive", "Tunnel", "TunnelDevice", "UsePrivilegedPort", "User", "UserKnownHostsFile", "VerifyHostKeyDNS", "VisualHostKey") } function Get-ConfigHostList { $hostList = @{} if (-Not(Test-Path -Path $configFile)) { return $hostList } $contents = Get-Content $configFile -Raw # determine line break LF or CR/LF if ($contents -match "^[^\n]+\r\n") { $splitter = "\r\n" } else { $splitter = "\n" } # split by "Host" - when at start of file or has prededing line breaks / whitespaces $splitEntries = "(?i)(^|" + $splitter + "+\s+)host\s" $list = [regex]::Split($contents, $splitEntries) if ($list.Count -le 1) { throw "splitting file $configFilename failed or no content" } # READ lists of hosts foreach ($entry in $list) { # $output += $entry -replace $($splitter+"\s+"), $($joiner+" ") $attributes = [regex]::Split($entry, $splitter) | % { $_.Trim() } $HostName = $null $HostValues = @{} foreach ($attribute in $attributes) { if ($attribute -ne "") { if ($HostName) { # split key/value and normalize key name $kv = [regex]::Split($attribute, "\s+", 1) $keyName = $kv[0] $keyValue = $kv[1] foreach ($keyword in ($keywords | ? { $_ -eq $keyName })) { $keyName = $keyword break } $HostValues[$keyName] = $keyValue } else { # assume first entry to be the host $HostName = $attribute.ToLower() } } } if ($HostName) { if ($hostList.ContainsKey($HostName)) { throw "duplicate Host $HostName" } else { $hostList[$HostName] = $HostValues } } } return $hostList } function Add-ConfigHostToList { param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [hashtable] $hostList, [Parameter(Mandatory = $true)] [string] $HostName, [Parameter(Mandatory = $true)] [hashtable] $HostValues, [switch] $IgnoreExisting ) if (!$IgnoreExisting) { if ($hostList.ContainsKey($HostName.ToLower())) { throw "HostName $HostName already exists" } } $keywords = Get-ConfigKeyWords $hostValuesCleaned = @{} foreach ($kv in $HostValues.GetEnumerator()) { $keyName = $null foreach ($keyword in ($keywords | ? { $_ -eq $kv.Key })) { $keyName = $keyword break } if ($keyName) { $hostValuesCleaned[$keyName] = $kv.Value.Trim() } else { throw "key $($kv.Key) not found in list of keywords" } } if ($HostValuesCleaned) { $hostList[$HostName.ToLower()] = $HostValuesCleaned } return $hostList } function Set-ConfigContents { param ( [Parameter(Mandatory = $true)] [string] $Contents ) $configFilename = $configFile if ($Contents) { $Contents | Set-Content $configFilename } } function Set-ConfigHostList { param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [hashtable] $hostList ) $joiner = "`n" $output = @() foreach ($hostEntry in $hostList.GetEnumerator()) { $hostOutput = "Host " + $hostEntry.Key + $joiner foreach ($kv in $hostEntry.Value.GetEnumerator()) { $hostOutput = $hostOutput + " " + $kv.key + " " + $kv.value + $joiner } $output += $hostOutput } if ($output) { $content = $($output -join $($joiner)) $content | Set-Content $configFile } } ##### VS Code Install-CodeExtension "ms-vscode-remote.remote-ssh" Install-CodeExtension "ms-vscode-remote.remote-wsl" $hostList = Get-ConfigHostList if (-Not($hostList.ContainsKey($name))) { $hostList = Add-ConfigHostToList -HostList $hostList -HostName $name -HostValues @{ identityfile = $sshKey hostname = $ip user = "box" StrictHostKeyChecking = "no" UserKnownHostsFile = "/dev/null" } Set-ConfigHostList $hostList } else { # TODO check IP } # https://code.visualstudio.com/docs/remote/troubleshooting#_connect-to-a-remote-host-from-the-terminal code --folder-uri "vscode-remote://ssh-remote+${name}/home/box" } function Install-HCloud { if (-Not(Get-Command "hcloud.exe" -ErrorAction SilentlyContinue)) { scoop install hcloud hcloud completion powershell | Out-String | Invoke-Expression } } function Remove-Hetzner { param( [Parameter(Position = 0, mandatory = $true)] [string] $name ) Install-HCloud hcloud server delete $name } |