diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7a7f3..977276f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,21 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Deprecated ### Fixed +## [0.66.0] - 2025-11-11 + +### Added +- Pre-installation checks for minimum system resources (10GB disk space, 2GB RAM, 2 CPU cores) are now performed during setup. +- All systemd services (IPFS, RQLite, Olric, Node, Gateway) now log directly to dedicated files in the logs directory instead of using the system journal. + +### Changed +- Improved logging instructions in the setup completion message to reference the new dedicated log files. + +### Deprecated + +### Removed + +### Fixed +\n ## [0.65.0] - 2025-11-11 ### Added diff --git a/Makefile b/Makefile index 539397c..3b2c48c 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test-e2e: .PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill -VERSION := 0.65.0 +VERSION := 0.66.0 COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' diff --git a/pkg/environments/production/checks.go b/pkg/environments/production/checks.go index 7b5a942..3ed64ae 100644 --- a/pkg/environments/production/checks.go +++ b/pkg/environments/production/checks.go @@ -5,7 +5,9 @@ import ( "os" "os/exec" "runtime" + "strconv" "strings" + "syscall" ) // OSInfo contains detected operating system information @@ -213,3 +215,74 @@ func (etc *ExternalToolChecker) CheckGoAvailable() bool { _, err := exec.LookPath("go") return err == nil } + +// ResourceChecker validates system resources for production deployment +type ResourceChecker struct{} + +// NewResourceChecker creates a new resource checker +func NewResourceChecker() *ResourceChecker { + return &ResourceChecker{} +} + +// CheckDiskSpace validates sufficient disk space (minimum 10GB free) +func (rc *ResourceChecker) CheckDiskSpace(path string) error { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil { + return fmt.Errorf("failed to check disk space: %w", err) + } + + // Available space in bytes + availableBytes := stat.Bavail * uint64(stat.Bsize) + minRequiredBytes := uint64(10 * 1024 * 1024 * 1024) // 10GB + + if availableBytes < minRequiredBytes { + availableGB := float64(availableBytes) / (1024 * 1024 * 1024) + return fmt.Errorf("insufficient disk space: %.1fGB available, minimum 10GB required", availableGB) + } + + return nil +} + +// CheckRAM validates sufficient RAM (minimum 2GB total) +func (rc *ResourceChecker) CheckRAM() error { + data, err := os.ReadFile("/proc/meminfo") + if err != nil { + return fmt.Errorf("failed to read memory info: %w", err) + } + + lines := strings.Split(string(data), "\n") + totalKB := uint64(0) + + for _, line := range lines { + if strings.HasPrefix(line, "MemTotal:") { + parts := strings.Fields(line) + if len(parts) >= 2 { + if kb, err := strconv.ParseUint(parts[1], 10, 64); err == nil { + totalKB = kb + break + } + } + } + } + + if totalKB == 0 { + return fmt.Errorf("could not determine total RAM") + } + + minRequiredKB := uint64(2 * 1024 * 1024) // 2GB in KB + if totalKB < minRequiredKB { + totalGB := float64(totalKB) / (1024 * 1024) + return fmt.Errorf("insufficient RAM: %.1fGB total, minimum 2GB required", totalGB) + } + + return nil +} + +// CheckCPU validates sufficient CPU cores (minimum 2 cores) +func (rc *ResourceChecker) CheckCPU() error { + cores := runtime.NumCPU() + if cores < 2 { + return fmt.Errorf("insufficient CPU cores: %d available, minimum 2 required", cores) + } + return nil +} diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index 90f220e..3b9403e 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -21,6 +21,7 @@ type ProductionSetup struct { privChecker *PrivilegeChecker osDetector *OSDetector archDetector *ArchitectureDetector + resourceChecker *ResourceChecker fsProvisioner *FilesystemProvisioner userProvisioner *UserProvisioner stateDetector *StateDetector @@ -48,6 +49,7 @@ func NewProductionSetup(debrosHome string, logWriter io.Writer, forceReconfigure privChecker: &PrivilegeChecker{}, osDetector: &OSDetector{}, archDetector: &ArchitectureDetector{}, + resourceChecker: NewResourceChecker(), fsProvisioner: NewFilesystemProvisioner(debrosHome), userProvisioner: NewUserProvisioner("debros", debrosHome, "/bin/bash"), stateDetector: NewStateDetector(debrosDir), @@ -115,6 +117,25 @@ func (ps *ProductionSetup) Phase1CheckPrerequisites() error { } ps.logf(" ✓ Basic dependencies available") + // Check system resources + if err := ps.resourceChecker.CheckDiskSpace(ps.debrosHome); err != nil { + ps.logf(" ❌ %v", err) + return err + } + ps.logf(" ✓ Sufficient disk space available") + + if err := ps.resourceChecker.CheckRAM(); err != nil { + ps.logf(" ❌ %v", err) + return err + } + ps.logf(" ✓ Sufficient RAM available") + + if err := ps.resourceChecker.CheckCPU(); err != nil { + ps.logf(" ❌ %v", err) + return err + } + ps.logf(" ✓ Sufficient CPU cores available") + return nil } @@ -426,8 +447,15 @@ func (ps *ProductionSetup) LogSetupComplete(peerID string) { ps.logf("\nNode Peer ID: %s", peerID) ps.logf("\nService Management:") ps.logf(" systemctl status debros-ipfs-bootstrap") - ps.logf(" systemctl logs debros-node-bootstrap") - ps.logf(" sudo tail -f %s/logs/node.log", ps.debrosDir) + ps.logf(" journalctl -u debros-node-bootstrap -f") + ps.logf(" tail -f %s/logs/node-bootstrap.log", ps.debrosDir) + ps.logf("\nLog Files:") + ps.logf(" %s/logs/ipfs-bootstrap.log", ps.debrosDir) + ps.logf(" %s/logs/ipfs-cluster-bootstrap.log", ps.debrosDir) + ps.logf(" %s/logs/rqlite-bootstrap.log", ps.debrosDir) + ps.logf(" %s/logs/olric.log", ps.debrosDir) + ps.logf(" %s/logs/node-bootstrap.log", ps.debrosDir) + ps.logf(" %s/logs/gateway.log", ps.debrosDir) ps.logf("\nStart All Services:") ps.logf(" systemctl start debros-ipfs-bootstrap debros-ipfs-cluster-bootstrap debros-rqlite-bootstrap debros-olric debros-node-bootstrap debros-gateway") ps.logf("\nVerify Installation:") diff --git a/pkg/environments/production/services.go b/pkg/environments/production/services.go index b2038ef..97a26e9 100644 --- a/pkg/environments/production/services.go +++ b/pkg/environments/production/services.go @@ -31,6 +31,8 @@ func (ssg *SystemdServiceGenerator) GenerateIPFSService(nodeType string) string ipfsRepoPath = filepath.Join(ssg.debrosDir, "data", "node", "ipfs", "repo") } + logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("ipfs-%s.log", nodeType)) + return fmt.Sprintf(`[Unit] Description=IPFS Daemon (%s) After=network-online.target @@ -46,8 +48,8 @@ ExecStartPre=/bin/bash -c 'if [ -f %s/secrets/swarm.key ] && [ ! -f %s/swarm.key ExecStart=/usr/bin/ipfs daemon --enable-pubsub-experiment --repo-dir=%s Restart=always RestartSec=5 -StandardOutput=journal -StandardError=journal +StandardOutput=file:%s +StandardError=file:%s SyslogIdentifier=ipfs-%s NoNewPrivileges=yes @@ -57,7 +59,7 @@ ReadWritePaths=%s [Install] WantedBy=multi-user.target -`, nodeType, ssg.debrosHome, ipfsRepoPath, ssg.debrosDir, ipfsRepoPath, ssg.debrosDir, ipfsRepoPath, ipfsRepoPath, ipfsRepoPath, nodeType, ssg.debrosDir) +`, nodeType, ssg.debrosHome, ipfsRepoPath, ssg.debrosDir, ipfsRepoPath, ssg.debrosDir, ipfsRepoPath, ipfsRepoPath, ipfsRepoPath, logFile, logFile, nodeType, ssg.debrosDir) } // GenerateIPFSClusterService generates the IPFS Cluster systemd unit @@ -69,6 +71,8 @@ func (ssg *SystemdServiceGenerator) GenerateIPFSClusterService(nodeType string) clusterPath = filepath.Join(ssg.debrosDir, "data", "node", "ipfs-cluster") } + logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("ipfs-cluster-%s.log", nodeType)) + return fmt.Sprintf(`[Unit] Description=IPFS Cluster Service (%s) After=debros-ipfs-%s.service @@ -85,8 +89,8 @@ Environment=IPFS_CLUSTER_PATH=%s ExecStart=/usr/local/bin/ipfs-cluster-service daemon Restart=always RestartSec=5 -StandardOutput=journal -StandardError=journal +StandardOutput=file:%s +StandardError=file:%s SyslogIdentifier=ipfs-cluster-%s NoNewPrivileges=yes @@ -96,7 +100,7 @@ ReadWritePaths=%s [Install] WantedBy=multi-user.target -`, nodeType, nodeType, nodeType, nodeType, ssg.debrosHome, ssg.debrosHome, clusterPath, nodeType, ssg.debrosDir) +`, nodeType, nodeType, nodeType, nodeType, ssg.debrosHome, ssg.debrosHome, clusterPath, logFile, logFile, nodeType, ssg.debrosDir) } // GenerateRQLiteService generates the RQLite systemd unit @@ -119,6 +123,8 @@ func (ssg *SystemdServiceGenerator) GenerateRQLiteService(nodeType string, httpP args += fmt.Sprintf(` %s`, dataDir) + logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("rqlite-%s.log", nodeType)) + return fmt.Sprintf(`[Unit] Description=RQLite Database (%s) After=network-online.target @@ -132,8 +138,8 @@ Environment=HOME=%s ExecStart=/usr/local/bin/rqlited %s Restart=always RestartSec=5 -StandardOutput=journal -StandardError=journal +StandardOutput=file:%s +StandardError=file:%s SyslogIdentifier=rqlite-%s NoNewPrivileges=yes @@ -143,12 +149,13 @@ ReadWritePaths=%s [Install] WantedBy=multi-user.target -`, nodeType, ssg.debrosHome, args, nodeType, ssg.debrosDir) +`, nodeType, ssg.debrosHome, args, logFile, logFile, nodeType, ssg.debrosDir) } // GenerateOlricService generates the Olric systemd unit func (ssg *SystemdServiceGenerator) GenerateOlricService() string { olricConfigPath := filepath.Join(ssg.debrosDir, "configs", "olric", "config.yaml") + logFile := filepath.Join(ssg.debrosDir, "logs", "olric.log") return fmt.Sprintf(`[Unit] Description=Olric Cache Server @@ -164,8 +171,8 @@ Environment=OLRIC_SERVER_CONFIG=%s ExecStart=/usr/local/bin/olric-server Restart=always RestartSec=5 -StandardOutput=journal -StandardError=journal +StandardOutput=file:%s +StandardError=file:%s SyslogIdentifier=olric NoNewPrivileges=yes @@ -175,7 +182,7 @@ ReadWritePaths=%s [Install] WantedBy=multi-user.target -`, ssg.debrosHome, olricConfigPath, ssg.debrosDir) +`, ssg.debrosHome, olricConfigPath, logFile, logFile, ssg.debrosDir) } // GenerateNodeService generates the DeBros Node systemd unit @@ -187,6 +194,8 @@ func (ssg *SystemdServiceGenerator) GenerateNodeService(nodeType string) string configFile = "node.yaml" } + logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("node-%s.log", nodeType)) + return fmt.Sprintf(`[Unit] Description=DeBros Network Node (%s) After=debros-ipfs-cluster-%s.service @@ -202,8 +211,8 @@ Environment=HOME=%s ExecStart=%s/bin/node --config %s/configs/%s Restart=always RestartSec=5 -StandardOutput=journal -StandardError=journal +StandardOutput=file:%s +StandardError=file:%s SyslogIdentifier=debros-node-%s NoNewPrivileges=yes @@ -213,12 +222,13 @@ ReadWritePaths=%s [Install] WantedBy=multi-user.target -`, nodeType, nodeType, nodeType, nodeType, ssg.debrosHome, ssg.debrosHome, ssg.debrosHome, ssg.debrosDir, configFile, nodeType, ssg.debrosDir) +`, nodeType, nodeType, nodeType, nodeType, ssg.debrosHome, ssg.debrosHome, ssg.debrosHome, ssg.debrosDir, configFile, logFile, logFile, nodeType, ssg.debrosDir) } // GenerateGatewayService generates the DeBros Gateway systemd unit func (ssg *SystemdServiceGenerator) GenerateGatewayService(nodeType string) string { nodeService := fmt.Sprintf("debros-node-%s.service", nodeType) + logFile := filepath.Join(ssg.debrosDir, "logs", "gateway.log") return fmt.Sprintf(`[Unit] Description=DeBros Gateway After=%s @@ -233,8 +243,8 @@ Environment=HOME=%s ExecStart=%s/bin/gateway --config %s/configs/gateway.yaml Restart=always RestartSec=5 -StandardOutput=journal -StandardError=journal +StandardOutput=file:%s +StandardError=file:%s SyslogIdentifier=debros-gateway AmbientCapabilities=CAP_NET_BIND_SERVICE @@ -247,7 +257,7 @@ ReadWritePaths=%s [Install] WantedBy=multi-user.target -`, nodeService, nodeService, ssg.debrosHome, ssg.debrosHome, ssg.debrosHome, ssg.debrosDir, ssg.debrosDir) +`, nodeService, nodeService, ssg.debrosHome, ssg.debrosHome, ssg.debrosHome, ssg.debrosDir, logFile, logFile, ssg.debrosDir) } // SystemdController manages systemd service operations