Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
wubbl0rz committed Mar 24, 2023
0 parents commit fb07743
Show file tree
Hide file tree
Showing 15 changed files with 827 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bin
obj
build
build_*
.idea
Folder.DotSettings.user
17 changes: 17 additions & 0 deletions AppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
public class AppConfig
{
public string AppDir { get; private set; }
public string CacheDir { get; private set; }
public string DataDir { get; private set; }
public string DefaultVmName { get; set; } = "testvm";
public string DefaultVmDistro { get; set; } = "Debian11";
public string DefaultUser { get; set; } = "user";

public AppConfig(string appName = "VmChamp", string sessionName = "default")
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
this.AppDir = Path.Combine(homeDir, appName);
this.CacheDir = Path.Combine(this.AppDir, sessionName, "cache");
this.DataDir = Path.Combine(this.AppDir, sessionName, "vms");
}
}
37 changes: 37 additions & 0 deletions CleanCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.CommandLine;
using Spectre.Console;
using VmChamp;

public class CleanCommand : Command
{
private readonly AppConfig _appConfig;

public CleanCommand(AppConfig appConfig) : base("clean", "delete all vms and images")
{
_appConfig = appConfig;

var allVmDirectories = Directory.GetDirectories(appConfig.DataDir);

this.SetHandler(() =>
{
AnsiConsole.MarkupLine($"[red]Going to delete all VMs ({allVmDirectories.Length}) and IMAGES[/]");

if (AnsiConsole.Ask<string>("Continue? (y/N)", "N").ToLower() != "y")
{
return;
}

using var libvirtConnection = LibvirtConnection.Create("qemu:///session");

foreach (var vmDir in allVmDirectories)
{
var vmName = Path.GetFileName(vmDir);
var vmId = Interop.virDomainLookupByName(libvirtConnection.NativePtr, vmName);

Interop.DestroyVm(vmId, vmName, vmDir);
}

Directory.Delete(appConfig.CacheDir, true);
});
}
}
34 changes: 34 additions & 0 deletions DistroInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace VmChamp;

public class DistroInfo
{
public static IEnumerable<DistroInfo> Distros { get; set; } = new List<DistroInfo>()
{
new()
{
Name = "Debian11",
ImageName = "debian-11-genericcloud-amd64.qcow2",
Url = "https://cloud.debian.org/images/cloud/bullseye/latest/",
Aliases = new[] { "Bullseye" }
},
new()
{
Name = "Debian10",
ImageName = "debian-10-genericcloud-amd64.qcow2",
Url = "https://cloud.debian.org/images/cloud/buster/latest/",
Aliases = new[] { "Buster" }
},
new()
{
Name = "Debian9",
ImageName = "debian-10-genericcloud-amd64.qcow2",
Url = "https://cloud.debian.org/images/cloud/stretch/latest/",
Aliases = new[] { "Stretch" }
}
};

public required string Name { get; set; }
public required string ImageName { get; set; }
public required string Url { get; set; }
public required string[] Aliases { get; set; }
}
49 changes: 49 additions & 0 deletions Downloader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace VmChamp;

using System.Net;
using Spectre.Console;

public class Downloader
{
private readonly DirectoryInfo _cacheDirectory;

public Downloader(DirectoryInfo cacheDirectory)
{
_cacheDirectory = cacheDirectory;
}

public async Task<FileInfo> DownloadAsync(DistroInfo distroInfo, bool force = false)
{
var targetFile = new FileInfo(Path.Combine(_cacheDirectory.FullName, distroInfo.ImageName));

if (targetFile.Exists && !force)
{
AnsiConsole.WriteLine($"Using existing image: {distroInfo.ImageName}");
return targetFile;
}

var uri = new Uri($"{distroInfo.Url}/{distroInfo.ImageName}");

AnsiConsole.WriteLine($"Download: {uri}");

#pragma warning disable SYSLIB0014
var webClient = new WebClient();
#pragma warning restore SYSLIB0014

await AnsiConsole.Progress()
.Columns(new SpinnerColumn(), new PercentageColumn(), new RemainingTimeColumn())
.StartAsync(async ctx =>
{
var task = ctx.AddTask($"progress");

webClient.DownloadProgressChanged += (_, eventArgs) =>
{
task.Value = eventArgs.ProgressPercentage;
};

await webClient.DownloadFileTaskAsync(uri, targetFile.FullName);
});

return targetFile;
}
}
28 changes: 28 additions & 0 deletions Helper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Diagnostics;
using Spectre.Console;

namespace VmChamp;

public class Helper
{
public static Process? ConnectViaSsh(string user, string ip)
{
ProcessStartInfo s = new ProcessStartInfo("ssh");
s.Arguments = $"-o StrictHostKeyChecking=off {user}@{ip}";

return Process.Start(s);
}

public static void DeleteExistingDirectory(string directoryPath)
{
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, true);
}
}

public static string?[] GetAllVmInDirectory(string path) =>
Directory.GetDirectories(path)
.Select(Path.GetFileName)
.ToArray();
}
120 changes: 120 additions & 0 deletions Interop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System.Runtime.InteropServices;
using Spectre.Console;

namespace VmChamp;

[StructLayout(LayoutKind.Sequential)]
public unsafe struct VirDomainIpAddress
{
public int type;
[MarshalAs(UnmanagedType.LPUTF8Str)] public byte* addr;
public uint prefix;
}

[StructLayout(LayoutKind.Sequential)]
public unsafe struct VirDomainInterface
{
public byte* name;
public byte* hwaddr;
public uint naddrs;
public VirDomainIpAddress* addrs;
}

public static unsafe class Interop
{
[DllImport("libvirt.so.0",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "virDomainInterfaceAddresses")]
public static extern nint virDomainInterfaceAddresses(IntPtr dom,
VirDomainInterface*** ifaces,
uint source,
uint flags = 0);

[DllImport("libvirt.so.0",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "virDomainCreateXML")]
public static extern nint virDomainCreateXML(IntPtr conn,
string xmlDesc,
uint flags);

[DllImport("libvirt.so.0",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "virConnectOpen")]
public static extern nint virConnectOpen(string name);

[DllImport("libvirt.so.0",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "virConnectClose")]
public static extern nint virConnectClose(nint conn);

[DllImport("libvirt.so.0",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "virDomainLookupByName")]
public static extern nint virDomainLookupByName(nint conn, string name);

[DllImport("libvirt.so.0",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "virDomainDestroy")]
public static extern int virDomainDestroy(nint domain);

public static string? GetFirstIpById(nint id)
{
VirDomainInterface** ifaces = null;

var n = Interop.virDomainInterfaceAddresses(id, &ifaces, 2);

if (n <= 0 || ifaces == null)
return null;

var virDomainIpAddress = ifaces[0]->addrs[0];
var ip = Marshal.PtrToStringUTF8((nint)virDomainIpAddress.addr);
return ip;
}

public static void DestroyVm(nint vmId, string vmName, string vmDir)
{
AnsiConsole.MarkupLine($"[yellow]💀 Removing VM: {vmName}[/]");

if(Interop.virDomainDestroy(vmId) != 0)
{
AnsiConsole.MarkupLine($"[red]Error removing VM: {vmName}[/]");
}

if (Directory.Exists(vmDir))
{
Directory.Delete(vmDir, true);
}
}
}

public class LibvirtConnection : IDisposable
{
private readonly nint _ptr;
public nint NativePtr => _ptr;
public bool IsValid => this._ptr != nint.Zero;

private LibvirtConnection(nint ptr)
{
_ptr = ptr;
}

public static LibvirtConnection Create(string target)
{
var ptr = Interop.virConnectOpen(target);

if (ptr == nint.Zero)
{
throw new ArgumentException($"Cannot connect to: {target}");
}

return new LibvirtConnection(ptr);
}

public void Dispose()
{
if (this.IsValid)
{
Interop.virConnectClose(_ptr);
}
}
}
73 changes: 73 additions & 0 deletions IsoImager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Text;
using DiscUtils.Iso9660;

public class IsoImager
{
private IEnumerable<string> FindSshKeys()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var keys = new List<string>();

foreach (var file in Directory.EnumerateFiles(Path.Combine(homeDir, ".ssh")))
{
if (file.EndsWith(".pub"))
{
keys.Add(File.ReadAllText(file));
}
}

return keys;
}

public FileInfo CreateImage(string hostname, DirectoryInfo outputDirectory)
{
var keys = "[" + string.Join(",", this.FindSshKeys().Select(key => $"\"{key.Trim()}\"")) + "]";

var userData = $"""
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version: 1.0
--==BOUNDARY==
Content-Type: text/cloud-config; charset="us-ascii"
#cloud-config
preserve_hostname: False
hostname: {hostname}
users:
- default
- name: user
groups: ['sudo']
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh-authorized-keys:
{keys}
output:
all: ">> /var/log/cloud-init.log"
ssh_genkeytypes: ['ed25519', 'rsa']
ssh_authorized_keys:
{keys}
runcmd:
- systemctl stop networking && systemctl start networking
- systemctl disable cloud-init.service
--==BOUNDARY==--
""";

var metaData = $"""
instance-id: {hostname}
local-hostname: {hostname}
""";

var builder = new CDBuilder();
builder.UseJoliet = true;
builder.VolumeIdentifier = "cidata";
builder.AddFile(@"user-data", Encoding.UTF8.GetBytes(userData));
builder.AddFile(@"meta-data", Encoding.UTF8.GetBytes(metaData));

var outputFile = Path.Combine(outputDirectory.FullName, "cloudInit.iso");

builder.Build(outputFile);

return new FileInfo(outputFile);
}
}
Loading

0 comments on commit fb07743

Please sign in to comment.