From 4d98678275533cbf2cc6e4f5e3033480918a2e17 Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 13 Dec 2024 16:37:20 -0600 Subject: [PATCH] repo init --- .gitignore | 1 + .vscode/settings.json | 13 ++++++ README.md | 25 +++++++++++ api/client.go | 42 ++++++++++++++++++ api/go.mod | 3 ++ api/instances.go | 52 +++++++++++++++++++++++ api/types.go | 99 +++++++++++++++++++++++++++++++++++++++++++ cmd/quickvast/main.go | 43 +++++++++++++++++++ go.mod | 9 ++++ go.sum | 6 +++ go.work | 6 +++ go.work.sum | 6 +++ 12 files changed, 305 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 api/client.go create mode 100644 api/go.mod create mode 100644 api/instances.go create mode 100644 api/types.go create mode 100644 cmd/quickvast/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 go.work create mode 100644 go.work.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5b92f23 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "cSpell.words": ["quickvast", "vmem"], + "cSpell.ignorePaths": [ + "package-lock.json", + "node_modules", + "vscode-extension", + ".git/{info,lfs,logs,refs,objects}/**", + ".git/{index,*refs,*HEAD}", + ".vscode", + ".vscode-insiders", + "/home/linuxbrew/**" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bc5032 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# quickvast + +CLI tools for managing Vast.ai instances. + +What I want from this tool: + +- [ ] Quickly choose machine types from a refined, customized list of options. + - This would not just be a filter, it would be a customized, weighted scoring system allowing me to quickly choose the best machine for my needs. + - It would provide warnings for machines about why they may not be the best choice, such as low RAM, low driver versions, etc. + - It would have a nice, colorful UI allowing me to quickly see the best options. + - At first this would be a single weighted config, one that could be hard-coded into the tool, but later I could add multiple configs via files perhaps +- [ ] Quickly create a new instance. + - Using the same feature above, quickly create an instance. + - Being able to upload scripts and profiles into the machine easily. + - Being able to choose and configure the 'template' of the machine. +- [ ] Quickly manage instances. + - Being able to quickly retrieve necessary connection information, open the browser for tooling, and copy authentication details. + - Being able to quickly SSH into the machine. +- [ ] Quickly destroy instances. + - Being able to destroy instances quickly. +- [ ] Long term monitoring for pricing + - My concern is that I might leave an instance running for too long and rack up a huge bill. + - I would like each instance to have a time limit for my usage, both a soft and hard limit. + - The soft limit will send me a message on Discord, and the hard limit will destroy the instance after another message. + - diff --git a/api/client.go b/api/client.go new file mode 100644 index 0000000..a4430f8 --- /dev/null +++ b/api/client.go @@ -0,0 +1,42 @@ +package api + +import "net/http" + +// Client handles API communication with the Vast.ai API +type Client struct { + apiKey string + baseURL string + httpClient *http.Client +} + +// Error represents an API error response +type APIError struct { + Success bool `json:"success"` + Error string `json:"error"` + Msg string `json:"msg"` +} + +// NewClient creates a new Vast.ai API client +func NewClient(apiKey string) *Client { + return &Client{ + apiKey: apiKey, + baseURL: "https://console.vast.ai/api/v0", + httpClient: &http.Client{}, + } +} + +// Apply applies the Bearer token to the request +func (c *Client) Apply(req *http.Request) { + req.Header.Set("Authorization", "Bearer "+c.apiKey) +} + +// makeRequest creates and sends an HTTP request with the provided method, path and body +func (c *Client) makeRequest(method, path string, body interface{}) (*http.Response, error) { + req, err := http.NewRequest(method, c.baseURL+path, nil) + if err != nil { + return nil, err + } + + c.Apply(req) + return c.httpClient.Do(req) +} diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..9b754aa --- /dev/null +++ b/api/go.mod @@ -0,0 +1,3 @@ +module xevion.dev/quickvast/api + +go 1.23.3 diff --git a/api/instances.go b/api/instances.go new file mode 100644 index 0000000..5789a47 --- /dev/null +++ b/api/instances.go @@ -0,0 +1,52 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// InstanceGetResponse represents the response from GET /instances +type InstanceGetResponse struct { + Instances []Instance `json:"instances"` +} + +// GetInstances retrieves all instances for the authenticated user +func (c *Client) GetInstances() (*InstanceGetResponse, error) { + resp, err := c.makeRequest(http.MethodGet, "/instances", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result InstanceGetResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// DeleteInstance removes an instance by ID +func (c *Client) DeleteInstance(id int) error { + path := fmt.Sprintf("/instances/%d", id) + resp, err := c.makeRequest(http.MethodDelete, path, nil) + if err != nil { + return err + } + + resp.Body.Close() + return nil +} + +// PutInstance updates an instance's status +func (c *Client) PutInstance(id int, status string) error { + path := fmt.Sprintf("/instances/%d", id) + data := map[string]string{"status": status} + + resp, err := c.makeRequest(http.MethodPut, path, data) + if err != nil { + return err + } + resp.Body.Close() + return nil +} diff --git a/api/types.go b/api/types.go new file mode 100644 index 0000000..b513933 --- /dev/null +++ b/api/types.go @@ -0,0 +1,99 @@ +package api + +type PortMapping struct { + HostIp string `json:"HostIp"` + HostPort string `json:"HostPort"` +} + +type Ports struct { + TCP22 []PortMapping `json:"22/tcp"` + TCP8080 []PortMapping `json:"8080/tcp"` + UDP8080 []PortMapping `json:"8080/udp"` +} + +type Instance struct { + IsBid bool `json:"is_bid"` + InetUpBilled float64 `json:"inet_up_billed"` + InetDownBilled float64 `json:"inet_down_billed"` + External bool `json:"external"` + Webpage *string `json:"webpage"` + Logo string `json:"logo"` + Rentable bool `json:"rentable"` + ComputeCap int `json:"compute_cap"` + DriverVersion string `json:"driver_version"` + CudaMaxGood int `json:"cuda_max_good"` + MachineID int `json:"machine_id"` + HostingType *string `json:"hosting_type"` + PublicIPAddr string `json:"public_ipaddr"` + Geolocation string `json:"geolocation"` + FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"` + DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"` + Reliability2 float64 `json:"reliability2"` + HostRunTime int64 `json:"host_run_time"` + HostID int `json:"host_id"` + ID int `json:"id"` + BundleID int `json:"bundle_id"` + NumGPUs int `json:"num_gpus"` + TotalFlops float64 `json:"total_flops"` + MinBid float64 `json:"min_bid"` + DPHBase float64 `json:"dph_base"` + DPHTotal float64 `json:"dph_total"` + GPUName string `json:"gpu_name"` + GPURam int `json:"gpu_ram"` + GPUDisplayActive bool `json:"gpu_display_active"` + GPUMemBW float64 `json:"gpu_mem_bw"` + BWNVLink int `json:"bw_nvlink"` + DirectPortCount int `json:"direct_port_count"` + GPULanes int `json:"gpu_lanes"` + PCIeBW float64 `json:"pcie_bw"` + PCIGen int `json:"pci_gen"` + DLPerf float64 `json:"dlperf"` + CPUName string `json:"cpu_name"` + MoboName string `json:"mobo_name"` + CPURam int `json:"cpu_ram"` + CPUCores int `json:"cpu_cores"` + CPUCoresEffective float64 `json:"cpu_cores_effective"` + GPUFrac float64 `json:"gpu_frac"` + HasAVX int `json:"has_avx"` + DiskSpace float64 `json:"disk_space"` + DiskName string `json:"disk_name"` + DiskBW float64 `json:"disk_bw"` + InetUp float64 `json:"inet_up"` + InetDown float64 `json:"inet_down"` + StartDate float64 `json:"start_date"` + EndDate int64 `json:"end_date"` + Duration float64 `json:"duration"` + StorageCost float64 `json:"storage_cost"` + InetUpCost float64 `json:"inet_up_cost"` + InetDownCost float64 `json:"inet_down_cost"` + StorageTotalCost float64 `json:"storage_total_cost"` + Verification string `json:"verification"` + Score float64 `json:"score"` + SSHIdx string `json:"ssh_idx"` + SSHHost string `json:"ssh_host"` + SSHPort int `json:"ssh_port"` + ActualStatus string `json:"actual_status"` + IntendedStatus string `json:"intended_status"` + CurState string `json:"cur_state"` + NextState string `json:"next_state"` + ImageUUID string `json:"image_uuid"` + ImageArgs []string `json:"image_args"` + ImageRuntype string `json:"image_runtype"` + ExtraEnv string `json:"extra_env"` + OnStart string `json:"onstart"` + Label *string `json:"label"` + JupyterToken string `json:"jupyter_token"` + StatusMsg string `json:"status_msg"` + GPUUtil float64 `json:"gpu_util"` + DiskUtil float64 `json:"disk_util"` + GPUTemp float64 `json:"gpu_temp"` + LocalIPAddrs string `json:"local_ipaddrs"` + DirectPortEnd int `json:"direct_port_end"` + DirectPortStart int `json:"direct_port_start"` + CPUUtil float64 `json:"cpu_util"` + MemUsage float64 `json:"mem_usage"` + MemLimit float64 `json:"mem_limit"` + VMemUsage float64 `json:"vmem_usage"` + MachineDirSSHPort int `json:"machine_dir_ssh_port"` + Ports Ports `json:"ports"` +} diff --git a/cmd/quickvast/main.go b/cmd/quickvast/main.go new file mode 100644 index 0000000..922d89a --- /dev/null +++ b/cmd/quickvast/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "log" + "os" + + "xevion.dev/quickvast/api" + + "github.com/joho/godotenv" +) + +func main() { + // Load .env file + if err := godotenv.Load(); err != nil { + log.Fatal("Error loading .env file") + } + + // Get API key from environment + apiKey := os.Getenv("VASTAI_API_KEY") + if apiKey == "" { + log.Fatal("VASTAI_API_KEY not found in environment") + } + + // Create client + client := api.NewClient(apiKey) + + // Get instances + resp, err := client.GetInstances() + if err != nil { + log.Fatalf("Error getting instances: %v", err) + } + + if len(resp.Instances) == 0 { + fmt.Println("No instances found") + return + } + + // Print instances + for _, instance := range resp.Instances { + fmt.Printf("Instance %d: %+v\n", instance.ID, instance) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cab0815 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module quickvast + +go 1.23.3 + +require ( + github.com/joho/godotenv v1.5.1 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..46acc19 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= diff --git a/go.work b/go.work new file mode 100644 index 0000000..6e5d20a --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.23.3 + +use ( + . + ./api +) \ No newline at end of file diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..dbed2cb --- /dev/null +++ b/go.work.sum @@ -0,0 +1,6 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=