diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-25 10:43:43 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-25 10:43:43 +0200 |
| commit | ef53a98c39c26d69b4bfd3a4e925050b220a02c9 (patch) | |
| tree | d6e747f4a9eea844f498b3f807567d3a5330694e /lib/hyperstack/state.rb | |
| parent | 917c3d9a777d343b422599f291f242f4bf025ba0 (diff) | |
hyperstack: split 3335-line monolith into lib/hyperstack/ modules
Extracts all classes from hyperstack.rb into focused library files:
- lib/hyperstack/config.rb — ConfigLoader + Config (TOML loading, validation)
- lib/hyperstack/state.rb — StateStore + PrefixedOutput (JSON state, threaded output)
- lib/hyperstack/client.rb — HyperstackClient (REST API + retry logic)
- lib/hyperstack/wireguard.rb — LocalWireGuard (wg1.conf peer management, /etc/hosts)
- lib/hyperstack/provisioning.rb — ProvisioningScripts + RemoteProvisioner (SSH bootstrap)
- lib/hyperstack/manager.rb — Manager (VM lifecycle orchestration)
- lib/hyperstack/watcher.rb — VllmWatcher (Prometheus + GPU dashboard)
- lib/hyperstack/cli.rb — CLI (OptionParser command dispatch)
hyperstack.rb becomes a 46-line entry point with require_relative calls.
All files pass `ruby -c` syntax check and `--help` runs correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'lib/hyperstack/state.rb')
| -rw-r--r-- | lib/hyperstack/state.rb | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/lib/hyperstack/state.rb b/lib/hyperstack/state.rb new file mode 100644 index 0000000..0ce2687 --- /dev/null +++ b/lib/hyperstack/state.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'json' + +module HyperstackVM + # Persists VM state to a JSON file with atomic writes (write-to-tmp + rename). + # Used to track provisioned VM ID, IP, WireGuard keys, and related metadata. + class StateStore + def initialize(path) + @path = path + end + + attr_reader :path + + def load + return nil unless File.exist?(@path) + + JSON.parse(File.read(@path)) + rescue JSON::ParserError => e + raise Error, "Failed to parse state file #{@path}: #{e.message}" + end + + def save(payload) + temp_path = "#{@path}.tmp" + File.write(temp_path, JSON.pretty_generate(payload)) + File.rename(temp_path, @path) + end + + def delete + File.delete(@path) if File.exist?(@path) + end + end + + # Thread-safe output wrapper that prepends a fixed prefix to each line. + # Used by create-both so interleaved output from VM1 and VM2 threads is distinguishable. + # #print buffers partial lines until a newline is received, then flushes with the prefix. + class PrefixedOutput + def initialize(prefix, delegate, mutex) + @prefix = prefix + @delegate = delegate + @mutex = mutex + @buffer = +'' + end + + def puts(msg = '') + @mutex.synchronize { @delegate.puts("#{@prefix}#{msg}") } + end + + def print(msg) + @buffer << msg.to_s + while (idx = @buffer.index("\n")) + line = @buffer.slice!(0, idx + 1) + @mutex.synchronize { @delegate.print("#{@prefix}#{line}") } + end + end + end +end |
