lazystack is a keyboard-driven terminal UI for OpenStack, built with Go. It follows the "lazy" convention (lazygit, lazydocker) to signal a fast, keyboard-first alternative to Horizon and the verbose OpenStack CLI. The goal is to make day-to-day OpenStack operations — especially VM lifecycle management — fast and intuitive from the terminal.
License: Apache 2.0 Language: Go Target cloud: Any OpenStack cloud with Keystone v3, Nova v2.1+ (tested against cloud.rlnc.eu with microversion 2.100)
| Phase | Status | Notes |
|---|---|---|
| Phase 1: MVP | ✓ Complete | Cloud connection, server CRUD, modals, help |
| Phase 2: Extended Compute | ✓ Complete | All actions, console log, resize, bulk ops, action history |
| Phase 3: Additional Resources | ✓ Complete | Tabbed navigation, volumes, floating IPs, security groups, key pairs, networks (CRUD), routers (CRUD) |
| Phase 4: Refactor, Octavia, Projects, Quotas | ✓ Complete | App refactor, dynamic tabs, Octavia LB tab, project switching, quota overlay |
| Phase 5: Quality of Life | Mostly Complete | Server rename, rebuild, snapshot, rescue/unrescue, image management (combined view with upload/download/edit), cloud-init/user data, port CRUD, subnet edit, search/filter on all views, confirmation dialogs, SSH integration, console access (noVNC), server cloning, cross-resource navigation, copy-to-clipboard picker — done. Config file, DNS — not started |
| Phase 6: Operational | Not started | Admin views, hypervisor view, service catalog browser |
-
Value receiver pattern: Bubble Tea v2 uses value receivers for
Update(), which means model mutations require returning new values. This interacts poorly with optimistic UI updates — changes made before an async command fires can be overwritten when the command's response arrives and gets routed throughupdateActiveView. This caused the resize confirmation banner to flicker (optimistic status set to ACTIVE, then stale API response overwrote it back to VERIFY_RESIZE). Solved with apendingActionstate that suppresses stale updates until the real state catches up. -
Message routing complexity: The root model routes messages to sub-views via a
switchon the active view. Adding overlay modals (resize picker, confirm, error, help) that intercept messages creates ordering dependencies in theUpdatemethod. The resize modal beingActivewas swallowing messages meant for other views. Each new modal/overlay adds routing complexity — consider a message bus or middleware pattern if this grows further. Phase 4's refactor split app.go from 1,643 lines into 7 focused files (app.go, actions_server.go, actions_resource.go, routing.go, render.go, connect.go, tabs.go), which makes this manageable for now. -
Import cycle avoidance: Shared types (keys, styles, messages) live in
internal/shared/rather thaninternal/app/to avoid import cycles betweenappand the UI packages. This is a pragmatic workaround but means the "app" package is really just the root model + view routing.
-
Ctrl-prefixed dangerous actions: Originally used
c/d/rfor create/delete/reboot. Changed toctrl+n/ctrl+d/ctrl+oafter realizing typing in the wrong terminal window could trigger destructive operations. This is a good pattern for any keyboard-first TUI. -
Optimistic UI is essential for async APIs: OpenStack actions like
confirmResizereturn 202 (accepted) but the server state doesn't change immediately. Polling the API right after the action often returns stale state. ThependingActionpattern — show the expected state immediately and suppress stale API responses until the real state catches up — provides much better UX than waiting for the tick. -
Column adaptivity matters: Fixed-width columns don't work across terminal sizes. The flex-weight + priority system (columns get proportional extra space, and low-priority columns hide on narrow terminals) works well. IPv6 addresses (39 chars) are particularly challenging — they need the highest flex weight but lowest display priority since they're rarely needed at a glance.
-
Modal vs view: The resize flavor picker was initially a full view, which caused navigation issues (Esc from resize opened from the list panicked because there was no detail view to go back to). Converting it to a modal overlay that sits on top of the current view eliminated the problem entirely. Prefer modals for transient selection UI.
-
Auto-refresh must survive view changes: The server list's auto-refresh tick was breaking when navigating to other views because the tick message got routed to the wrong view. Fixed with
updateAllViewsthat routes non-key messages to all initialized tab views. Modal overlays (resize, FIP picker) must not swallow background ticks — route to all views first, then to the active modal. -
Tick chain fragility: Each view's auto-refresh is a chain: tick fires → fetch + schedule next tick. If any message in the chain gets swallowed (e.g., by a modal that doesn't handle it), the chain breaks permanently. The fix is to always route messages to background views before routing to modal overlays.
-
Bulk action support is partial: Space-select works for delete, reboot, pause, suspend, shelve, and resize. Console log only operates on a single server (the cursor, not the selection), which is intentional since console log is inherently single-server. Bulk resize applies the same target flavor to all selected servers.
-
Image name resolution: Nova's server response doesn't include the image name (only ID) with newer microversions. The server list fetches all images from Glance to resolve names, which works but adds an extra API call on every refresh. Should cache more aggressively or resolve lazily.
-
Server detail refresh creates a new model: After actions from the detail view, the detail model is recreated with
New()+Init()to force a fresh fetch. This resets scroll position and loses the pending action state if not carefully managed. A properRefresh()method on the detail model would be cleaner. -
Limited tests: The codebase has minimal test coverage (cloud, compute, serverlist columns). The compute layer functions are thin wrappers around gophercloud and would benefit from interface-based mocking. The UI components are harder to test but snapshot testing of
View()output would catch rendering regressions. -
Error handling in bulk operations: Bulk actions collect errors and report them as a single concatenated string. Individual failure tracking and partial success reporting would be better UX.
-
Microversion dependency: The app sets microversion
2.100on the compute client. This means it requires a relatively recent Nova deployment. Theoriginal_namefield in the flavor response (used for display) requires microversion 2.47+. Should gracefully degrade for older clouds. -
Image map structure varies by microversion: The
imagefield in the server response ismap[string]anyand its contents depend on the microversion. With 2.100, it typically only containsidandlinks— noname. Boot-from-volume servers have an empty image map. -
Unshelve requires non-nil opts: gophercloud's
Unshelve()panics if passednilopts (it callsopts.ToUnshelveMap()on the nil interface). Must passservers.UnshelveOpts{}. This is arguably a gophercloud bug. -
Locked server awareness: The server list shows a 🔒 icon for locked servers, but doesn't prevent actions on them — the API will reject the action and the error modal will display. Could pre-check lock status and show a more helpful message.
OpenStack operators and developers lack a fast, keyboard-driven terminal interface:
- Horizon is slow, requires a browser, and is painful for repetitive tasks
- The OpenStack CLI is verbose — simple operations require long commands with many flags
- No Go-based TUI alternative exists for OpenStack management
lazystack fills this gap by providing a single binary that connects to any OpenStack cloud via clouds.yaml and presents a navigable, auto-refreshing interface for the most common operations.
- Keyboard-first: Every action is reachable via keyboard shortcuts. Mouse support is not a goal.
- Fast startup: Connect and show servers in under 2 seconds on a healthy cloud.
- Non-destructive by default: Destructive actions require Ctrl-prefixed shortcuts and confirmation modals.
- Minimal configuration: Reads standard
clouds.yaml— no additional config files needed. - Single binary: No runtime dependencies beyond the compiled Go binary.
- Safe by default: Can't accidentally trigger destructive actions by typing in the wrong window.
- OpenStack operators managing VMs across one or more clouds
- Developers who provision and tear down test instances frequently
- Anyone who prefers terminal workflows over web UIs
- TUI framework: Bubble Tea v2
- UI components: Bubbles v2 (spinner, text input)
- Styling: Lip Gloss v2
- OpenStack SDK: gophercloud v2
- YAML parsing:
gopkg.in/yaml.v3(for clouds.yaml cloud name extraction)
src/
cmd/lazystack/main.go # Entry point, CLI flags, restart via syscall.Exec
internal/
app/
app.go # Root model, New(), Init(), Update() routing, type defs
actions_server.go # Server CRUD/lifecycle actions, bulk operations
actions_image.go # Image CRUD actions
actions_resource.go # Volume, FIP, security group, keypair, LB actions
routing.go # View routing, modal updates, view change handling
render.go # View(), viewContent(), viewName()
connect.go # Cloud connection and picker switching
tabs.go # Dynamic tab registry (TabDef), tab switching, tab bar
shared/
keys.go # Global key bindings (Ctrl-prefixed for dangerous ops)
styles.go # Lipgloss theme constants (Solarized Dark)
messages.go # Shared message types for inter-component communication
cloud/
client.go # Auth, service client initialization, optional service detection
clouds.go # clouds.yaml parser
projects.go # Keystone project listing for project switching
compute/
servers.go # Server CRUD + pause/suspend/shelve/resize/reboot
actions.go # Instance action history
flavors.go # Flavor listing
keypairs.go # Keypair CRUD (list, get, create/generate, import, delete)
image/
images.go # Image listing
network/
networks.go # Network listing, external networks
ports.go # Port CRUD (list, create, update, delete)
routers.go # Router CRUD, interfaces, static routes
floatingips.go # Floating IP CRUD (allocate, associate, disassociate, release)
secgroups.go # Security group listing, rule create/delete
volume/
volumes.go # Volume CRUD (list, get, create, delete, attach, detach, volume types)
selfupdate/ # GitHub release self-update
loadbalancer/
lb.go # Octavia LB, listener, pool, member CRUD
quota/
quota.go # Compute, network, block storage quota fetching
ui/
serverlist/
serverlist.go # Server table with auto-refresh, filtering, bulk select, sorting
columns.go # Adaptive columns with flex weights and priority hiding
serverdetail/
serverdetail.go # Server property view with auto-refresh, network names
servercreate/
servercreate.go # Create form with inline pickers, count field
serverresize/
serverresize.go # Resize modal with flavor picker, current flavor indicator
consolelog/
consolelog.go # Scrollable console output viewer
actionlog/
actionlog.go # Instance action history viewer
volumelist/
volumelist.go # Volume table with sorting, server name resolution
volumedetail/
volumedetail.go # Volume property view with metadata, attachment info
floatingiplist/
floatingiplist.go # Floating IP table with sorting
secgroupview/
secgroupview.go # Security group viewer with expandable rules, rule deletion
keypaircreate/
keypaircreate.go # Key pair create/import form with type picker, file browser
keypairdetail/
keypairdetail.go # Key pair detail view showing public key
keypairlist/
keypairlist.go # Key pair table with sorting, delete, auto-refresh
lbview/ # Combined LB view (list + detail tree + search)
serverrename/ # Server rename inline input
serverrebuild/ # Server rebuild with image picker
serversnapshot/ # Server snapshot creation
networkview/ # Combined network view with subnets and ports
networkcreate/ # Network create form
subnetcreate/ # Subnet create form with IPv6 support
subnetedit/ # Subnet edit modal
subnetpicker/ # Subnet picker modal
portcreate/ # Port create form with port security, allowed address pairs
portedit/ # Port edit form
routerview/ # Combined router list + detail with interfaces
routercreate/ # Router create form
sgcreate/ # Security group create form
imageview/ # Combined image view (list + detail + servers)
imagecreate/ # Image upload form (file picker or URL, format detection)
imageedit/ # Image edit form (name, visibility, tags, etc.)
imagedownload/ # Image download with directory picker
volumecreate/
volumecreate.go # Volume create form with type/AZ pickers
serverpicker/
serverpicker.go # Server picker modal for volume attach
sgrulecreate/
sgrulecreate.go # Security group rule create modal
fippicker/
fippicker.go # Floating IP picker modal for server association
sshprompt/ # SSH IP selection prompt
cloneprogress/ # Server clone progress view
consoleurl/ # Console URL retrieval and browser launch
configview/ # Configuration display
lbcreate/ # Load balancer create form
lblistenercreate/ # LB listener create form
lbpoolcreate/ # LB pool create form
lbmembercreate/ # LB member create form
lbmonitorcreate/ # LB health monitor create form
volumepicker/ # Volume picker modal
projectpicker/
projectpicker.go # Project picker modal for project switching
quotaview/
quotaview.go # Quota overlay with ASCII progress bars
modal/
confirm.go # Confirmation dialog (single + bulk), custom body/title
error.go # Error modal
cloudpicker/
cloudpicker.go # Cloud selection overlay
statusbar/
statusbar.go # Bottom bar: cloud, project, region, context hints
help/
help.go # Scrollable help overlay
┌─────────────┐
start ────→ │ Cloud Picker │
└──────┬──────┘
│ select cloud (auto if single)
▼
┌─────────── Dynamic Tab Bar (1-N / ←→) ─────────────────────────────────────┐
│ Tabs built from service catalog: Servers, Volumes (if Cinder), │
│ Images, Floating IPs, Security Groups, Networks, Key Pairs — always. │
│ Load Balancers — if Octavia. Routers — always. │
│ │
│ ┌────────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌───────┐ ┌─────┐ ┌────┐ ┌───────┐ │
│ │Servers │ │Vols │ │Images│ │FIPs │ │SecGrps│ │Nets │ │Keys│ │Routers│ │
│ │List │ │List │ │View │ │List │ │View │ │View │ │List│ │List │ │
│ └──┬─┬───┘ └──┬───┘ └──┬───┘ └──────┘ └───────┘ └──┬──┘ └────┘ └──┬────┘ │
│ │ │^n Enter Enter │ Enter │
│ │ │ │ │ │ │ │
│ Enter ▼ ▼ ▼ ▼ ▼ │
│ │ ┌──────┐┌──────┐┌────────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ │Create││Vol ││Image Detail │ │Network │ │Router │ │
│ │ │Form ││Detail││+ Upload (^n) │ │+ Subnets │ │Detail │ │
│ │ └──────┘└──────┘│+ Download (d) │ │+ Ports │ │+ Intf list│ │
│ │ │+ Edit (e) │ │+ Port CRUD │ │+ Add (^a) │ │
│ ▼ └────────────────┘ │+ Subnet Edit│ │+ Rm (^t) │ │
│ ┌──────────┐ └─────────────┘ └───────────┘ │
│ │Server │ │
│ │Detail │ ┌──────────────────────────────────────────────┐ │
│ └──┬──┬────┘ │ Load Balancers (if Octavia) │ │
│ l a │ Combined view: LB list + detail tree │ │
│ │ │ │ Create: LB → Listener → Pool → Member │ │
│ ▼ ▼ │ Health monitor CRUD │ │
│ ┌────────┐ └──────────────────────────────────────────────┘ │
│ │Console │ ┌──────────┐ │
│ │Log │ │Action Log│ │
│ └────────┘ └──────────┘ │
└───────────────────────────────────────────────────────────────────────────-┘
Overlays (always available):
┌─────────────┐ ┌──────────┐ ┌──────┐ ┌────────┐ ┌──────────┐
│Confirm Modal│ │Error │ │Help │ │Resize │ │FIP Picker│
│(y/n/enter) │ │(enter) │ │(?) │ │(^f) │ │(^a) │
└─────────────┘ └──────────┘ └──────┘ └────────┘ └──────────┘
┌──────────────┐ ┌──────────┐ ┌──────────────┐
│Project Picker│ │Quotas │ │File Picker │
│(P) │ │(Q) │ │(in forms) │
└──────────────┘ └──────────┘ └──────────────┘
- Parse
clouds.yamlfrom standard locations:./clouds.yaml,$OS_CLIENT_CONFIG_FILE,~/.config/openstack/clouds.yaml,/etc/openstack/clouds.yaml - Cloud picker overlay when multiple clouds are configured
- Auto-select when only one cloud exists (override with
--pick-cloudflag) - Switch clouds at any time with
C - Authentication via Keystone v3 using gophercloud's
clouds.Parse+config.NewProviderClient
- Adaptive columns with flex weights — columns grow to fill terminal width
- Priority-based column hiding on narrow terminals (Name/Status always visible, IPv6 hides first)
- Columns: Name, Status (with power state), IPv4, IPv6, Floating IP, Flavor, Image, Age, Key
- Image names resolved from Glance (Nova only returns image ID with microversion 2.100)
- Lock indicator (🔒) on server names
- Auto-refresh at configurable interval (default 5s, set with
--refreshflag) - Auto-refresh persists across view changes (ticks always route to server list)
- Client-side filtering with
/(case-insensitive match on name, ID, status, IPs, flavor, image) - Status/power colors: ACTIVE/RUNNING=green, BUILD=yellow, SHUTOFF=gray, ERROR=red, REBOOT=cyan
- Bulk selection with
space(selected servers shown with ● prefix in purple) - LAZYSTACK branding badge in top-right corner
- Two-column property list: name, ID, status, power state, flavor, image (name + ID), keypair, locked, tenant, AZ, created, security groups, volumes
- Networks section: IPs grouped by network name with type and version info
- Auto-refresh at same interval as server list
- Scrollable viewport
- Resize pending banner with confirm/revert actions
- Pending action state — optimistic UI that suppresses stale API responses
- Empty fields hidden for cleaner display
- Form fields: Name, Image, Flavor, Network, Key Pair (inline filterable pickers), User Data (cloud-init file picker), Count
- User data field opens file picker for cloud-init scripts (.yaml, .yml, .sh, .cfg, .txt, .conf, extensionless)
- Count field (1–100) for batch creation using Nova's
min_count/max_count - Parallel resource fetching on form open (images, flavors, networks, keypairs)
- Type-to-filter in picker dropdowns
- Cursor advances to next field after picker selection
- Focusable Submit/Cancel buttons with hotkey labels
- Navigation: Tab/Shift+Tab/Arrow keys between fields, Enter to open pickers
- Submit via button or Ctrl+S hotkey
- Required for all server state-change actions (delete, reboot, stop/start, pause, suspend, shelve, lock/unlock, rescue/unrescue)
- Supports both single-server and bulk operations
- Focusable [y] Confirm / [n] Cancel buttons
- Navigate buttons with arrow keys, Tab, or use hotkeys directly
- Defaults to Cancel for safety
- API errors displayed in modal with context
- Dismissible with Enter or Esc
- Network errors during auto-refresh shown in status bar (non-blocking)
- Missing clouds.yaml shows helpful error with search paths
?toggles scrollable help overlay- Keybindings grouped by context (Global, Server List, Server Detail, Create Form, Console Log, Modals)
- Scrollable with ↑/↓ when content doesn't fit
- Shows current cloud name, project, and region
- Context-sensitive key hints per view (adapts to server state, selection count)
- Sticky hints for action success messages (survives background auto-refresh, clears on next key press)
- Error/warning display
- Truncates gracefully when bar overflows
- Terminal too small: centered warning with required dimensions (80x20 minimum)
- Empty server list: "press [ctrl+n] to create" message
- No clouds.yaml: error modal with guidance
- Ctrl+R restarts the app (re-exec with same flags) for rapid testing after rebuilds
- Pause/unpause (
p) — auto-detects current state - Suspend/resume (
ctrl+z) — auto-detects current state - Shelve/unshelve (
ctrl+e) — auto-detects current state - Resize (
ctrl+f) — modal flavor picker with filter, current flavor marked with ★ - Confirm resize (
ctrl+y) / Revert resize (ctrl+x) with optimistic UI - Hard reboot (
ctrl+p) - All toggle actions read server status from both list and detail views
- Scrollable console output (last 500 lines from Nova)
g/Gfor top/bottom navigationRto refreshEscreturns to previous view
- Scrollable list of all instance actions (create, reboot, resize, etc.)
- Shows action name, timestamp with relative age, request ID
- Failed actions highlighted in red
Rto refresh
- Modal overlay (not a separate view) — sits on top of list or detail
- Flavor list auto-sizes to content (no wrapping)
- Current flavor dimmed with ★ indicator
- After resize, server enters VERIFY_RESIZE state
- Detail view shows contextual banner: "Resize pending — ctrl+y confirm • ctrl+x revert"
- Confirm/revert uses optimistic UI with pending action state
- Staggered re-fetch (0.5s, 2s) for non-resize actions to catch state transitions
spacetoggles selection on current server (advances cursor)- Selected servers shown with ● prefix and purple highlighting
- Status bar shows selection count and available bulk actions
- Bulk delete, reboot, pause, suspend, shelve, resize all work on selection
- Confirm modal shows "N servers" for bulk operations
- Selection auto-clears after action execution
- Errors collected and reported as single modal (partial success visible)
- Same configurable interval as server list
Rfor manual refresh- Immediate re-fetch after actions (delete navigates to list)
- Pending action state prevents stale responses from overwriting optimistic updates
- Dynamic tabs built from service catalog (see Phase 4 refactor): Servers, Volumes, Floating IPs, Security Groups, Networks, Key Pairs (always present), Load Balancers (if Octavia available)
- Switch with number keys
1-9or←/→from any top-level list view - Tab bar with active tab highlighted, inactive tabs muted
- Each tab lazily initializes on first visit, auto-refreshes independently
- Background tick routing ensures all initialized tabs keep refreshing even when not active
- Volume List: Adaptive columns (Name, Status, Size, Type, Attached To, Device, Bootable), auto-refresh, sorting
- Volume Detail: Enter on a volume shows full properties — Name, ID, Status, Size, Type, AZ, Bootable, Encrypted, Multiattach, Description, Created, Updated, Snapshot ID, Source Volume ID, Attached Server (resolved name), Device, Metadata (key=value)
- Create (
Ctrl+N): Form with name, size (GB), type picker (from volume types API), AZ, description - Delete (
Ctrl+D): Confirmation modal, works from list or detail - Attach (
Ctrl+A): Server picker modal showing ACTIVE/SHUTOFF servers with type-to-filter - Detach (
Ctrl+T): From detail view, finds attached server and detaches - Status colors: available=green, in-use=cyan, creating/extending=yellow, error=red, deleting=muted
- List: Columns (Floating IP, Status, Fixed IP, Port ID), auto-refresh, sorting
- Allocate (
Ctrl+N): Allocates from first external network, shows progress in status bar - Associate (
Ctrl+Afrom server list/detail): Opens FIP picker modal showing unassociated IPs + "Allocate new" option. If no unassociated IPs exist, auto-allocates and assigns - Disassociate (
Ctrl+T): Confirmation modal, only enabled when FIP has a port - Release (
Ctrl+D): Confirmation modal
- Group List: Expandable groups showing name, description, rule count
- Rule View: Enter expands/collapses group rules. Rules show direction, protocol, port range, remote, ethertype
- Rule Navigation: Down arrow enters rule list within expanded group, Up arrow exits back to group level
- Create Rule (
Ctrl+Nin rules): Modal with cycle pickers for direction/ethertype/protocol, port range, remote IP prefix - Delete Rule (
Ctrl+Din rules): When cursor is on a rule, confirmation modal then deletes - Create Security Group (
Ctrl+Nat group level): Form with name and description - Delete Security Group (
Ctrl+Dat group level): Confirmation modal - Selected rule highlighted with
▸prefix and background color
- List: Columns (Name, Type), sorting, auto-refresh
- Detail (
Enter): Shows name, type, and full public key with scroll - Create/Import (
Ctrl+N): Form with name, type picker (RSA 2048/RSA 4096/ED25519), public key field with~/.ssh/file browser. Keys generated locally using Go crypto (crypto/rsa,crypto/ed25519,x/crypto/ssh), imported via Nova API - Save Private Key (
sin private key view): Save generated private key to file (default~/.ssh/<name>, 0600 permissions), public key saved alongside as.pub - Delete (
Ctrl+D): Confirmation modal, works from list or detail
- Networks Tab: Combined view with network list, expandable subnets, and port management
- Expandable Subnets: Enter expands/collapses network to show subnet details (name, CIDR, gateway, IP version, DHCP status)
- Create Network (
Ctrl+N): Form with name, admin state, shared option - Delete Network (
Ctrl+D): Confirmation modal - Create Subnet (
Ctrl+Nwhen expanded): Form with name, CIDR, gateway, IP version, DHCP, IPv6 address mode, IPv6 RA mode - Edit Subnet (
eon subnet): Modal to modify subnet properties - Delete Subnet (
Ctrl+Dwhen in subnets): Confirmation modal - IPv6 Subnet Support: Address mode (DHCP stateful, DHCP stateless, SLAAC, unmanaged), RA mode configuration, custom prefix length. Hidden IPv6-specific fields skipped during tab navigation when IP version is v4
- Port CRUD: Create ports with port security toggle and allowed address pairs. Edit existing ports. Delete ports with confirmation
- Auto-refresh, sorting, search/filter
- Router List: Columns (Name, Status, External Gateway, Routes), auto-refresh, sorting
- Router Detail (
Enter): Properties view with interfaces section and static routes - Create Router (
Ctrl+N): Form with name, external network selection, admin state - Delete Router (
Ctrl+D): Confirmation modal - Add Interface (
Ctrl+Afrom detail): Subnet picker modal with optional custom IP assignment - Remove Interface (
Ctrl+Tfrom detail): Confirmation modal. Handles removing individual IPs from multi-IP router ports - IPv6 handling: Auto-addressed IPv6 subnets handled correctly when adding interfaces. Supports routers with multiple IPs on the same network
scycles sort to next visible column (ascending),Stoggles sort direction- Active sort column shows ▲/▼ indicator
- Column header briefly highlights on sort change (1.5s)
- Sort persists through data refreshes
- Available on all list views (servers, volumes, floating IPs, key pairs)
- Networks section: Shows IPs grouped by network name instead of flat lists
- Assign Floating IP (
Ctrl+A): Opens FIP picker modal
Rforce refresh — handled globally, dispatches to active viewPgUp/PgDn— page navigation everywhere arrow keys work (lists, detail views, help modal)
- Custom title and body text per resource type (e.g., "Delete Volume", "Release Floating IP")
- Reused across all resource types via generic action routing
- Split monolithic
app.go(1,643 lines) into 7 focused files with no logic changes - Replaced hardcoded
activeTabenum with dynamicTabDefregistry — tabs are now data-driven - Tab number keys
1-9map dynamically to index position; arrow keys cycle with modulo - Tab availability determined at connection time based on service catalog
- Octavia (Load Balancer) service detected optionally via
openstack.NewLoadBalancerV2 - Block Storage detected optionally (try v3, v2, v1 — same as before)
- Tabs built conditionally: Volumes only if Cinder, Load Balancers only if Octavia
- Clouds without optional services work normally — those tabs simply don't appear
- Combined View: Merged list+detail with search/filter, tree structure (Listeners → Pools → Members)
- Create LB (
Ctrl+N): Form with name, VIP subnet, VIP address - Create Listener (
Ctrl+Non LB): Protocol, port, connection limit - Create Pool (
Ctrl+Non listener): Algorithm, protocol, session persistence - Create Member (
Ctrl+Non pool): Address, port, weight, subnet - Create Health Monitor (
Ctrl+Non pool): Type, delay, timeout, max retries - Edit: Members (weight, admin state), pools, listeners
- Delete (
Ctrl+D): Cascade delete for LBs, individual delete for listeners/pools/members/monitors - Bulk member operations: Add multiple members at once
- Status colors: ACTIVE/ONLINE=green, PENDING_*=yellow, ERROR/OFFLINE=red
- After cloud connection, accessible projects fetched in background via Keystone
ListAvailable - If more than one project available,
Pkey opens project picker modal - Current project marked with
*in picker - On selection, re-authenticates scoped to new project via
ConnectWithProject(overrides TenantID) - All tabs reset on project switch (same as cloud switch)
- Project name shown in status bar between cloud and region
Qkey opens full-screen quota overlay (same pattern as help)- Three sections: Compute (instances, cores, RAM, key pairs, server groups), Network (floating IPs, networks, ports, routers, security groups, subnets), Block Storage (volumes, gigabytes, snapshots, backups)
- ASCII progress bars (18 chars wide) with color coding: green (<70%), yellow (70-90%), red (>90%)
- Unlimited quotas (limit=-1) shown as "used / unlimited" with no bar
- Lazy fetch on open, cached for 30 seconds
- Scroll support, close with
QorEsc - Block Storage section omitted if Cinder unavailable
- Inline rename from server list or detail view
- Text input pre-filled with current name
- Uses
servers.Update()API
- Rebuild server with new image, keeping same ID/IPs
- Image picker modal with type-to-filter
- Confirmation before rebuild (destructive operation)
- Create image snapshot from running server
- Name input pre-filled with server name
- Progress shown in status bar
- Handles 409 conflict (snapshot already in progress) with friendly error
- Toggle rescue mode for broken servers
- Confirmation modal
- Rescue mode returns admin password displayed in status bar
- Supports bulk rescue operations
- Combined View: Merged list+detail with servers-using-image panel, search/filter
- Image Detail (
Enter): Full properties view with servers using this image - Upload (
Ctrl+N): Local file picker or URL import with disk format auto-detection (qcow2, raw, vmdk, vdi, iso, vhd, aki, ari, ami). Auto-fills image name from filename. Progress bar with atomic counters - Download (
d): Stream image to local file with directory picker and overwrite protection. Progress bar - Edit (
e): Modify name, visibility, min disk/RAM, tags, protected flag - Delete Image (
Ctrl+D): Confirmation modal, works from list or detail - Deactivate/Reactivate: Toggle image availability
- Launch SSH session directly from server list or detail view (
xkey) - SSH prompt with IP selection (floating IP, IPv4, IPv6 — IPv6 preferred when available)
- Copy SSH command to clipboard (
ykey) - Option to ignore host key checking
Yopens a copy-field picker modal on every list/detail view- Arrow keys + Enter to copy;
1-9number row for quick-pick - Fields surfaced per resource:
- Server: ID, Name, each IPv4/IPv6/Floating IP (one row per address)
- Volume: ID, Name
- Floating IP: ID, Floating Address, Fixed IP, Port ID
- Network: Network ID/Name; focused subnet's ID/Name/CIDR/Gateway; focused port's ID/Name/MAC/Fixed IPs
- Router: ID, Name, External Gateway IPv4/IPv6/Network ID; focused interface's Subnet/Port/IP
- Security group: ID, Name; focused rule's ID; focused attached server ID
- Load balancer: LB ID/Name/VIP; focused listener/pool/member ID/Name (and member Address)
- Image: ID, Name, Checksum, Owner; focused attached server ID
- Keypair: Name (list); Name + Public key (detail)
- Empty fields are skipped so the menu never shows placeholders
- Clipboard writes emit
"Copied <label>: <value>"to the status bar; errors surface as"Clipboard error: …"
- Clone a server with its configuration (flavor, network, key pair, security groups)
- Progress view showing clone status
- Retrieve VNC console URL from Nova
- Opens URL in default browser
- Jump from server detail to attached volumes, networks, security groups
- Jump from volume detail to attached server
- Resource links are navigable with keyboard
--updateflag downloads latest release from GitHub--no-check-updateskips automatic version check on startup- Downloads binary for current OS/architecture with SHA256 checksum verification
| Flag | Type | Default | Description |
|---|---|---|---|
--version |
bool | false | Print version and exit |
--pick-cloud |
bool | false | Always show cloud picker, even with one cloud |
--cloud NAME |
string | "" | Connect directly to named cloud, skip picker |
--refresh N |
int | 5 | Auto-refresh interval in seconds |
--idle-timeout N |
int | 0 | Pause polling after N minutes of no input (0 = disabled) |
--no-check-update |
bool | false | Skip automatic update check on startup |
--update |
bool | false | Self-update to the latest version |
| Key | Action |
|---|---|
q / Ctrl+C |
Quit |
? |
Toggle help (scrollable) |
C |
Switch cloud |
1-9 / ←/→ |
Switch tab (dynamic based on available services) |
R |
Force refresh |
s / S |
Sort column / reverse sort |
P |
Switch project (when multiple projects available) |
Q |
Resource quotas overlay |
PgUp / PgDn |
Page up / page down |
Ctrl+R |
Restart app (re-exec binary) |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
space |
Select/deselect for bulk actions |
Enter |
View detail |
Ctrl+N |
Create server |
Ctrl+D |
Delete server (or selected) |
Ctrl+O |
Soft reboot (or selected) |
p |
Pause/unpause (or selected) |
Ctrl+Z |
Suspend/resume (or selected) |
o |
Stop/start (or selected) |
Ctrl+E |
Shelve/unshelve (or selected) |
Ctrl+L |
Lock/unlock (or selected) |
Ctrl+W |
Rescue/unrescue (or selected) |
Ctrl+F |
Resize (modal) |
r |
Rename server |
Ctrl+G |
Rebuild with new image |
Ctrl+S |
Create snapshot |
Ctrl+A |
Assign floating IP (FIP picker modal) |
l |
Console log |
a |
Action history |
x |
SSH to server |
y |
Copy SSH command to clipboard |
Y |
Copy field picker (ID, IP, floating IP, …) |
V |
Open VNC console in browser |
c |
Clone server |
/ |
Filter |
Esc |
Clear filter / clear selection |
| Key | Action |
|---|---|
↑/k ↓/j |
Scroll |
Ctrl+D |
Delete server |
Ctrl+O |
Soft reboot |
Ctrl+P |
Hard reboot |
p |
Pause/unpause |
Ctrl+Z |
Suspend/resume |
Ctrl+E |
Shelve/unshelve |
Ctrl+W |
Rescue/unrescue |
Ctrl+F |
Resize (modal) |
r |
Rename server |
Ctrl+G |
Rebuild with new image |
Ctrl+S |
Create snapshot |
Ctrl+A |
Assign floating IP (FIP picker modal) |
Ctrl+Y |
Confirm resize (when VERIFY_RESIZE) |
Ctrl+X |
Revert resize (when VERIFY_RESIZE) |
l |
Console log |
a |
Action history |
x |
SSH to server |
y |
Copy SSH command to clipboard |
Y |
Copy field picker (ID, IP, floating IP, …) |
V |
Open VNC console in browser |
Esc |
Back to list |
| Key | Action |
|---|---|
Tab / ↓ |
Next field |
Shift+Tab / ↑ |
Previous field |
Enter |
Open picker / activate button / advance |
Ctrl+S |
Submit (hotkey) |
Esc |
Cancel |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Enter |
View detail |
Ctrl+N |
Create volume |
Ctrl+D |
Delete volume |
Y |
Copy field picker |
/ |
Filter |
| Key | Action |
|---|---|
↑/k ↓/j |
Scroll |
Ctrl+D |
Delete volume |
Ctrl+A |
Attach to server (server picker modal) |
Ctrl+T |
Detach from server |
Y |
Copy field picker |
Esc |
Back to list |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Ctrl+N |
Allocate new floating IP |
Ctrl+T |
Disassociate from port |
Ctrl+D |
Release floating IP |
Y |
Copy field picker |
/ |
Filter |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate groups / rules |
Enter |
Expand / collapse group |
Ctrl+N |
Create group (or add rule when in rules) |
Ctrl+D |
Delete group (or rule when in rules) |
Y |
Copy field picker |
Esc |
Back to group level (from rules) |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Enter |
View detail (public key) |
Ctrl+N |
Create / import key pair |
Ctrl+D |
Delete key pair |
Y |
Copy field picker |
/ |
Filter |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Enter |
Expand / collapse subnets |
Ctrl+N |
Create network (or subnet/port contextually) |
Ctrl+D |
Delete network (or subnet/port contextually) |
e |
Edit subnet (when on subnet) |
Y |
Copy field picker |
/ |
Filter |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Enter |
View detail (interfaces, static routes) |
Ctrl+N |
Create router |
Ctrl+D |
Delete router |
Ctrl+A |
Add interface (from detail, with optional custom IP) |
Ctrl+T |
Remove interface (from detail) |
Y |
Copy field picker |
/ |
Filter |
Esc |
Back to list (from detail) |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Enter |
Expand / view detail (tree navigation) |
Ctrl+N |
Create (LB / listener / pool / member / monitor, contextual) |
e |
Edit (member, pool, listener) |
Ctrl+D |
Delete (cascade for LBs, individual for children) |
Y |
Copy field picker |
/ |
Filter |
Esc |
Collapse / back |
| Key | Action |
|---|---|
↑/k ↓/j |
Navigate |
Enter |
View detail |
Ctrl+N |
Upload image (file picker or URL) |
d |
Download image to local file |
e |
Edit image properties |
Ctrl+D |
Delete image |
Y |
Copy field picker |
/ |
Filter |
| Key | Action |
|---|---|
↑/k ↓/j |
Scroll |
g / G |
Top / Bottom (console only) |
Esc |
Back to previous view |
| Key | Action |
|---|---|
y |
Confirm |
n / Esc |
Cancel |
←/→ ↑/↓ Tab |
Navigate buttons |
Enter |
Activate focused button |
- Color palette: Solarized Dark base
- Primary accent:
#7D56F4(purple) — used for branding badge, selected items, focused buttons - Secondary:
#6C71C4 - Status indicators: Green (ACTIVE/RUNNING), Yellow (BUILD/warning), Red (ERROR), Gray (SHUTOFF), Cyan (REBOOT)
- Power state colors: Green (RUNNING), Gray (SHUTDOWN), Red (CRASHED), Muted (PAUSED/SUSPENDED)
- Selected row:
#073642background with bold text - Bulk selected: ● prefix with purple text
- Locked servers: 🔒 prefix on name
- Current flavor in resize: Dimmed with ★ suffix
- Buttons: Styled with background color, highlight on focus (green for confirm/submit, red for cancel/deny)
- Branding: LAZYSTACK pill badge in top-right, dark text on purple background
- ✅ Create Volume form — name, size, type picker, AZ, description
- ✅ Create/Import Key Pair — RSA/ED25519, file browser, save-to-file
- ✅ Create Security Group Rule — modal with cycle pickers
- ✅ Volume Attach from detail — server picker modal
- ✅ Network/subnet browsing — Networks tab with expandable subnets, port CRUD, subnet edit
Actions available in Nova but not yet implemented, prioritized by usefulness:
- ✅ Rename server —
rkey, inline rename - ✅ Rebuild —
Ctrl+G, image picker modal - ✅ Create snapshot —
Ctrl+S, creates image from server - ✅ Rescue/Unrescue —
Ctrl+W, toggle rescue mode
- ✅ Console access (noVNC) — Retrieve and open VNC console URL via
Vkey. Opens in browser or copies to clipboard. - Get password — Retrieve auto-generated password for Windows VMs (
servers.GetPassword).
- Migrate / Live Migrate — Move server to different hypervisor. Admin privilege required.
- Evacuate — Emergency recovery from failed host. Admin-only.
- Force Delete — Bypass normal deletion flow. Admin-only.
- Reset State — Force server into a specific state. Recovery from stuck transitions.
- Metadata browser — Full CRUD on server metadata key-value pairs.
- Configuration file (
~/.config/lazystack/config.yaml) for defaults - Custom column selection and ordering
- Saved filters
- Server name templates for create
- ✅ SSH integration (launch SSH session to selected server) — Done (#27)
- ✅ Copy-to-clipboard for IDs, IPs — Done (#28):
Yopens a field picker on every list/detail view (server, volume, network, subnet, router, port, LB + listener/pool/member, floating IP, security group, keypair, image).ystill copies the SSH command on server views. - Log/audit trail of actions taken
- Designate (DNS) tab
- ✅ Console access (noVNC URL retrieval and browser launch) — Done (#50)
- ✅ Image upload/download/edit with file picker and combined view — Done (#126)
- ✅ Cloud-init / user data file picker in server create — Done
- ✅ Port CRUD with port security and allowed address pairs — Done
- ✅ Subnet edit modal — Done
- ✅ IPv6 subnet support (address mode, RA mode, auto-addressed subnets) — Done
- ✅ Search/filter (
/) on all list views — Done - ✅ Load balancer full CRUD (create/edit/delete LB, listeners, pools, members, monitors) — Done
- ✅ Server cloning with progress view — Done
- Hypervisor view (admin)
- User management (admin)
- Service catalog browser
- Migrate/evacuate, force delete, reset state (see Server Action Gaps above)
- Mouse-driven interaction: This is a keyboard-first tool. Mouse support may come later but is not a priority.
- Full Horizon replacement: lazystack focuses on the most common operations. Rarely-used Horizon features (e.g., orchestration, identity management) are out of scope.
- Multi-platform OpenStack alternatives: This tool is specifically for OpenStack, not a generic cloud TUI.
- Configuration management: lazystack is for interactive operations, not declarative infrastructure management (use Terraform/OpenTofu for that).
go buildproduces a single binary with zero runtime dependencies- Connects to any standard OpenStack cloud via
clouds.yaml - Server list loads and auto-refreshes without manual intervention
- Full VM lifecycle (list, inspect, create, delete, reboot, pause, suspend, shelve, resize) from keyboard
- All destructive actions require Ctrl-prefix and explicit confirmation
- Responsive at terminal sizes from 80x20 upward
- Bulk operations work on multi-selected servers
- Console log and action history accessible per server