summaryrefslogtreecommitdiff
path: root/lib/hyperstack/state.rb
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-25 10:43:43 +0200
committerPaul Buetow <paul@buetow.org>2026-03-25 10:43:43 +0200
commitef53a98c39c26d69b4bfd3a4e925050b220a02c9 (patch)
treed6e747f4a9eea844f498b3f807567d3a5330694e /lib/hyperstack/state.rb
parent917c3d9a777d343b422599f291f242f4bf025ba0 (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.rb57
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