Skip to content

Commit 3eb2b0a

Browse files
committed
Expose full Vast instance metadata
1 parent 9eddbb5 commit 3eb2b0a

6 files changed

Lines changed: 431 additions & 20 deletions

File tree

‎README.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,26 @@ var instances = await client.GetInstancesAsync();
8484
var active = instances.Where(instance => instance.IsActive).ToList();
8585
```
8686

87-
`VastInstance` includes normalized status, label, GPU name, hourly price, estimated age, estimated cost, and SSH target fields when Vast returns them.
87+
`VastInstance` includes normalized status, label, GPU name, hourly price, estimated age, estimated cost, SSH target fields, public IP, exposed port mappings, and `extra_env` values when Vast returns them.
88+
89+
Use `extra_env` and exposed ports when reconnecting to software that was started by a Vast `onstart` command:
90+
91+
```csharp
92+
if (instance.TryGetExtraEnvironmentValue("VAST_LFS_TOKEN", out var workerToken) &&
93+
instance.TryGetPublicUriForContainerPort(8088, out var workerUri))
94+
{
95+
Console.WriteLine(workerUri);
96+
}
97+
```
98+
99+
The full raw Vast instance object is also available for newly introduced Vast fields that do not yet have first-class properties:
100+
101+
```csharp
102+
if (instance.Raw.TryGetProperty("future_vast_field", out var value))
103+
{
104+
Console.WriteLine(value?.ToJsonString());
105+
}
106+
```
88107

89108
### GetInstanceAsync
90109

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,82 @@
11
namespace VastAI.NET.Models;
22

3-
/// <summary>Normalized Vast instance state used for safety checks, status output, and SSH target discovery.</summary>
4-
public sealed record VastInstance(
5-
string Id,
6-
string Status,
7-
string Label,
8-
string GpuName,
9-
double Price,
10-
double AgeHours,
11-
string SshHost = "",
12-
int SshPort = 0,
13-
string SshUser = "root")
3+
using System.Collections.ObjectModel;
4+
using System.Text.Json.Nodes;
5+
6+
/// <summary>Normalized Vast instance state used for safety checks, status output, SSH discovery, and browser worker recovery.</summary>
7+
public sealed record VastInstance
148
{
9+
/// <summary>Creates a normalized Vast instance model from the fields most callers need directly.</summary>
10+
public VastInstance(
11+
string id,
12+
string status,
13+
string label,
14+
string gpuName,
15+
double price,
16+
double ageHours,
17+
string sshHost = "",
18+
int sshPort = 0,
19+
string sshUser = "root",
20+
string publicIpAddress = "",
21+
IReadOnlyDictionary<string, string>? extraEnvironment = null,
22+
IReadOnlyList<VastPortMapping>? ports = null,
23+
VastRawJson? raw = null)
24+
{
25+
Id = id;
26+
Status = status;
27+
Label = label;
28+
GpuName = gpuName;
29+
Price = price;
30+
AgeHours = ageHours;
31+
SshHost = sshHost;
32+
SshPort = sshPort;
33+
SshUser = sshUser;
34+
PublicIpAddress = publicIpAddress;
35+
ExtraEnvironment = new ReadOnlyDictionary<string, string>(
36+
new Dictionary<string, string>(extraEnvironment ?? new Dictionary<string, string>(), StringComparer.Ordinal));
37+
Ports = ports?.ToArray() ?? [];
38+
Raw = raw ?? VastRawJson.Empty;
39+
}
40+
41+
/// <summary>Vast instance or contract id.</summary>
42+
public string Id { get; init; }
43+
44+
/// <summary>Vast-reported instance state, such as running, loading, stopped, or destroyed.</summary>
45+
public string Status { get; init; }
46+
47+
/// <summary>User-supplied Vast label assigned when the instance was created.</summary>
48+
public string Label { get; init; }
49+
50+
/// <summary>Vast-reported GPU name.</summary>
51+
public string GpuName { get; init; }
52+
53+
/// <summary>Hourly price in USD reported by Vast.</summary>
54+
public double Price { get; init; }
55+
56+
/// <summary>Estimated rental age in hours.</summary>
57+
public double AgeHours { get; init; }
58+
59+
/// <summary>SSH host reported by Vast when direct SSH metadata is available.</summary>
60+
public string SshHost { get; init; }
61+
62+
/// <summary>SSH port reported by Vast when direct SSH metadata is available.</summary>
63+
public int SshPort { get; init; }
64+
65+
/// <summary>SSH user reported by Vast, defaulting to root for Vast containers.</summary>
66+
public string SshUser { get; init; }
67+
68+
/// <summary>Public IPv4 address reported by Vast for direct networking and exposed application ports.</summary>
69+
public string PublicIpAddress { get; init; }
70+
71+
/// <summary>Environment values returned by Vast as extra_env for the rented instance.</summary>
72+
public IReadOnlyDictionary<string, string> ExtraEnvironment { get; init; }
73+
74+
/// <summary>Application and service port mappings returned by Vast for the rented instance.</summary>
75+
public IReadOnlyList<VastPortMapping> Ports { get; init; }
76+
77+
/// <summary>Full raw Vast instance object so callers can inspect new Vast fields before the library adds first-class properties.</summary>
78+
public VastRawJson Raw { get; init; }
79+
1580
/// <summary>Whether this instance should block a new paid rental unless the user explicitly overrides the gate.</summary>
1681
public bool IsActive => Status.Equals("running", StringComparison.OrdinalIgnoreCase) ||
1782
Status.Equals("loading", StringComparison.OrdinalIgnoreCase) ||
@@ -22,4 +87,66 @@ public sealed record VastInstance(
2287

2388
/// <summary>Whether Vast has reported enough SSH metadata to connect directly.</summary>
2489
public bool HasSshTarget => !string.IsNullOrWhiteSpace(SshHost) && SshPort > 0;
90+
91+
/// <summary>Reads one environment value returned by Vast in extra_env.</summary>
92+
public bool TryGetExtraEnvironmentValue(string name, out string value) =>
93+
ExtraEnvironment.TryGetValue(name, out value!);
94+
95+
/// <summary>Resolves an exposed HTTP URI for a container port using Vast's public IP and port mapping data.</summary>
96+
public bool TryGetPublicUriForContainerPort(int containerPort, out Uri uri)
97+
{
98+
var mapping = Ports.FirstOrDefault(port => port.ContainerPort == containerPort);
99+
if (mapping is null || mapping.HostPort <= 0)
100+
{
101+
uri = null!;
102+
return false;
103+
}
104+
105+
var host = string.IsNullOrWhiteSpace(mapping.Host) || mapping.Host is "0.0.0.0" or "::"
106+
? PublicIpAddress
107+
: mapping.Host;
108+
if (string.IsNullOrWhiteSpace(host))
109+
{
110+
uri = null!;
111+
return false;
112+
}
113+
114+
uri = new UriBuilder("http", host, mapping.HostPort).Uri;
115+
return true;
116+
}
117+
}
118+
119+
/// <summary>One Vast port mapping from a container port to a public host port.</summary>
120+
/// <param name="ContainerPort">Port inside the Vast container.</param>
121+
/// <param name="Protocol">Port protocol, normally tcp.</param>
122+
/// <param name="Host">Public or wildcard host reported by Vast.</param>
123+
/// <param name="HostPort">Host-side port exposed by Vast.</param>
124+
public sealed record VastPortMapping(int ContainerPort, string Protocol, string Host, int HostPort);
125+
126+
/// <summary>Full raw Vast JSON object for callers that need fields not yet normalized by this library.</summary>
127+
public sealed class VastRawJson
128+
{
129+
/// <summary>Empty raw object used when no Vast JSON object was available.</summary>
130+
public static VastRawJson Empty { get; } = new(new JsonObject());
131+
132+
/// <summary>Creates a raw JSON wrapper around a cloned Vast object.</summary>
133+
public VastRawJson(JsonObject value)
134+
{
135+
Object = value;
136+
}
137+
138+
/// <summary>Raw Vast object, including normalized and not-yet-modeled fields.</summary>
139+
public JsonObject Object { get; }
140+
141+
/// <summary>Reads a top-level property from the raw Vast object.</summary>
142+
public bool TryGetProperty(string name, out JsonNode? value) =>
143+
Object.TryGetPropertyValue(name, out value);
144+
145+
/// <summary>Returns the raw Vast object as compact JSON.</summary>
146+
public override string ToString() =>
147+
Object.ToJsonString();
148+
149+
/// <summary>Clones one Vast JSON object into a public raw wrapper.</summary>
150+
internal static VastRawJson FromJson(string json) =>
151+
new(JsonNode.Parse(json)?.AsObject() ?? new JsonObject());
25152
}

‎src/VastAI.NET/Serialization/VastInstanceDto.cs‎

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
namespace VastAI.NET.Serialization;
22

3+
using System.Globalization;
4+
using System.Text.Json;
35
using System.Text.Json.Serialization;
46
using Models;
57
using static VastResponseHelpers;
@@ -38,9 +40,11 @@ internal sealed class VastInstanceDto
3840
public double DirectSshPort { get; set; }
3941
public string SshUser { get; set; } = "";
4042
public string User { get; set; } = "";
43+
public JsonElement ExtraEnv { get; set; }
44+
public JsonElement Ports { get; set; }
4145

4246
/// <summary>Maps raw Vast instance fields to the normalized model used by safety and SSH discovery.</summary>
43-
public VastInstance ToModel()
47+
public VastInstance ToModel(VastRawJson? raw = null)
4448
{
4549
var ageHours = FirstPositive(AgeHours, AgeHrs, DurationHours);
4650
var start = FirstPositive(StartDate, CreatedAt, CreationDate, RentedAt);
@@ -56,6 +60,167 @@ public VastInstance ToModel()
5660
ageHours,
5761
FirstNonEmpty(SshHost, DirectSshHost, PublicIpaddr),
5862
(int)FirstPositive(SshPort, DirectSshPort),
59-
FirstNonEmpty(SshUser, User, "root"));
63+
FirstNonEmpty(SshUser, User, "root"),
64+
PublicIpaddr,
65+
ReadExtraEnvironment(ExtraEnv),
66+
ReadPorts(Ports),
67+
raw);
68+
}
69+
70+
/// <summary>Normalizes Vast extra_env when it arrives as arrays or objects.</summary>
71+
private static IReadOnlyDictionary<string, string> ReadExtraEnvironment(JsonElement extraEnv)
72+
{
73+
var values = new Dictionary<string, string>(StringComparer.Ordinal);
74+
if (extraEnv.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
75+
return values;
76+
77+
if (extraEnv.ValueKind == JsonValueKind.Object)
78+
{
79+
foreach (var property in extraEnv.EnumerateObject())
80+
values[property.Name] = property.Value.ToString();
81+
return values;
82+
}
83+
84+
if (extraEnv.ValueKind != JsonValueKind.Array)
85+
return values;
86+
87+
foreach (var item in extraEnv.EnumerateArray())
88+
if (TryReadExtraEnvironmentItem(item, out var name, out var value))
89+
values[name] = value;
90+
91+
return values;
92+
}
93+
94+
/// <summary>Reads one Vast extra_env item from the documented array shape or an object alias shape.</summary>
95+
private static bool TryReadExtraEnvironmentItem(JsonElement item, out string name, out string value)
96+
{
97+
name = "";
98+
value = "";
99+
if (item.ValueKind == JsonValueKind.Array && item.GetArrayLength() >= 2)
100+
{
101+
name = item[0].ToString();
102+
value = item[1].ToString();
103+
return !string.IsNullOrWhiteSpace(name);
104+
}
105+
106+
if (item.ValueKind != JsonValueKind.Object)
107+
return false;
108+
109+
name = ReadStringProperty(item, "name", "key");
110+
value = ReadStringProperty(item, "value");
111+
return !string.IsNullOrWhiteSpace(name);
112+
}
113+
114+
/// <summary>Normalizes Vast ports when they arrive as Docker-style objects or arrays of mapping objects.</summary>
115+
private static IReadOnlyList<VastPortMapping> ReadPorts(JsonElement ports)
116+
{
117+
if (ports.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
118+
return [];
119+
120+
if (ports.ValueKind == JsonValueKind.Array)
121+
return ports.EnumerateArray().Select(ReadArrayPortMapping).Where(mapping => mapping is not null).Cast<VastPortMapping>().ToList();
122+
123+
if (ports.ValueKind != JsonValueKind.Object)
124+
return [];
125+
126+
var mappings = new List<VastPortMapping>();
127+
foreach (var property in ports.EnumerateObject())
128+
{
129+
var (containerPort, protocol) = ParsePortKey(property.Name);
130+
if (containerPort <= 0)
131+
continue;
132+
133+
if (property.Value.ValueKind == JsonValueKind.Array)
134+
mappings.AddRange(property.Value.EnumerateArray()
135+
.Select(item => ReadDockerPortMapping(containerPort, protocol, item))
136+
.Where(mapping => mapping is not null)
137+
.Cast<VastPortMapping>());
138+
else
139+
{
140+
var mapping = ReadDockerPortMapping(containerPort, protocol, property.Value);
141+
if (mapping is not null)
142+
mappings.Add(mapping);
143+
}
144+
}
145+
146+
return mappings;
147+
}
148+
149+
/// <summary>Reads one array-style Vast port mapping.</summary>
150+
private static VastPortMapping? ReadArrayPortMapping(JsonElement item)
151+
{
152+
if (item.ValueKind != JsonValueKind.Object)
153+
return null;
154+
155+
var containerPort = ReadIntProperty(item, "container_port", "containerPort", "port", "private_port");
156+
var hostPort = ReadIntProperty(item, "host_port", "hostPort", "public_port");
157+
if (containerPort <= 0 || hostPort <= 0)
158+
return null;
159+
160+
return new VastPortMapping(
161+
containerPort,
162+
FirstNonEmpty(ReadStringProperty(item, "protocol", "proto"), "tcp"),
163+
ReadStringProperty(item, "host", "host_ip", "HostIp", "ip"),
164+
hostPort);
165+
}
166+
167+
/// <summary>Reads one Docker-style Vast port mapping value.</summary>
168+
private static VastPortMapping? ReadDockerPortMapping(int containerPort, string protocol, JsonElement item)
169+
{
170+
if (item.ValueKind is JsonValueKind.Number or JsonValueKind.String)
171+
{
172+
var scalarPort = ReadInt(item);
173+
return scalarPort <= 0 ? null : new VastPortMapping(containerPort, protocol, "", scalarPort);
174+
}
175+
176+
if (item.ValueKind != JsonValueKind.Object)
177+
return null;
178+
179+
var hostPort = ReadIntProperty(item, "HostPort", "host_port", "hostPort", "public_port");
180+
if (hostPort <= 0)
181+
return null;
182+
183+
return new VastPortMapping(
184+
containerPort,
185+
protocol,
186+
ReadStringProperty(item, "HostIp", "host_ip", "host", "ip"),
187+
hostPort);
188+
}
189+
190+
/// <summary>Reads the container port and protocol from a Docker-style key like 8088/tcp.</summary>
191+
private static (int ContainerPort, string Protocol) ParsePortKey(string key)
192+
{
193+
var parts = key.Split('/', 2, StringSplitOptions.TrimEntries);
194+
return (int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var port) ? port : 0,
195+
parts.Length == 2 && !string.IsNullOrWhiteSpace(parts[1]) ? parts[1] : "tcp");
196+
}
197+
198+
/// <summary>Reads the first matching string property from a JSON object.</summary>
199+
private static string ReadStringProperty(JsonElement item, params string[] names)
200+
{
201+
foreach (var name in names)
202+
if (item.TryGetProperty(name, out var property))
203+
return property.ToString();
204+
205+
return "";
206+
}
207+
208+
/// <summary>Reads the first matching integer property from a JSON object.</summary>
209+
private static int ReadIntProperty(JsonElement item, params string[] names)
210+
{
211+
foreach (var name in names)
212+
if (item.TryGetProperty(name, out var property))
213+
return ReadInt(property);
214+
215+
return 0;
216+
}
217+
218+
/// <summary>Reads a JSON number or numeric string as an integer.</summary>
219+
private static int ReadInt(JsonElement value)
220+
{
221+
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number))
222+
return number;
223+
224+
return int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : 0;
60225
}
61226
}

‎src/VastAI.NET/VastAI.NET.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<!-- NuGet Package Properties (common properties inherited from Directory.Build.props) -->
77
<PackageId>VastAI.NET</PackageId>
8-
<Version>1.0.1</Version>
8+
<Version>1.0.2</Version>
99
<Description>Typed .NET client for Vast.ai REST provider operations.</Description>
1010
<PackageTags>alos;vastai;vast;cloud-gpu;rest-client</PackageTags>
1111
</PropertyGroup>

0 commit comments

Comments
 (0)