Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dda6fb8e91 | ||
|
|
cf250604f9 | ||
|
|
963d41f1ed | ||
|
|
5188cca9aa | ||
|
|
f39f59fb81 | ||
|
|
61fc61cceb | ||
|
|
4b9800aef3 | ||
|
|
c447674ef9 | ||
|
|
a77bfc5070 | ||
|
|
7b381d0387 | ||
|
|
ec43d631d4 | ||
|
|
82a9952e63 | ||
|
|
1eb4be2c01 | ||
|
|
6f1edb938e | ||
|
|
bd96782229 | ||
|
|
cf23438a5c | ||
|
|
d2c3fd066e | ||
|
|
1bfec68d77 | ||
|
|
bda1372a1d | ||
|
|
13c700ca44 | ||
|
|
f6f87d6b03 | ||
|
|
4f75117540 | ||
|
|
2bb02e50d9 | ||
|
|
4d1c3565c9 | ||
|
|
3678aedc88 |
@@ -7,9 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.0.5] - 2026-08-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- LND Update to v0.21.1-beta
|
||||||
|
- Nixpkgs Update
|
||||||
|
- Nixpkgs Update
|
||||||
|
- Update LND REST Zeus Connect instructions and design to match NWC exactly
|
||||||
|
- UI: Align LND REST and NWC Zeus connection instructions styling
|
||||||
|
- Make Zeus LND REST instructions coherent with NWC Zeus connect guide
|
||||||
|
- Clarify Zeus NWC wallet setup
|
||||||
|
- Make Manual Backup match the system role (Desktop Only scope)
|
||||||
|
- Reload Caddy immediately when a domain (or ACME email) is saved
|
||||||
|
- Unify Njal.la + router port-forwarding guidance across onboarding and feature enable modals
|
||||||
|
- Enhance Lightning Wallet Connect modal with professional benefits grid and refined messaging
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Resolve 'vdev' version badge and align it under the Hub title
|
||||||
|
- Align release script with Gitea staging-dev and stable branch workflow
|
||||||
|
- Auto-detect git remotes and clean up temp files in release-stable.sh
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Write real v1.0.4 changelog and auto-generate release notes in release script
|
||||||
|
[1.0.5]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.0.5
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Installer VM compatibility: legacy BIOS VM boots now install with GRUB, UEFI VM installs avoid depending on NVRAM boot-entry writes, VM users get clearer disk/resource guidance, and the internet check falls back to HTTPS when ICMP is blocked.
|
||||||
- **Manual Backup now matches the system role**: on the Desktop Only role the Hub's
|
- **Manual Backup now matches the system role**: on the Desktop Only role the Hub's
|
||||||
"What gets backed up" list no longer shows Node / Server + Desktop items
|
"What gets backed up" list no longer shows Node / Server + Desktop items
|
||||||
(nix-bitcoin secrets, `/var/lib` system service data, and the database/blockchain
|
(nix-bitcoin secrets, `/var/lib` system service data, and the database/blockchain
|
||||||
|
|||||||
+27
-5
@@ -2,13 +2,35 @@
|
|||||||
|
|
||||||
First off, thank you for considering contributing to Sovran_SystemsOS! 🎉
|
First off, thank you for considering contributing to Sovran_SystemsOS! 🎉
|
||||||
|
|
||||||
## This Github Repo
|
## Development and Release Repositories
|
||||||
|
|
||||||
This repo is for the development of Sovran_SystemsOS and serves as its connection to the GitHub ecosystem. The main repo for Sovran_SystemsOS is hosted at https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable. All activity in this GitHub repo is tested and eventually mirrored to the main repo on Gitea, as the ethos of Sovran_SystemsOS is self-sovereignty.
|
The GitHub repository is the primary location for Sovran_SystemsOS development
|
||||||
|
and collaboration and serves as the project's connection to the GitHub
|
||||||
|
ecosystem. Most work is contributed through feature branches and pull requests
|
||||||
|
that target GitHub `main`. Development may also begin in a local branch or on
|
||||||
|
`staging-dev` at the project's self-hosted Gitea instance:
|
||||||
|
https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS.
|
||||||
|
|
||||||
Please note: this repo may contain new features and code not yet in the stable branch on Gitea, and this code is not fully tested.
|
GitHub `main` and Gitea `staging-dev` are kept synchronized as the active
|
||||||
|
development line, although one may briefly lead the other while changes are
|
||||||
|
being integrated. The canonical, release-ready code is maintained on Gitea's
|
||||||
|
[`stable` branch](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable),
|
||||||
|
in keeping with the self-sovereignty ethos of Sovran_SystemsOS.
|
||||||
|
|
||||||
Moreover, Sovran_SystemsOS has been improved with the help of AI. We have used Copilot to work through significant coding challenges and troubleshooting hurdles. We will continue to use AI to help keep Sovran_SystemsOS stable and maintained.
|
The workflow is:
|
||||||
|
|
||||||
|
1. Most development and project collaboration — including issues, pull
|
||||||
|
requests, and reviews — happens through GitHub, with completed work
|
||||||
|
integrated into `main`.
|
||||||
|
2. Work may also originate locally or on Gitea `staging-dev`. Accepted changes
|
||||||
|
are synchronized between GitHub `main` and Gitea `staging-dev`.
|
||||||
|
3. When changes are complete and tested, they are promoted to Gitea `stable`,
|
||||||
|
which is the code used by released Sovran_SystemsOS builds.
|
||||||
|
|
||||||
|
Please note: GitHub `main` and Gitea `staging-dev` may contain new features and
|
||||||
|
code not yet in `stable`, and that code may not be fully tested.
|
||||||
|
|
||||||
|
Moreover, Sovran_SystemsOS has been improved with the help of AI. We have used Copilot and Arean.Ai to work through significant coding challenges and troubleshooting hurdles. We will continue to use AI to help keep Sovran_SystemsOS stable and maintained.
|
||||||
|
|
||||||
## How Can I Contribute?
|
## How Can I Contribute?
|
||||||
|
|
||||||
@@ -26,7 +48,7 @@ Moreover, Sovran_SystemsOS has been improved with the help of AI. We have used C
|
|||||||
### 🔧 Submitting Code Changes
|
### 🔧 Submitting Code Changes
|
||||||
|
|
||||||
#### 1. Fork the Repository
|
#### 1. Fork the Repository
|
||||||
Click the "Fork" button at the top right of this repository.
|
Click the "Fork" button at the top right of the GitHub repository.
|
||||||
|
|
||||||
#### 2. Clone Your Fork
|
#### 2. Clone Your Fork
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,24 +4,25 @@
|
|||||||
|
|
||||||
# Sovran_SystemsOS
|
# Sovran_SystemsOS
|
||||||
|
|
||||||
### Your Bitcoin life. Your keys. Your node. Your machine.
|
### Bitcoin sovereignty. Sovereign computing. One system.
|
||||||
|
|
||||||
Sovran_SystemsOS is a free and open-source operating system for **Bitcoin
|
Sovran_SystemsOS is a free and open-source operating system built for two
|
||||||
self-custody** and **digital sovereignty**. Hold your own keys, trade
|
inseparable freedoms: **Bitcoin self-custody** and **sovereign computing**.
|
||||||
peer-to-peer, and verify your own money with your own node. Then extend the
|
Hold your own keys, trade peer-to-peer, and verify your own money with your
|
||||||
same ownership to the rest of your digital life: your files, communications,
|
own node. Then claim that same uncompromising ownership over the rest of your
|
||||||
passwords, and websites, all on hardware you control.
|
digital life — your files, communications, passwords, and websites — all on
|
||||||
|
hardware you control, running auditable open-source software you can trust.
|
||||||
|
|
||||||
Every installation is a private [NixOS](https://nixos.org) desktop with
|
Every installation is a private [NixOS](https://nixos.org) desktop with
|
||||||
[Sparrow Wallet](https://sparrowwallet.com), [Bisq](https://bisq.network), and
|
[Sparrow Wallet](https://sparrowwallet.com), [Bisq](https://bisq.network), and
|
||||||
[Bisq 2](https://github.com/bisq-network/bisq2) ready to use. Move beyond
|
[Bisq 2](https://github.com/bisq-network/bisq2) ready to use. Move beyond
|
||||||
custodial exchanges from the first boot, and grow into your own Bitcoin and
|
custodial exchanges from the first boot, and grow into your own Bitcoin node,
|
||||||
Lightning infrastructure when you are ready.
|
Lightning infrastructure, private cloud, and communications platform when you
|
||||||
|
are ready.
|
||||||
**Privacy. Sovereignty. Bitcoin.**
|
|
||||||
|
|
||||||
[Visit the Website](https://sovransystems.com) ·
|
[Visit the Website](https://sovransystems.com) ·
|
||||||
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.0.3.iso) ·
|
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.0.3.iso) ·
|
||||||
|
[Try it safely in a VM](#try-it-first-in-a-virtual-machine) ·
|
||||||
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS.iso.sha256) ·
|
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS.iso.sha256) ·
|
||||||
[Build from Source](#build-from-source)
|
[Build from Source](#build-from-source)
|
||||||
|
|
||||||
@@ -31,17 +32,25 @@ Lightning infrastructure when you are ready.
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
> **📌 Active development on GitHub `main` / Gitea `staging-dev` — releases on
|
||||||
|
> [Gitea `stable`](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable).**
|
||||||
|
> See [Development workflow](#development-workflow) for details.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
- [Why Sovran_SystemsOS?](#why-sovran_systemsos)
|
- [Why Sovran_SystemsOS?](#why-sovran_systemsos)
|
||||||
|
- [Try it first in a virtual machine](#try-it-first-in-a-virtual-machine)
|
||||||
- [What is included](#what-is-included)
|
- [What is included](#what-is-included)
|
||||||
- [Three modes](#three-modes)
|
- [Three modes](#three-modes)
|
||||||
- [Use it your way](#use-it-your-way)
|
- [Use it your way](#use-it-your-way)
|
||||||
- [The Sovran Hub](#the-sovran-hub)
|
- [The Sovran Hub](#the-sovran-hub)
|
||||||
- [Install Sovran_SystemsOS](#install-sovran_systemsos)
|
- [Install Sovran_SystemsOS](#install-sovran_systemsos)
|
||||||
- [For developers](#for-developers)
|
- [For developers](#for-developers)
|
||||||
|
- [Development workflow](#development-workflow)
|
||||||
|
- [Build from source](#build-from-source)
|
||||||
|
- [About Bitcoin wallet entropy](#about-bitcoin-wallet-entropy)
|
||||||
- [Security approach](#security-approach)
|
- [Security approach](#security-approach)
|
||||||
- [Acknowledgements](#acknowledgements)
|
- [Acknowledgements](#acknowledgements)
|
||||||
- [License](#license) · [Contributing](#contributing)
|
- [License](#license) · [Contributing](#contributing)
|
||||||
@@ -98,6 +107,40 @@ and a reproducible, auditable NixOS foundation.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Try it first in a virtual machine
|
||||||
|
|
||||||
|
**Curious, but not ready to replace your current operating system?** Start with
|
||||||
|
Sovran_SystemsOS in a virtual machine (VM). A VM runs Sovran_SystemsOS in a
|
||||||
|
window on your existing Windows, macOS, or Linux computer, using a virtual disk
|
||||||
|
file instead of your computer's internal drive. You can explore the desktop,
|
||||||
|
Sovran Hub, Sparrow, Bisq, and the installation experience before changing how
|
||||||
|
you use any physical machine.
|
||||||
|
|
||||||
|
This is a low-commitment way to decide whether Sovran_SystemsOS is right for
|
||||||
|
you:
|
||||||
|
|
||||||
|
- **Keep your current OS.** Closing or deleting the VM leaves the host operating
|
||||||
|
system in place.
|
||||||
|
- **Learn at your own pace.** Familiarize yourself with the desktop and tools
|
||||||
|
without needing to make it your daily computer on day one.
|
||||||
|
- **Choose your next step with confidence.** When you are ready, install it on
|
||||||
|
a dedicated computer, or keep using the VM as a learning environment.
|
||||||
|
|
||||||
|
You can use the same ISO with [VirtualBox](https://www.virtualbox.org), VMware,
|
||||||
|
QEMU/KVM, Proxmox, and similar x86_64 VM software. For a first look, select
|
||||||
|
**Desktop Only**, allocate at least **8 GB RAM**, and create a **256 GB or
|
||||||
|
larger dynamically allocated virtual disk**. The full VM setup and installer
|
||||||
|
requirements are in [Installing in a virtual machine](#installing-in-a-virtual-machine-optional).
|
||||||
|
|
||||||
|
> **A VM is for evaluation and learning, not a substitute for a dedicated,
|
||||||
|
> hardened setup.** Do not use a trial VM to hold meaningful Bitcoin, recovery
|
||||||
|
> phrases, passwords, or other sensitive data. Avoid attaching physical drives
|
||||||
|
> to the VM, and be deliberate about shared folders, clipboard sharing, and
|
||||||
|
> network settings. The installer only changes the disk you select, but you
|
||||||
|
> should always review VM disk selections before confirming an install.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## What is included
|
## What is included
|
||||||
|
|
||||||
Depending on the selected mode and enabled features, Sovran_SystemsOS brings
|
Depending on the selected mode and enabled features, Sovran_SystemsOS brings
|
||||||
@@ -390,6 +433,26 @@ installer. Do not format the drive.
|
|||||||
If the normal operating system starts instead, restart and try the boot-menu
|
If the normal operating system starts instead, restart and try the boot-menu
|
||||||
key again.
|
key again.
|
||||||
|
|
||||||
|
### Installing in a virtual machine (optional)
|
||||||
|
|
||||||
|
Sovran_SystemsOS can be tested in VirtualBox, VMware, QEMU/KVM, Proxmox, and
|
||||||
|
similar x86_64 virtual machines. Use production-like resources where possible:
|
||||||
|
|
||||||
|
- Allocate **8 GB RAM or more** for Desktop Only. Node and Server + Desktop
|
||||||
|
should follow the normal 16 GB / 32 GB recommendations.
|
||||||
|
- Create a **256 GB or larger virtual OS disk**. Thin-provisioned / dynamically
|
||||||
|
allocated disks are fine; they do not consume the full size immediately.
|
||||||
|
- Use NAT or bridged networking with internet access before opening the
|
||||||
|
installer.
|
||||||
|
- UEFI/EFI firmware is preferred. In UEFI VMs, the installer avoids depending
|
||||||
|
on VM NVRAM boot-entry writes. If a VM boots the ISO in legacy BIOS mode,
|
||||||
|
the installer automatically switches the installed system to GRUB.
|
||||||
|
- For Node or Server + Desktop, attach a **second 2 TB virtual data disk**.
|
||||||
|
If you only attach one disk, choose **Desktop Only**.
|
||||||
|
- Present the install target as a normal virtual disk such as VirtIO, SATA,
|
||||||
|
SCSI, or NVMe. USB-attached target disks are intentionally hidden by the
|
||||||
|
installer to avoid erasing the installer USB by mistake.
|
||||||
|
|
||||||
### 5. Install
|
### 5. Install
|
||||||
|
|
||||||
Follow the on-screen installer. Before confirming:
|
Follow the on-screen installer. Before confirming:
|
||||||
@@ -452,6 +515,20 @@ Sovran Hub into a complete Bitcoin operating system. The operating system
|
|||||||
configuration, installer, Hub, desktop integration, Bitcoin services, and
|
configuration, installer, Hub, desktop integration, Bitcoin services, and
|
||||||
optional self-hosting services are all maintained in this repository.
|
optional self-hosting services are all maintained in this repository.
|
||||||
|
|
||||||
|
### Development workflow
|
||||||
|
|
||||||
|
| Branch | Host | Purpose |
|
||||||
|
|---------------|---------------------------------------------------|-----------------------------|
|
||||||
|
| `main` | [GitHub](https://github.com/naturallaw777/Sovran_SystemsOS) | Active development & PRs |
|
||||||
|
| `staging-dev` | [Gitea](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS) | Synced with `main` |
|
||||||
|
| `stable` | [Gitea](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable) | Tested, release-ready builds |
|
||||||
|
|
||||||
|
**Flow:** develop → sync `main` ↔ `staging-dev` → promote to `stable`
|
||||||
|
|
||||||
|
> ⚠️ `main` and `staging-dev` may contain unreleased or less-tested code. For
|
||||||
|
> stable, audited code, use Gitea
|
||||||
|
> [`stable`](https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/src/branch/stable).
|
||||||
|
|
||||||
### Technology
|
### Technology
|
||||||
|
|
||||||
- [NixOS](https://nixos.org) and [Nix flakes](https://nixos.wiki/wiki/Flakes)
|
- [NixOS](https://nixos.org) and [Nix flakes](https://nixos.wiki/wiki/Flakes)
|
||||||
@@ -472,10 +549,23 @@ optional self-hosting services are all maintained in this repository.
|
|||||||
|
|
||||||
You need a system with Nix installed and flakes enabled.
|
You need a system with Nix installed and flakes enabled.
|
||||||
|
|
||||||
|
Clone the **stable** branch from Gitea (recommended for builds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone -b stable https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS.git
|
||||||
|
cd Sovran_SystemsOS
|
||||||
|
```
|
||||||
|
|
||||||
|
Or clone the **active development** line from GitHub:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/naturallaw777/Sovran_SystemsOS.git
|
git clone https://github.com/naturallaw777/Sovran_SystemsOS.git
|
||||||
cd Sovran_SystemsOS
|
cd Sovran_SystemsOS
|
||||||
|
```
|
||||||
|
|
||||||
|
Then build the installer ISO:
|
||||||
|
|
||||||
|
```bash
|
||||||
nix build \
|
nix build \
|
||||||
.#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
|
.#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
|
||||||
```
|
```
|
||||||
@@ -593,6 +683,42 @@ production environment.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## About Bitcoin wallet entropy
|
||||||
|
|
||||||
|
When you create a Bitcoin wallet in Sparrow Wallet, you generate a **24-word
|
||||||
|
seed phrase**. You should also apply a **128-bit BIP39 passphrase**. These are
|
||||||
|
**two independent entropy sources** — both are required to recover your wallet,
|
||||||
|
and neither one alone is sufficient.
|
||||||
|
|
||||||
|
**Create each entropy source on different hardware.**
|
||||||
|
|
||||||
|
The seed phrase is generated by Sparrow Wallet on your Sovran_SystemsOS machine.
|
||||||
|
The 128-bit passphrase should be generated on a separate device — for example:
|
||||||
|
|
||||||
|
- A **GrapheneOS** phone using Bitwarden's passphrase generator
|
||||||
|
- A different computer running the **Diceware** program
|
||||||
|
|
||||||
|
This way, no single device ever produces or holds both sources of randomness.
|
||||||
|
If one device is ever compromised, the other entropy source remains unknown to
|
||||||
|
an attacker. Your wallet cannot be recovered without **both**.
|
||||||
|
|
||||||
|
This is not a recommendation against hardware wallets, dice, or any other
|
||||||
|
well-tested method of creating entropy. Those are established and valuable
|
||||||
|
approaches. If you use a hardware wallet, verify for yourself that its entropy
|
||||||
|
generation — both the software and the hardware — is sound and follows best
|
||||||
|
practices.
|
||||||
|
|
||||||
|
The takeaway is simply this: **how your entropy is created matters, and
|
||||||
|
generating your two entropy sources on two physically separate devices is a
|
||||||
|
strong practice to understand before setting up a Bitcoin wallet** with the
|
||||||
|
software included in Sovran_SystemsOS.
|
||||||
|
|
||||||
|
**You must do your own research.** The information above is basic educational
|
||||||
|
context to help you build your own best practices — not financial advice and not
|
||||||
|
a substitute for your own due diligence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Security approach
|
## Security approach
|
||||||
|
|
||||||
Sovran_SystemsOS is designed around local ownership and explicit control.
|
Sovran_SystemsOS is designed around local ownership and explicit control.
|
||||||
@@ -722,7 +848,10 @@ Read the complete license terms in [`LICENSE`](LICENSE).
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.
|
We welcome contributions! The
|
||||||
|
[GitHub repository](https://github.com/naturallaw777/Sovran_SystemsOS) is the
|
||||||
|
primary location for collaboration. Please read our
|
||||||
|
[Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,11 @@
|
|||||||
|
|
||||||
.title-group {
|
.title-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2px;
|
gap: 6px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-buttons {
|
.header-buttons {
|
||||||
@@ -53,42 +54,21 @@
|
|||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── OS Version Badge (polished) ───────────────────────────────── */
|
/* ── OS Version Badge — identical styling to the version badge ────
|
||||||
|
── shown next to titles in the service modal windows ──────────── */
|
||||||
.os-version-badge {
|
.os-version-badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.06);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: linear-gradient(145deg, #1a2a22, #0f1a15);
|
|
||||||
border: 1px solid rgba(74, 222, 128, 0.25);
|
|
||||||
border-radius: 9999px;
|
|
||||||
padding: 2px 14px 2px 10px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4ade80;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-self: flex-start;
|
flex-shrink: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.os-version-badge:hover {
|
|
||||||
border-color: rgba(74, 222, 128, 0.45);
|
|
||||||
box-shadow: 0 4px 8px rgba(74, 222, 128, 0.15);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.os-version-badge .version-label {
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #4ade80;
|
|
||||||
opacity: 0.85;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.os-version-badge .version-number {
|
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
color: #86efac;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── IP bar ─────────────────────────────────────────────────────── */
|
/* ── IP bar ─────────────────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -25,11 +25,8 @@
|
|||||||
<div class="title-group">
|
<div class="title-group">
|
||||||
<span class="title">Sovran_SystemsOS Hub</span>
|
<span class="title">Sovran_SystemsOS Hub</span>
|
||||||
|
|
||||||
<!-- OS Version Badge -->
|
<!-- OS Version Badge — styled identically to the version badge in service modals -->
|
||||||
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">
|
<span class="os-version-badge" id="os-version-badge" title="Sovran_SystemsOS Version">v{{ sovran_version }}</span>
|
||||||
<span class="version-label">v</span>
|
|
||||||
<span class="version-number" id="version-number">{{ sovran_version }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
|
|||||||
+194
-31
@@ -103,25 +103,81 @@ def human_size(nbytes):
|
|||||||
return f"{nbytes:.1f} PB"
|
return f"{nbytes:.1f} PB"
|
||||||
|
|
||||||
def check_internet():
|
def check_internet():
|
||||||
"""Return True if the machine can reach the internet."""
|
"""Return True if the machine can reach the internet.
|
||||||
|
|
||||||
|
Some VM NAT setups and managed networks block ICMP even when HTTPS works,
|
||||||
|
so keep the existing ping checks but fall back to small HTTPS requests.
|
||||||
|
"""
|
||||||
|
checks = [
|
||||||
|
["ping", "-c", "1", "-W", "5", "nixos.org"],
|
||||||
|
["curl", "--location", "--fail", "--silent", "--show-error", "--connect-timeout", "5", "--max-time", "10", "--output", "/dev/null", "https://cache.nixos.org/nix-cache-info"],
|
||||||
|
["curl", "--location", "--fail", "--silent", "--show-error", "--connect-timeout", "5", "--max-time", "10", "--output", "/dev/null", "https://nixos.org/"],
|
||||||
|
["ping", "-c", "1", "-W", "5", "1.1.1.1"],
|
||||||
|
]
|
||||||
|
|
||||||
|
for cmd in checks:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True
|
||||||
|
log(f"Connectivity check failed ({' '.join(cmd)}): {result.stderr.strip() or result.stdout.strip()}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Connectivity check failed ({' '.join(cmd)}): {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def detect_virtualization():
|
||||||
|
"""Return the VM technology name when running inside a VM, else None."""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["ping", "-c", "1", "-W", "5", "nixos.org"],
|
["systemd-detect-virt", "--vm"],
|
||||||
capture_output=True, text=True
|
capture_output=True, text=True
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
return True
|
name = result.stdout.strip()
|
||||||
|
if name == "none":
|
||||||
|
return None
|
||||||
|
return name or "virtual machine"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Fallback: try a second host in case DNS for nixos.org is down
|
|
||||||
try:
|
# Fallback for minimal environments where systemd-detect-virt is absent.
|
||||||
result = subprocess.run(
|
dmi_paths = [
|
||||||
["ping", "-c", "1", "-W", "5", "1.1.1.1"],
|
"/sys/class/dmi/id/product_name",
|
||||||
capture_output=True, text=True
|
"/sys/class/dmi/id/sys_vendor",
|
||||||
)
|
"/sys/class/dmi/id/board_vendor",
|
||||||
return result.returncode == 0
|
]
|
||||||
except Exception:
|
markers = (
|
||||||
return False
|
"kvm", "qemu", "virtualbox", "vmware", "hyper-v",
|
||||||
|
"bhyve", "parallels", "xen", "bochs", "virtual",
|
||||||
|
)
|
||||||
|
for dmi_path in dmi_paths:
|
||||||
|
try:
|
||||||
|
value = open(dmi_path, "r", encoding="utf-8", errors="ignore").read().strip()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
lowered = value.lower()
|
||||||
|
if any(marker in lowered for marker in markers):
|
||||||
|
return value or "virtual machine"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def detect_boot_mode():
|
||||||
|
"""Return the firmware mode used to boot the installer ISO."""
|
||||||
|
return "uefi" if os.path.isdir("/sys/firmware/efi") else "bios"
|
||||||
|
|
||||||
|
def boot_mode_label(mode):
|
||||||
|
if mode == "uefi":
|
||||||
|
return "UEFI (systemd-boot)"
|
||||||
|
return "Legacy BIOS (GRUB)"
|
||||||
|
|
||||||
|
def nix_string(value):
|
||||||
|
"""Quote a Python string for a simple Nix string literal."""
|
||||||
|
escaped = value.replace("\\", "\\\\")
|
||||||
|
escaped = escaped.replace('"', '\\"')
|
||||||
|
escaped = escaped.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
|
||||||
|
escaped = escaped.replace("${", "\\${")
|
||||||
|
return f'"{escaped}"'
|
||||||
|
|
||||||
def symbolic_icon(name):
|
def symbolic_icon(name):
|
||||||
"""Create a crisp symbolic icon suitable for use as an ActionRow prefix."""
|
"""Create a crisp symbolic icon suitable for use as an ActionRow prefix."""
|
||||||
@@ -159,6 +215,10 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
self.data_size = None
|
self.data_size = None
|
||||||
self.data_drive_has_timechain = False
|
self.data_drive_has_timechain = False
|
||||||
self.free_password = None
|
self.free_password = None
|
||||||
|
self.virtualization = detect_virtualization()
|
||||||
|
self.boot_mode = detect_boot_mode()
|
||||||
|
|
||||||
|
log(f"Installer environment: boot_mode={self.boot_mode}, virtualization={self.virtualization or 'none'}")
|
||||||
|
|
||||||
# Root navigation view
|
# Root navigation view
|
||||||
self.nav = Adw.NavigationView()
|
self.nav = Adw.NavigationView()
|
||||||
@@ -343,7 +403,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if os.path.exists(LOGO):
|
if os.path.exists(LOGO):
|
||||||
try:
|
try:
|
||||||
img = Gtk.Image.new_from_file(LOGO)
|
img = Gtk.Image.new_from_file(LOGO)
|
||||||
img.set_pixel_size(480)
|
img.set_pixel_size(320 if self.virtualization else 480)
|
||||||
hero.append(img)
|
hero.append(img)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -397,6 +457,42 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
notice_frame.set_child(notice_box)
|
notice_frame.set_child(notice_box)
|
||||||
outer.append(notice_frame)
|
outer.append(notice_frame)
|
||||||
|
|
||||||
|
if self.virtualization:
|
||||||
|
vm_frame = Gtk.Frame()
|
||||||
|
vm_frame.add_css_class("card")
|
||||||
|
vm_frame.set_margin_start(40)
|
||||||
|
vm_frame.set_margin_end(40)
|
||||||
|
vm_frame.set_margin_top(12)
|
||||||
|
vm_frame.set_margin_bottom(4)
|
||||||
|
|
||||||
|
vm_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
|
vm_box.set_margin_top(12)
|
||||||
|
vm_box.set_margin_bottom(12)
|
||||||
|
vm_box.set_margin_start(16)
|
||||||
|
vm_box.set_margin_end(16)
|
||||||
|
|
||||||
|
vm_icon = symbolic_icon("computer-symbolic")
|
||||||
|
vm_icon.set_valign(Gtk.Align.START)
|
||||||
|
vm_box.append(vm_icon)
|
||||||
|
|
||||||
|
vm_lbl = Gtk.Label()
|
||||||
|
vm_lbl.set_use_markup(True)
|
||||||
|
vm_lbl.set_wrap(True)
|
||||||
|
vm_lbl.set_xalign(0)
|
||||||
|
vm_lbl.set_halign(Gtk.Align.FILL)
|
||||||
|
vm_lbl.set_markup(
|
||||||
|
f"<span weight='bold'>Virtual machine detected: {GLib.markup_escape_text(self.virtualization)}</span>\n"
|
||||||
|
f"Boot mode: <span weight='bold'>{boot_mode_label(self.boot_mode)}</span>. "
|
||||||
|
"For the smoothest VM test, use 8+ GB RAM, NAT/bridged networking, "
|
||||||
|
"and a thin-provisioned virtual OS disk of at least 256 GB. "
|
||||||
|
"For Node or Server + Desktop, attach a second 2 TB virtual data disk; "
|
||||||
|
"otherwise choose Desktop Only."
|
||||||
|
)
|
||||||
|
vm_box.append(vm_lbl)
|
||||||
|
|
||||||
|
vm_frame.set_child(vm_box)
|
||||||
|
outer.append(vm_frame)
|
||||||
|
|
||||||
# Role label
|
# Role label
|
||||||
role_lbl = Gtk.Label()
|
role_lbl = Gtk.Label()
|
||||||
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
||||||
@@ -448,7 +544,10 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
available = has_second_drive or key not in NEEDS_DATA_DRIVE
|
available = has_second_drive or key not in NEEDS_DATA_DRIVE
|
||||||
if not available:
|
if not available:
|
||||||
card.set_subtitle(desc + "\n⚠ Requires a second internal drive (not detected)")
|
drive_hint = "⚠ Requires a second internal drive (not detected)"
|
||||||
|
if self.virtualization:
|
||||||
|
drive_hint += " — in a VM, attach a second virtual disk or choose Desktop Only"
|
||||||
|
card.set_subtitle(desc + f"\n{drive_hint}")
|
||||||
card.set_sensitive(False)
|
card.set_sensitive(False)
|
||||||
else:
|
else:
|
||||||
card.set_subtitle(desc)
|
card.set_subtitle(desc)
|
||||||
@@ -531,9 +630,10 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
# ── OS Drive group ────────────────────────────────────────────
|
# ── OS Drive group ────────────────────────────────────────────
|
||||||
os_group = Adw.PreferencesGroup()
|
os_group = Adw.PreferencesGroup()
|
||||||
os_group.set_title("OS Drive (NixOS Boot + Root)")
|
os_group.set_title("OS Drive (NixOS Boot + Root)")
|
||||||
os_group.set_description(
|
os_description = "Choose the drive for the NixOS installation. Minimum 256 GB required."
|
||||||
"Choose the drive for the NixOS installation. Minimum 256 GB required."
|
if self.virtualization:
|
||||||
)
|
os_description += " In a VM, a thin-provisioned virtual disk is OK."
|
||||||
|
os_group.set_description(os_description)
|
||||||
os_group.set_margin_top(24)
|
os_group.set_margin_top(24)
|
||||||
os_group.set_margin_start(40)
|
os_group.set_margin_start(40)
|
||||||
os_group.set_margin_end(40)
|
os_group.set_margin_end(40)
|
||||||
@@ -546,6 +646,8 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
row.set_title(f"/dev/{name}")
|
row.set_title(f"/dev/{name}")
|
||||||
type_label = tran.upper() if tran else "Disk"
|
type_label = tran.upper() if tran else "Disk"
|
||||||
meets = "✓ Meets 256 GB minimum" if size >= BYTES_256GB else "✗ Below 256 GB minimum"
|
meets = "✓ Meets 256 GB minimum" if size >= BYTES_256GB else "✗ Below 256 GB minimum"
|
||||||
|
if self.virtualization and size < BYTES_256GB:
|
||||||
|
meets += " (expand the virtual disk)"
|
||||||
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
|
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
|
||||||
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||||
|
|
||||||
@@ -570,11 +672,14 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if self.role != "Desktop Only":
|
if self.role != "Desktop Only":
|
||||||
data_group = Adw.PreferencesGroup()
|
data_group = Adw.PreferencesGroup()
|
||||||
data_group.set_title("Bitcoin Timechain & Backups Drive")
|
data_group.set_title("Bitcoin Timechain & Backups Drive")
|
||||||
data_group.set_description(
|
data_description = (
|
||||||
"💡 Tip: Always assign your LARGEST drive here. "
|
"💡 Tip: Always assign your LARGEST drive here. "
|
||||||
"The full Bitcoin timechain is over 700 GB and grows continuously — "
|
"The full Bitcoin timechain is over 700 GB and grows continuously — "
|
||||||
"a 2 TB or larger drive is required."
|
"a 2 TB or larger drive is required."
|
||||||
)
|
)
|
||||||
|
if self.virtualization:
|
||||||
|
data_description += " In a VM, attach a second 2 TB thin-provisioned virtual disk for Node or Server + Desktop."
|
||||||
|
data_group.set_description(data_description)
|
||||||
data_group.set_margin_top(20)
|
data_group.set_margin_top(20)
|
||||||
data_group.set_margin_start(40)
|
data_group.set_margin_start(40)
|
||||||
data_group.set_margin_end(40)
|
data_group.set_margin_end(40)
|
||||||
@@ -599,6 +704,8 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
row.set_title(f"/dev/{name}")
|
row.set_title(f"/dev/{name}")
|
||||||
type_label = tran.upper() if tran else "Disk"
|
type_label = tran.upper() if tran else "Disk"
|
||||||
meets = "✓ Meets 2 TB minimum" if size >= BYTES_2TB else "✗ Below 2 TB minimum"
|
meets = "✓ Meets 2 TB minimum" if size >= BYTES_2TB else "✗ Below 2 TB minimum"
|
||||||
|
if self.virtualization and size < BYTES_2TB:
|
||||||
|
meets += " (use a 2 TB thin-provisioned virtual disk)"
|
||||||
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
|
row.set_subtitle(f"{human_size(size)} · {type_label} — {meets}")
|
||||||
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
row.add_prefix(symbolic_icon("drive-harddisk-symbolic"))
|
||||||
|
|
||||||
@@ -655,10 +762,16 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
dlg = Adw.MessageDialog()
|
dlg = Adw.MessageDialog()
|
||||||
dlg.set_transient_for(self)
|
dlg.set_transient_for(self)
|
||||||
dlg.set_heading("OS Drive Too Small")
|
dlg.set_heading("OS Drive Too Small")
|
||||||
dlg.set_body(
|
body = (
|
||||||
f"The selected OS drive (/dev/{os_name}, {human_size(os_size)}) "
|
f"The selected OS drive (/dev/{os_name}, {human_size(os_size)}) "
|
||||||
f"does not meet the 256 GB minimum. Please choose a larger drive."
|
f"does not meet the 256 GB minimum. Please choose a larger drive."
|
||||||
)
|
)
|
||||||
|
if self.virtualization:
|
||||||
|
body += (
|
||||||
|
"\n\nVM tip: expand the virtual OS disk to at least 256 GB. "
|
||||||
|
"Thin-provisioned disks usually do not consume the full size immediately."
|
||||||
|
)
|
||||||
|
dlg.set_body(body)
|
||||||
dlg.add_response("ok", "OK")
|
dlg.add_response("ok", "OK")
|
||||||
dlg.present()
|
dlg.present()
|
||||||
return
|
return
|
||||||
@@ -668,12 +781,18 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
dlg = Adw.MessageDialog()
|
dlg = Adw.MessageDialog()
|
||||||
dlg.set_transient_for(self)
|
dlg.set_transient_for(self)
|
||||||
dlg.set_heading("Bitcoin Drive Too Small")
|
dlg.set_heading("Bitcoin Drive Too Small")
|
||||||
dlg.set_body(
|
body = (
|
||||||
f"The selected Bitcoin Timechain & Backups drive "
|
f"The selected Bitcoin Timechain & Backups drive "
|
||||||
f"(/dev/{data_name}, {human_size(data_size)}) "
|
f"(/dev/{data_name}, {human_size(data_size)}) "
|
||||||
f"does not meet the 2 TB minimum. "
|
f"does not meet the 2 TB minimum. "
|
||||||
f"Please choose a larger drive or select \"None\"."
|
f"Please choose a larger drive or select \"None\"."
|
||||||
)
|
)
|
||||||
|
if self.virtualization:
|
||||||
|
body += (
|
||||||
|
"\n\nVM tip: attach a second 2 TB thin-provisioned virtual disk "
|
||||||
|
"for Node or Server + Desktop test installs."
|
||||||
|
)
|
||||||
|
dlg.set_body(body)
|
||||||
dlg.add_response("ok", "OK")
|
dlg.add_response("ok", "OK")
|
||||||
dlg.present()
|
dlg.present()
|
||||||
return
|
return
|
||||||
@@ -829,7 +948,12 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
# ── Worker: partition ─────────────────────────────────────────────────
|
# ── Worker: partition ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def partition_path(self, dev_path, num):
|
def partition_path(self, dev_path, num):
|
||||||
return f"{dev_path}p{num}" if "nvme" in dev_path else f"{dev_path}{num}"
|
base = os.path.basename(dev_path)
|
||||||
|
needs_p = (
|
||||||
|
base.startswith(("nvme", "mmcblk", "loop", "md")) or
|
||||||
|
(base[-1:].isdigit())
|
||||||
|
)
|
||||||
|
return f"{dev_path}p{num}" if needs_p else f"{dev_path}{num}"
|
||||||
|
|
||||||
def detect_existing_timechain_data(self, data_path, buf=None):
|
def detect_existing_timechain_data(self, data_path, buf=None):
|
||||||
data_p1 = self.partition_path(data_path, 1)
|
data_p1 = self.partition_path(data_path, 1)
|
||||||
@@ -900,12 +1024,19 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# ── Partition boot disk: 512M ESP + rest as root ──
|
# ── Partition boot disk ──
|
||||||
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk ===\n")
|
if self.boot_mode == "uefi":
|
||||||
run_stream(["sudo", "sgdisk",
|
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk (UEFI) ===\n")
|
||||||
"-n", "1:1M:+512M", "-t", "1:EF00", "-c", "1:ESP",
|
run_stream(["sudo", "sgdisk",
|
||||||
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
|
"-n", "1:1M:+512M", "-t", "1:EF00", "-c", "1:ESP",
|
||||||
boot_path], buf)
|
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
|
||||||
|
boot_path], buf)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(append_text, buf, "\n=== Partitioning boot disk (Legacy BIOS) ===\n")
|
||||||
|
run_stream(["sudo", "sgdisk",
|
||||||
|
"-n", "1:1M:+1M", "-t", "1:EF02", "-c", "1:BIOS-boot",
|
||||||
|
"-n", "2:0:0", "-t", "2:8300", "-c", "2:root",
|
||||||
|
boot_path], buf)
|
||||||
|
|
||||||
run_stream(["sudo", "partprobe", boot_path], buf)
|
run_stream(["sudo", "partprobe", boot_path], buf)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -925,7 +1056,10 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
boot_p1 = self.partition_path(boot_path, 1)
|
boot_p1 = self.partition_path(boot_path, 1)
|
||||||
boot_p2 = self.partition_path(boot_path, 2)
|
boot_p2 = self.partition_path(boot_path, 2)
|
||||||
|
|
||||||
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
|
if self.boot_mode == "uefi":
|
||||||
|
run_stream(["sudo", "mkfs.vfat", "-F", "32", boot_p1], buf)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(append_text, buf, "Skipping ESP format for Legacy BIOS install.\n")
|
||||||
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
|
run_stream(["sudo", "mkfs.ext4", "-F", "-L", "sovran_systemsos", boot_p2], buf)
|
||||||
|
|
||||||
if data_path and not self.data_drive_has_timechain:
|
if data_path and not self.data_drive_has_timechain:
|
||||||
@@ -935,8 +1069,11 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
# ── Mount filesystems ──
|
# ── Mount filesystems ──
|
||||||
GLib.idle_add(append_text, buf, "\n=== Mounting filesystems ===\n")
|
GLib.idle_add(append_text, buf, "\n=== Mounting filesystems ===\n")
|
||||||
run_stream(["sudo", "mount", boot_p2, "/mnt"], buf)
|
run_stream(["sudo", "mount", boot_p2, "/mnt"], buf)
|
||||||
run_stream(["sudo", "mkdir", "-p", "/mnt/boot/efi"], buf)
|
if self.boot_mode == "uefi":
|
||||||
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
|
run_stream(["sudo", "mkdir", "-p", "/mnt/boot/efi"], buf)
|
||||||
|
run_stream(["sudo", "mount", "-o", "umask=0077,defaults", boot_p1, "/mnt/boot/efi"], buf)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(append_text, buf, "Legacy BIOS install: /boot/efi mount not required.\n")
|
||||||
|
|
||||||
if data_path:
|
if data_path:
|
||||||
data_p1 = self.partition_path(data_path, 1)
|
data_p1 = self.partition_path(data_path, 1)
|
||||||
@@ -966,12 +1103,33 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
is_server = str(self.role == "Server+Desktop").lower()
|
is_server = str(self.role == "Server+Desktop").lower()
|
||||||
is_desktop = str(self.role == "Desktop Only").lower()
|
is_desktop = str(self.role == "Desktop Only").lower()
|
||||||
is_node = str(self.role == "Node (Bitcoin-only)").lower()
|
is_node = str(self.role == "Node (Bitcoin-only)").lower()
|
||||||
|
boot_overrides = ""
|
||||||
|
if self.boot_mode == "bios":
|
||||||
|
grub_device = nix_string(f"/dev/{self.boot_disk}")
|
||||||
|
boot_overrides = f"""
|
||||||
|
|
||||||
|
# The installer ISO was booted in Legacy BIOS mode, so install GRUB to the
|
||||||
|
# target disk instead of using the default UEFI systemd-boot configuration.
|
||||||
|
boot.loader.systemd-boot.enable = lib.mkForce false;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
||||||
|
boot.loader.grub.enable = lib.mkForce true;
|
||||||
|
boot.loader.grub.device = lib.mkForce {grub_device};
|
||||||
|
fileSystems."/boot/efi".enable = lib.mkForce false;
|
||||||
|
"""
|
||||||
|
elif self.virtualization:
|
||||||
|
boot_overrides = """
|
||||||
|
|
||||||
|
# VM UEFI firmware can reject or forget NVRAM boot-entry writes. Keep the
|
||||||
|
# normal UEFI systemd-boot layout, but use the fallback removable path instead
|
||||||
|
# of depending on EFI variable updates.
|
||||||
|
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
||||||
|
"""
|
||||||
content = f"""# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
|
content = f"""# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
|
||||||
{{ config, lib, ... }}:
|
{{ config, lib, ... }}:
|
||||||
{{
|
{{
|
||||||
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault {is_server};
|
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault {is_server};
|
||||||
sovran_systemsOS.roles.desktop = lib.mkDefault {is_desktop};
|
sovran_systemsOS.roles.desktop = lib.mkDefault {is_desktop};
|
||||||
sovran_systemsOS.roles.node = lib.mkDefault {is_node};
|
sovran_systemsOS.roles.node = lib.mkDefault {is_node};{boot_overrides}
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
@@ -1008,6 +1166,11 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}")
|
boot_row.set_subtitle(f"/dev/{self.boot_disk} — {human_size(self.boot_size)}")
|
||||||
details.add(boot_row)
|
details.add(boot_row)
|
||||||
|
|
||||||
|
boot_mode_row = Adw.ActionRow()
|
||||||
|
boot_mode_row.set_title("Boot Mode")
|
||||||
|
boot_mode_row.set_subtitle(boot_mode_label(self.boot_mode))
|
||||||
|
details.add(boot_mode_row)
|
||||||
|
|
||||||
if self.data_disk:
|
if self.data_disk:
|
||||||
data_row = Adw.ActionRow()
|
data_row = Adw.ActionRow()
|
||||||
data_row.set_title("Data Disk")
|
data_row.set_title("Data Disk")
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ Options:
|
|||||||
--deploy-key "ssh-ed25519 AAAA..." SSH pubkey for remote access after install
|
--deploy-key "ssh-ed25519 AAAA..." SSH pubkey for remote access after install
|
||||||
--headscale-server URL Headscale login server for post-install Tailnet
|
--headscale-server URL Headscale login server for post-install Tailnet
|
||||||
--headscale-key KEY Headscale pre-auth key for the installed OS
|
--headscale-key KEY Headscale pre-auth key for the installed OS
|
||||||
|
|
||||||
|
VM notes:
|
||||||
|
Desktop test installs still need a 256 GB OS disk (thin-provisioned is OK).
|
||||||
|
Node/server installs need a separate 2 TB data disk. UEFI is preferred; UEFI
|
||||||
|
VMs avoid NVRAM boot-entry writes, and legacy BIOS VMs use GRUB.
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +30,8 @@ DEPLOY_KEY=""
|
|||||||
HEADSCALE_SERVER=""
|
HEADSCALE_SERVER=""
|
||||||
HEADSCALE_KEY=""
|
HEADSCALE_KEY=""
|
||||||
DATA_DISK_HAS_TIMECHAIN=false
|
DATA_DISK_HAS_TIMECHAIN=false
|
||||||
|
BOOT_MODE=""
|
||||||
|
VIRTUALIZATION=""
|
||||||
|
|
||||||
FLAKE="/etc/sovran/flake"
|
FLAKE="/etc/sovran/flake"
|
||||||
LOG="/tmp/sovran-headless-install.log"
|
LOG="/tmp/sovran-headless-install.log"
|
||||||
@@ -69,6 +76,22 @@ case "$ROLE" in
|
|||||||
*) die "--role must be one of: server, desktop, node" ;;
|
*) die "--role must be one of: server, desktop, node" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# ── Detect firmware / VM environment ─────────────────────────────────────────
|
||||||
|
if [[ -d /sys/firmware/efi ]]; then
|
||||||
|
BOOT_MODE="uefi"
|
||||||
|
else
|
||||||
|
BOOT_MODE="bios"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemd-detect-virt >/dev/null 2>&1; then
|
||||||
|
if VIRT_RESULT=$(systemd-detect-virt --vm 2>/dev/null); then
|
||||||
|
[[ "$VIRT_RESULT" != "none" ]] && VIRTUALIZATION="$VIRT_RESULT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Boot mode: ${BOOT_MODE} ($([[ "$BOOT_MODE" == uefi ]] && echo systemd-boot || echo GRUB))"
|
||||||
|
[[ -n "$VIRTUALIZATION" ]] && log "Virtual machine detected: ${VIRTUALIZATION}"
|
||||||
|
|
||||||
# ── Validate disk existence and size ─────────────────────────────────────────
|
# ── Validate disk existence and size ─────────────────────────────────────────
|
||||||
log "=== Validating disks ==="
|
log "=== Validating disks ==="
|
||||||
|
|
||||||
@@ -96,13 +119,26 @@ fi
|
|||||||
# ── Helper: partition suffix ──────────────────────────────────────────────────
|
# ── Helper: partition suffix ──────────────────────────────────────────────────
|
||||||
part_suffix() {
|
part_suffix() {
|
||||||
local dev="$1" n="$2"
|
local dev="$1" n="$2"
|
||||||
if [[ "$dev" == *nvme* ]]; then
|
local base
|
||||||
|
base=$(basename "$dev")
|
||||||
|
if [[ "$base" == nvme* || "$base" == mmcblk* || "$base" == loop* || "$base" == md* || "$base" =~ [0-9]$ ]]; then
|
||||||
echo "${dev}p${n}"
|
echo "${dev}p${n}"
|
||||||
else
|
else
|
||||||
echo "${dev}${n}"
|
echo "${dev}${n}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nix_string() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value//\\/\\\\}"
|
||||||
|
value="${value//\"/\\\"}"
|
||||||
|
value="${value//$'\n'/\\n}"
|
||||||
|
value="${value//$'\r'/\\r}"
|
||||||
|
value="${value//$'\t'/\\t}"
|
||||||
|
value="${value//\$\{/\\\$\{}"
|
||||||
|
printf '"%s"' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
# ── Detect existing Bitcoin timechain data on data disk ───────────────────────
|
# ── Detect existing Bitcoin timechain data on data disk ───────────────────────
|
||||||
if [[ -n "$DATA_DISK" ]]; then
|
if [[ -n "$DATA_DISK" ]]; then
|
||||||
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
||||||
@@ -142,12 +178,19 @@ partprobe "$DISK"
|
|||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# ── Step 2: Partition OS disk ─────────────────────────────────────────────────
|
# ── Step 2: Partition OS disk ─────────────────────────────────────────────────
|
||||||
log "=== Partitioning OS disk ==="
|
if [[ "$BOOT_MODE" == "uefi" ]]; then
|
||||||
|
log "=== Partitioning OS disk (UEFI) ==="
|
||||||
sgdisk \
|
sgdisk \
|
||||||
-n "1:1M:+512M" -t "1:EF00" -c "1:ESP" \
|
-n "1:1M:+512M" -t "1:EF00" -c "1:ESP" \
|
||||||
-n "2:0:0" -t "2:8300" -c "2:root" \
|
-n "2:0:0" -t "2:8300" -c "2:root" \
|
||||||
"$DISK"
|
"$DISK"
|
||||||
|
else
|
||||||
|
log "=== Partitioning OS disk (Legacy BIOS) ==="
|
||||||
|
sgdisk \
|
||||||
|
-n "1:1M:+1M" -t "1:EF02" -c "1:BIOS-boot" \
|
||||||
|
-n "2:0:0" -t "2:8300" -c "2:root" \
|
||||||
|
"$DISK"
|
||||||
|
fi
|
||||||
|
|
||||||
partprobe "$DISK"
|
partprobe "$DISK"
|
||||||
sleep 2
|
sleep 2
|
||||||
@@ -168,7 +211,11 @@ log "=== Formatting partitions ==="
|
|||||||
BOOT_P1=$(part_suffix "$DISK" 1)
|
BOOT_P1=$(part_suffix "$DISK" 1)
|
||||||
BOOT_P2=$(part_suffix "$DISK" 2)
|
BOOT_P2=$(part_suffix "$DISK" 2)
|
||||||
|
|
||||||
mkfs.vfat -F 32 "$BOOT_P1"
|
if [[ "$BOOT_MODE" == "uefi" ]]; then
|
||||||
|
mkfs.vfat -F 32 "$BOOT_P1"
|
||||||
|
else
|
||||||
|
log "Skipping ESP format for Legacy BIOS install"
|
||||||
|
fi
|
||||||
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
|
mkfs.ext4 -F -L sovran_systemsos "$BOOT_P2"
|
||||||
|
|
||||||
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
|
if [[ -n "$DATA_DISK" && "$DATA_DISK_HAS_TIMECHAIN" != true ]]; then
|
||||||
@@ -180,8 +227,12 @@ fi
|
|||||||
log "=== Mounting filesystems ==="
|
log "=== Mounting filesystems ==="
|
||||||
|
|
||||||
mount "$BOOT_P2" /mnt
|
mount "$BOOT_P2" /mnt
|
||||||
mkdir -p /mnt/boot/efi
|
if [[ "$BOOT_MODE" == "uefi" ]]; then
|
||||||
mount -o umask=0077,defaults "$BOOT_P1" /mnt/boot/efi
|
mkdir -p /mnt/boot/efi
|
||||||
|
mount -o umask=0077,defaults "$BOOT_P1" /mnt/boot/efi
|
||||||
|
else
|
||||||
|
log "Legacy BIOS install: /boot/efi mount not required"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$DATA_DISK" ]]; then
|
if [[ -n "$DATA_DISK" ]]; then
|
||||||
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
DATA_P1=$(part_suffix "$DATA_DISK" 1)
|
||||||
@@ -219,16 +270,41 @@ case "$ROLE" in
|
|||||||
IS_SERVER=false; IS_DESKTOP=false; IS_NODE=true ;;
|
IS_SERVER=false; IS_DESKTOP=false; IS_NODE=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
cat > /mnt/etc/nixos/role-state.nix <<EOF
|
{
|
||||||
|
cat <<EOF
|
||||||
# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
|
# THIS FILE IS AUTO-GENERATED BY THE INSTALLER. DO NOT EDIT.
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault ${IS_SERVER};
|
sovran_systemsOS.roles.server_plus_desktop = lib.mkDefault ${IS_SERVER};
|
||||||
sovran_systemsOS.roles.desktop = lib.mkDefault ${IS_DESKTOP};
|
sovran_systemsOS.roles.desktop = lib.mkDefault ${IS_DESKTOP};
|
||||||
sovran_systemsOS.roles.node = lib.mkDefault ${IS_NODE};
|
sovran_systemsOS.roles.node = lib.mkDefault ${IS_NODE};
|
||||||
}
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
if [[ "$BOOT_MODE" == "bios" ]]; then
|
||||||
|
GRUB_DEVICE=$(nix_string "$DISK")
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
# The installer ISO was booted in Legacy BIOS mode, so install GRUB to the
|
||||||
|
# target disk instead of using the default UEFI systemd-boot configuration.
|
||||||
|
boot.loader.systemd-boot.enable = lib.mkForce false;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
||||||
|
boot.loader.grub.enable = lib.mkForce true;
|
||||||
|
boot.loader.grub.device = lib.mkForce ${GRUB_DEVICE};
|
||||||
|
fileSystems."/boot/efi".enable = lib.mkForce false;
|
||||||
|
EOF
|
||||||
|
elif [[ -n "$VIRTUALIZATION" ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
# VM UEFI firmware can reject or forget NVRAM boot-entry writes. Keep the
|
||||||
|
# normal UEFI systemd-boot layout, but use the fallback removable path instead
|
||||||
|
# of depending on EFI variable updates.
|
||||||
|
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}"
|
||||||
|
} > /mnt/etc/nixos/role-state.nix
|
||||||
|
|
||||||
# ── Step 10: Write custom.nix with deploy config ──────────────────────────────
|
# ── Step 10: Write custom.nix with deploy config ──────────────────────────────
|
||||||
log "=== Writing custom.nix ==="
|
log "=== Writing custom.nix ==="
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user