diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9c5ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,98 @@ +# Build output directory +target/ + +# Debian package build artifacts +*.deb +*.ddeb +*.buildinfo +*.changes +*.dsc +*.tar.gz +*.tar.xz + +# sonic-dbus-bridge build artifacts +sonic-dbus-bridge-dbgsym_*.deb +sonic-dbus-bridge_*.deb +sonic-dbus-bridge_*.buildinfo +sonic-dbus-bridge_*.changes +sonic-dbus-bridge/debian/.debhelper/ +sonic-dbus-bridge/debian/debhelper-build-stamp +sonic-dbus-bridge/debian/files +sonic-dbus-bridge/debian/*.debhelper.log +sonic-dbus-bridge/debian/*.substvars +sonic-dbus-bridge/debian/sonic-dbus-bridge/ +sonic-dbus-bridge/debian/sonic-dbus-bridge-dbgsym/ +sonic-dbus-bridge/debian/tmp/ +sonic-dbus-bridge/obj-*/ + +# sonic-dbus-bridge subprojects - ignore downloaded directories, keep .wrap files +sonic-dbus-bridge/subprojects/*/ +!sonic-dbus-bridge/subprojects/*.wrap + +# bmcweb build artifacts +bmcweb-dbg_*.deb +bmcweb_*.deb +bmcweb_*.buildinfo +bmcweb_*.changes +bmcweb/* + +# Meson build directories +build/ +builddir/ +obj-*/ +.builddir/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +*.egg-info/ +dist/ +*.egg + +# C/C++ +*.o +*.a +*.so +*.so.* +*.dylib +*.out +*.app +*.i*86 +*.x86_64 +*.hex +core + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.*.swp +.*.swo +*.bak +.DS_Store + +# Docker +.dockerignore + +# Logs +*.log +logs/ + +# Temporary files +*.tmp +*.temp +tmp/ +temp/ + +# System files +.directory +Thumbs.db + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ae2b9f --- /dev/null +++ b/Makefile @@ -0,0 +1,416 @@ +# Makefile for sonic-redfish +.ONESHELL: +SHELL = /bin/bash +.SHELLFLAGS += -e + +# Build configuration +CONFIGURED_ARCH ?= amd64 +SONIC_CONFIG_MAKE_JOBS ?= $(shell nproc) + +# Source configuration +BMCWEB_HEAD_COMMIT ?= 6926d430 +BMCWEB_REPO_URL ?= https://github.com/openbmc/bmcweb.git + +# Target directory for build artifacts +SONIC_REDFISH_TARGET ?= target/debs/trixie + +# Directories +REPO_ROOT := $(shell pwd) +BMCWEB_DIR := $(REPO_ROOT)/bmcweb +BRIDGE_DIR := $(REPO_ROOT)/sonic-dbus-bridge +PATCHES_DIR := $(REPO_ROOT)/patches +SCRIPTS_DIR := $(REPO_ROOT)/scripts +BUILD_DIR := $(REPO_ROOT)/build +TARGET_DIR := $(REPO_ROOT)/$(SONIC_REDFISH_TARGET) +SERIES_FILE := $(PATCHES_DIR)/series +DEBIAN_DIR := $(BMCWEB_DIR)/debian + +# Build artifacts +BMCWEB_BINARY := $(BMCWEB_DIR)/build/bmcweb +BRIDGE_BINARY := $(BRIDGE_DIR)/build/sonic-dbus-bridge + +# Docker configuration +DOCKER_BUILDER_IMAGE := sonic-redfish-builder:latest +DOCKERFILE_BUILD := $(BUILD_DIR)/Dockerfile.build + +# Main targets +MAIN_TARGET := $(BMCWEB_BINARY) +DERIVED_TARGETS := $(BRIDGE_BINARY) + +.PHONY: all build clean reset setup-bmcweb copy-patches apply-patches build-bmcweb build-bridge build-bmcweb-native build-bridge-native build-in-docker help + +# Default target - Build both components (via Docker) +all: build + @echo "" + @echo "=========================================" + @echo "All components built successfully!" + @echo "=========================================" + @echo "" + @echo "Build artifacts in $(SONIC_REDFISH_TARGET):" + @ls -lh $(TARGET_DIR)/*.deb 2>/dev/null || echo " No .deb files found" + +help: + @echo "sonic-redfish Build System (Docker-Only)" + @echo "=========================================" + @echo "" + @echo "Targets:" + @echo " all - Build all components (Docker-only)" + @echo " build-bmcweb - Build bmcweb only" + @echo " build-bridge - Build sonic-dbus-bridge only" + @echo " clean - Remove build artifacts (Docker-based)" + @echo " reset - Complete cleanup (clean + reset bmcweb + remove Docker images)" + @echo "" + @echo "Variables:" + @echo " SONIC_CONFIG_MAKE_JOBS - Number of parallel jobs (default: nproc)" + @echo " SONIC_REDFISH_TARGET - Target directory for build artifacts (default: target/debs/trixie)" + @echo " BMCWEB_HEAD_COMMIT - bmcweb commit to checkout (default: 6926d430)" + @echo " BMCWEB_REPO_URL - bmcweb repository URL (default: https://github.com/openbmc/bmcweb.git)" + @echo "" + @echo "Examples:" + @echo " make -f Makefile # Build with Docker (auto-clone bmcweb if needed)" + @echo " make -f Makefile clean # Clean build artifacts" + @echo " make -f Makefile reset # Complete reset" + @echo " make -f Makefile SONIC_CONFIG_MAKE_JOBS=4 # Build with 4 parallel jobs" + @echo " make -f Makefile BMCWEB_HEAD_COMMIT=abc123 # Build with specific bmcweb commit" + @echo " make -f Makefile SONIC_REDFISH_TARGET=output/debs # Use custom output directory" + @echo "" + @echo "NOTE: This build system is Docker-only for consistency with sonic-buildimage" + @echo " bmcweb will be automatically cloned if not present" + +# Build target - Always Docker +build: $(DOCKERFILE_BUILD) + @echo "=========================================" + @echo "Building sonic-redfish (Docker-only mode)" + @echo "=========================================" + @echo "" + + # Build Docker image + @echo "Building Docker build environment..." + docker build -t $(DOCKER_BUILDER_IMAGE) -f $(DOCKERFILE_BUILD) $(BUILD_DIR) + @echo " Build environment ready" + @echo "" + + # Run build inside Docker + @echo "Running build inside Docker container..." + docker run --rm \ + -v "$(REPO_ROOT):/workspace" \ + -w /workspace \ + -e SONIC_CONFIG_MAKE_JOBS=$(SONIC_CONFIG_MAKE_JOBS) \ + $(DOCKER_BUILDER_IMAGE) \ + bash -c "\ + set -e; \ + git config --global --add safe.directory /workspace; \ + git config --global --add safe.directory /workspace/bmcweb; \ + make -f Makefile build-in-docker; \ + " + + @echo "" + @echo "=========================================" + @echo "Build completed successfully!" + @echo "=========================================" + @echo "" + @echo "Build artifacts in $(SONIC_REDFISH_TARGET):" + @ls -lh $(TARGET_DIR)/*.deb 2>/dev/null || echo " No .deb files found" + +# Build inside Docker (called from Docker container) +# Note: sdbusplus is pre-installed in the Docker image +build-in-docker: setup-bmcweb apply-patches build-bridge-native build-bmcweb-native + @echo " Build inside Docker completed" + +# Setup bmcweb source +setup-bmcweb: + @echo "Checking bmcweb source..." + @if [ ! -d "$(BMCWEB_DIR)" ]; then \ + echo " bmcweb directory not found, cloning from $(BMCWEB_REPO_URL)..."; \ + git clone $(BMCWEB_REPO_URL) $(BMCWEB_DIR); \ + echo " Checking out commit $(BMCWEB_HEAD_COMMIT)..."; \ + cd $(BMCWEB_DIR) && git checkout $(BMCWEB_HEAD_COMMIT); \ + echo " bmcweb cloned and checked out to $(BMCWEB_HEAD_COMMIT)"; \ + elif [ -d "$(BMCWEB_DIR)/.git" ]; then \ + cd $(BMCWEB_DIR) && \ + current_commit=$$(git rev-parse --short HEAD 2>/dev/null || echo "unknown"); \ + if ! git diff --quiet 2>/dev/null; then \ + echo " bmcweb has local changes (patches applied), ready"; \ + else \ + echo " bmcweb source is clean at commit $$current_commit, ready for patches"; \ + fi; \ + else \ + echo " bmcweb source directory ready"; \ + fi + @echo " bmcweb ready" + +# Copy patches to debian/ directory +copy-patches: $(SERIES_FILE) + @echo "Copying patches to debian/ directory ..." + @# Note: Patches will create debian/ directory, so we only copy series file after patches are applied + @echo " Patches will be applied from $(PATCHES_DIR)" + +# Apply patches using series file +apply-patches: setup-bmcweb + @echo "Applying patches from series file..." + @if [ ! -d "$(BMCWEB_DIR)" ]; then \ + echo "Error: bmcweb directory not found"; \ + exit 1; \ + fi + + @cd $(BMCWEB_DIR) && \ + if git diff --quiet 2>/dev/null; then \ + echo " Applying patches from $(PATCHES_DIR)/series..."; \ + while IFS= read -r patch || [ -n "$$patch" ]; do \ + patch=$$(echo "$$patch" | sed 's/#.*//;s/^[[:space:]]*//;s/[[:space:]]*$$//'); \ + [ -z "$$patch" ] && continue; \ + echo " Applying: $$patch"; \ + if [ -f "$(PATCHES_DIR)/$$patch" ]; then \ + git apply "$(PATCHES_DIR)/$$patch" || { echo "Error applying $$patch"; exit 1; }; \ + else \ + echo "Error: Patch file not found: $$patch"; \ + exit 1; \ + fi; \ + done < $(PATCHES_DIR)/series; \ + echo " All patches applied successfully"; \ + else \ + echo " Patches already applied (bmcweb has local changes)"; \ + fi + +# Build bmcweb Debian package +# Dependencies: clean → setup-bmcweb → apply-patches → build-bmcweb +build-bmcweb: clean setup-bmcweb apply-patches + @echo "=========================================" + @echo "Building bmcweb Debian package" + @echo "=========================================" + @echo "" + + # Build Docker image if needed + @echo "Ensuring Docker build environment..." + @docker build -t $(DOCKER_BUILDER_IMAGE) -f $(DOCKERFILE_BUILD) $(BUILD_DIR) 2>/dev/null || true + @echo "" + + # Run dpkg-buildpackage inside Docker + @echo "Building bmcweb Debian package inside Docker..." + @mkdir -p $(TARGET_DIR) + @docker run --rm \ + -v "$(REPO_ROOT):/workspace" \ + -w /workspace/bmcweb \ + $(DOCKER_BUILDER_IMAGE) \ + bash -c "dpkg-buildpackage -us -uc -j$(SONIC_CONFIG_MAKE_JOBS)" + @echo "" + + # Copy all build artifacts to target directory + @echo "Collecting build artifacts to $(SONIC_REDFISH_TARGET)..." + @mv $(REPO_ROOT)/bmcweb_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb-dbg_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb_*.changes $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb_*.buildinfo $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb_*.dsc $(TARGET_DIR)/ 2>/dev/null || true + @echo "" + @echo "=========================================" + @echo "bmcweb build complete!" + @echo "=========================================" + @echo "Build artifacts in $(SONIC_REDFISH_TARGET):" + @ls -lh $(TARGET_DIR)/bmcweb* 2>/dev/null || echo "No artifacts found" + @echo "" + +# Build sonic-dbus-bridge Debian package +# Dependencies: clean → build-bridge +build-bridge: clean + @echo "=========================================" + @echo "Building sonic-dbus-bridge Debian package" + @echo "=========================================" + @echo "" + + # Build Docker image if needed + @echo "Ensuring Docker build environment..." + @docker build -t $(DOCKER_BUILDER_IMAGE) -f $(DOCKERFILE_BUILD) $(BUILD_DIR) 2>/dev/null || true + @echo "" + + # Build .deb package inside Docker + @echo "Building sonic-dbus-bridge .deb package in Docker..." + @docker run --rm \ + -v "$(REPO_ROOT):/workspace" \ + -w /workspace \ + -e SONIC_CONFIG_MAKE_JOBS=$(SONIC_CONFIG_MAKE_JOBS) \ + $(DOCKER_BUILDER_IMAGE) \ + bash -c "\ + set -e; \ + git config --global --add safe.directory /workspace; \ + git config --global --add safe.directory /workspace/bmcweb; \ + echo 'Installing Debian packaging tools and build dependencies...'; \ + apt-get update -qq; \ + apt-get install -y -qq debhelper devscripts build-essential fakeroot dpkg-dev libboost-dev meson; \ + echo 'Building sonic-dbus-bridge package...'; \ + cd /workspace/sonic-dbus-bridge; \ + dpkg-buildpackage -us -uc -b -j$(SONIC_CONFIG_MAKE_JOBS); \ + echo 'Package built successfully'; \ + " + + # Copy all build artifacts to target directory + @echo "" + @echo "Collecting build artifacts to $(SONIC_REDFISH_TARGET)..." + @mkdir -p $(TARGET_DIR) + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge-dbgsym_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.changes $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.buildinfo $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.dsc $(TARGET_DIR)/ 2>/dev/null || true + @echo "" + @echo "=========================================" + @echo "sonic-dbus-bridge package build complete!" + @echo "=========================================" + @echo "" + @echo "Build artifacts in $(SONIC_REDFISH_TARGET):" + @ls -lh $(TARGET_DIR)/sonic-dbus-bridge* 2>/dev/null || echo " No artifacts found" + +# Build bmcweb natively (inside Docker container, no nested Docker) +build-bmcweb-native: + @echo "=========================================" + @echo "Building bmcweb Debian package (native)" + @echo "=========================================" + @echo "" + + # Build directly with dpkg-buildpackage (no Docker) + @echo "Building bmcweb package..." + @cd $(BMCWEB_DIR) && dpkg-buildpackage -us -uc -j$(SONIC_CONFIG_MAKE_JOBS) + @echo "" + + # Copy all build artifacts to target directory + @echo "Collecting build artifacts to $(SONIC_REDFISH_TARGET)..." + @mkdir -p $(TARGET_DIR) + @mv $(REPO_ROOT)/bmcweb_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb-dbg_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb_*.changes $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb_*.buildinfo $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/bmcweb_*.dsc $(TARGET_DIR)/ 2>/dev/null || true + @echo "" + @echo "=========================================" + @echo "bmcweb build complete!" + @echo "=========================================" + @echo "Build artifacts in $(SONIC_REDFISH_TARGET):" + @ls -lh $(TARGET_DIR)/bmcweb* 2>/dev/null || echo "No artifacts found" + @echo "" + +# Build sonic-dbus-bridge natively (inside Docker container, no nested Docker) +build-bridge-native: + @echo "=========================================" + @echo "Building sonic-dbus-bridge Debian package (native)" + @echo "=========================================" + @echo "" + + # Build directly with dpkg-buildpackage (no Docker) + @echo "Building sonic-dbus-bridge package..." + @cd $(BRIDGE_DIR) && dpkg-buildpackage -us -uc -b -j$(SONIC_CONFIG_MAKE_JOBS) + @echo "" + + # Copy all build artifacts to target directory + @echo "Collecting build artifacts to $(SONIC_REDFISH_TARGET)..." + @mkdir -p $(TARGET_DIR) + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge-dbgsym_*.deb $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.changes $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.buildinfo $(TARGET_DIR)/ 2>/dev/null || true + @mv $(REPO_ROOT)/sonic-dbus-bridge_*.dsc $(TARGET_DIR)/ 2>/dev/null || true + @echo "" + @echo "=========================================" + @echo "sonic-dbus-bridge package build complete!" + @echo "=========================================" + @echo "" + @echo "Build artifacts in $(SONIC_REDFISH_TARGET):" + @ls -lh $(TARGET_DIR)/sonic-dbus-bridge* 2>/dev/null || echo " No artifacts found" + +# ======================================== +# sonic-buildimage Integration Targets +# ======================================== +# These targets are called by the sonic-buildimage build system +BMCWEB = bmcweb_$(SONIC_REDFISH_VERSION)_$(CONFIGURED_ARCH).deb +BMCWEB_DBG = bmcweb-dbg_$(SONIC_REDFISH_VERSION)_$(CONFIGURED_ARCH).deb + +# Main bmcweb package target for sonic-buildimage +$(addprefix $(DEST)/, $(BMCWEB)): $(DEST)/% : setup-bmcweb apply-patches + # Build bmcweb package using dpkg-buildpackage + pushd $(BMCWEB_DIR) + +ifeq ($(CROSS_BUILD_ENVIRON), y) + dpkg-buildpackage -b -us -uc -a$(CONFIGURED_ARCH) -Pcross,nocheck -j$(SONIC_CONFIG_MAKE_JOBS) +else + dpkg-buildpackage -b -us -uc -j$(SONIC_CONFIG_MAKE_JOBS) +endif + popd + +ifneq ($(DEST),) + mv $(BMCWEB) $(BMCWEB_DBG) $(DEST)/ +endif + +# Derived package (debug symbols) depends on main package +$(addprefix $(DEST)/, $(BMCWEB_DBG)): $(DEST)/% : $(DEST)/$(BMCWEB) + +# Clean build artifacts +clean: + @echo "=========================================" + @echo "Cleaning build artifacts..." + @echo "=========================================" + @echo "" + + # Clean root-owned files (from Docker builds) using sudo + @echo "Cleaning build directories..." + @if [ -d "$(BMCWEB_DIR)/obj-"* ] || [ -d "$(BMCWEB_DIR)/subprojects" ]; then \ + echo " Removing bmcweb build artifacts (may require sudo)..."; \ + sudo rm -rf $(BMCWEB_DIR)/obj-* 2>/dev/null || true; \ + if [ -d "$(BMCWEB_DIR)/subprojects" ]; then \ + find $(BMCWEB_DIR)/subprojects -mindepth 1 -maxdepth 1 -type d -exec sudo rm -rf {} + 2>/dev/null || true; \ + fi; \ + fi + @if [ -d "$(BRIDGE_DIR)/obj-"* ] || [ -d "$(BRIDGE_DIR)/subprojects" ] || [ -d "$(BRIDGE_DIR)/debian/.debhelper" ]; then \ + echo " Removing sonic-dbus-bridge build artifacts (may require sudo)..."; \ + sudo rm -rf $(BRIDGE_DIR)/obj-* $(BRIDGE_DIR)/debian/.debhelper $(BRIDGE_DIR)/debian/debhelper-build-stamp $(BRIDGE_DIR)/debian/files $(BRIDGE_DIR)/debian/sonic-dbus-bridge $(BRIDGE_DIR)/debian/*.log $(BRIDGE_DIR)/debian/*.substvars 2>/dev/null || true; \ + if [ -d "$(BRIDGE_DIR)/subprojects" ]; then \ + find $(BRIDGE_DIR)/subprojects -mindepth 1 -maxdepth 1 -type d -exec sudo rm -rf {} + 2>/dev/null || true; \ + fi; \ + fi + + # Clean host-owned files + @echo "Cleaning package artifacts..." + @rm -rf $(BMCWEB_DIR)/debian 2>/dev/null || true + @rm -rf $(BRIDGE_DIR)/build 2>/dev/null || true + @rm -f $(REPO_ROOT)/*.deb $(REPO_ROOT)/*.changes $(REPO_ROOT)/*.buildinfo $(REPO_ROOT)/*.dsc $(REPO_ROOT)/*.tar.gz 2>/dev/null || true + @echo " Removed package artifacts from root directory" + + # Reset bmcweb source to clean state (so patches can be reapplied) + @echo "Resetting bmcweb source to clean state..." + @if [ -d "$(BMCWEB_DIR)/.git" ]; then \ + cd $(BMCWEB_DIR) && git reset --hard HEAD 2>/dev/null || true; \ + cd $(BMCWEB_DIR) && git clean -fd 2>/dev/null || true; \ + echo " bmcweb source reset to clean state"; \ + fi + + @echo "" + @echo "Clean completed" + +# Reset - Complete cleanup including bmcweb source and Docker images +reset: clean + @echo "" + @echo "=========================================" + @echo "Resetting workspace to clean state..." + @echo "=========================================" + @echo "" + @echo "Removing Docker images..." + @docker rmi $(DOCKER_BUILDER_IMAGE) 2>/dev/null || echo " (Docker image not found, skipping)" + @echo "" + @echo "Resetting bmcweb source..." + @if [ -d "$(BMCWEB_DIR)/.git" ]; then \ + sudo rm -rf $(BMCWEB_DIR)/debian $(BMCWEB_DIR)/obj-* 2>/dev/null || true; \ + cd $(BMCWEB_DIR) && git reset --hard HEAD 2>/dev/null || true; \ + cd $(BMCWEB_DIR) && git clean -fdx 2>/dev/null || true; \ + echo " bmcweb source reset"; \ + else \ + echo " bmcweb is not a git repository, skipping"; \ + fi + @echo "" + @echo "Removing target directory..." + @sudo rm -rf target 2>/dev/null || true + @echo " Target directory removed" + @echo "" + @echo "=========================================" + @echo "Workspace reset complete!" + @echo "=========================================" + @echo "" + @echo "You can now run: make -f Makefile" + diff --git a/README.md b/README.md index cf75752..733f3cf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,509 @@ # sonic-redfish -SONiC redfish submodule + +SONiC Redfish implementation providing bmcweb and sonic-dbus-bridge as Debian packages. + +## Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Build System](#build-system) +- [Patch Management](#patch-management) +- [Cleanup Targets](#cleanup-targets) +- [Dependency Management](#dependency-management) +- [Configuration](#configuration) +- [Components](#components) +- [Redfish API Endpoints](#redfish-api-endpoints) +- [Troubleshooting](#troubleshooting) +- [License](#license) + +## Overview + +This repository contains: +- **bmcweb**: OpenBMC web server source code with SONiC-specific patches +- **sonic-dbus-bridge**: Bridge between SONiC Redis and D-Bus for bmcweb integration + +Both components are built as Debian packages (`.deb`) for easy integration with SONiC. + +## Quick Start + +### Prerequisites + +- Docker installed on your system +- Git +- sudo access (for cleaning root-owned build artifacts) + +### Build + +```bash +# Build all components (Docker-based, produces .deb packages) +make + +# Or explicitly +make all +``` + +Build artifacts will be available in `target/debs/trixie/`: +- `bmcweb_1.0.0_arm64.deb` +- `bmcweb-dbg_1.0.0_arm64.deb` +- `sonic-dbus-bridge_1.0.0_arm64.deb` +- `sonic-dbus-bridge-dbgsym_1.0.0_arm64.deb` + +### Build Targets + +```bash +# Show all available targets +make help + +# Build individual components (automatically runs clean + dependencies) +make build-bmcweb # Runs: clean → setup-bmcweb → apply-patches → build +make build-bridge # Runs: clean → build + +# Clean build artifacts (removes build dirs, resets bmcweb source) +make clean + +# Complete reset (clean + remove Docker images + full git reset) +make reset +``` + +### Build Options + +```bash +# Use custom number of parallel jobs (default: nproc) +make SONIC_CONFIG_MAKE_JOBS=8 + +# Use custom output directory (default: target/debs/trixie) +make SONIC_REDFISH_TARGET=output/debs + +# Build with specific bmcweb commit (default: 6926d430) +make BMCWEB_HEAD_COMMIT=abc123 + +# Build with custom bmcweb repository URL +make BMCWEB_REPO_URL=https://github.com/custom/bmcweb.git +``` + +## Build System + +The build system is designed for **Debian Trixie** and uses: + +1. **Docker-based builds**: All compilation happens inside a `debian:trixie` container for consistency +2. **Debian packaging**: Uses `dpkg-buildpackage` to create `.deb` packages +3. **Meson subprojects**: Dependencies (sdbusplus, stdexec) are managed via `.wrap` files +4. **Automatic dependencies**: Build targets automatically trigger required cleanup and setup steps +5. **Patch management**: Uses a `patches/series` file to define patch order + +### Build Flow + +![Build Flow Chart](images/BuildFlowChart.png) + +``` +make all + +1. Build Docker image (sonic-redfish-builder:latest) + - Base: debian:trixie + - Installs: build-essential, meson, debhelper, C++23 toolchain, sdbusplus + +2. Setup bmcweb source + - Auto-clone from GitHub if not present + - Checkout to specified commit (default: 6926d430) + +3. Apply patches + - Apply patches from patches/series to bmcweb source + +4. Build sonic-dbus-bridge + - Meson downloads dependencies (sdbusplus, stdexec) via .wrap files + - dpkg-buildpackage creates .deb packages + +5. Build bmcweb + - Meson downloads dependencies via .wrap files + - dpkg-buildpackage creates .deb packages + +6. Collect artifacts to target/debs/trixie/ + - bmcweb_1.0.0_arm64.deb + - bmcweb-dbg_1.0.0_arm64.deb + - sonic-dbus-bridge_1.0.0_arm64.deb + - sonic-dbus-bridge-dbgsym_1.0.0_arm64.deb + - Plus .changes, .buildinfo, .dsc files +``` + +### Automatic Dependencies + +The build system automatically handles dependencies: + +- **`build-bmcweb`**: Automatically runs `clean` → `setup-bmcweb` → `apply-patches` → build +- **`build-bridge`**: Automatically runs `clean` → build + +This ensures a clean, reproducible build every time. + +## Patch Management + +Patches are located in `patches/` directory: +- `patches/series` - Defines patch order (lines starting with `#` are comments) +- `patches/*.patch` - Individual patch files + +Current patches: +1. `0001-Integrating-bmcweb-with-SONiC-s-build-system.patch` - Adds Debian packaging + + +To add a new patch: +1. Make changes in bmcweb source directory +2. Generate patch: `cd bmcweb && git format-patch -1 HEAD` +3. Move patch to `patches/` directory +4. Add patch filename to `patches/series` + +## Cleanup Targets + +### `clean` - Remove build artifacts, reset source +- Removes: `obj-*`, `debian/`, `.deb` files, subproject builds +- Resets: bmcweb source to clean git state (so patches can be reapplied) +- Keeps: Docker images, target directory +- Use when: You want to rebuild from scratch + +### `reset` - Complete cleanup +- Does everything `clean` does, plus: +- Removes: Docker images, target directory +- Resets: bmcweb to base commit with `git clean -fdx` +- Use when: You want to start completely fresh + +## Dependency Management + +Dependencies are managed via **Meson wrap files** (`.wrap`): + +### bmcweb dependencies: +- `bmcweb/subprojects/sdbusplus.wrap` - D-Bus C++ bindings +- `bmcweb/subprojects/stdexec.wrap` - C++23 executors +- Plus other dependencies defined in bmcweb upstream + +### sonic-dbus-bridge dependencies: +- `sonic-dbus-bridge/subprojects/sdbusplus.wrap` - D-Bus C++ bindings +- `sonic-dbus-bridge/subprojects/stdexec.wrap` - C++23 executors + +Meson automatically downloads and builds these dependencies during the build process. + +The Debian packages can be installed in SONiC images. + +## Components + +### bmcweb +- **Source**: https://github.com/openbmc/bmcweb +- **Base commit**: 6926d430 (configurable via `BMCWEB_HEAD_COMMIT`) +- **License**: Apache-2.0 +- **Purpose**: Redfish API server providing standard Redfish REST API +- **Build system**: Meson + Debian packaging +- **Output**: `bmcweb_1.0.0_arm64.deb`, `bmcweb-dbg_1.0.0_arm64.deb` +- **Auto-clone**: Automatically cloned from GitHub if not present + +### sonic-dbus-bridge +- **License**: Apache-2.0 +- **Purpose**: Bridge SONiC Redis database to D-Bus for bmcweb integration +- **Features**: + - Redis to D-Bus data synchronization + - Platform inventory management + - FRU EEPROM data export + - User management integration + - State management (host, chassis) +- **Build system**: Meson + Debian packaging +- **Output**: `sonic-dbus-bridge_1.0.0_arm64.deb`, `sonic-dbus-bridge-dbgsym_1.0.0_arm64.deb` +- **Configuration**: `config/config.yaml` for Redis, D-Bus, and platform settings + +## Configuration + +### sonic-dbus-bridge Configuration + +The bridge is configured via `sonic-dbus-bridge/config/config.yaml`: + +- **Redis settings**: Connection parameters for CONFIG_DB and STATE_DB +- **Platform data**: Path to platform.json and FRU EEPROM locations +- **D-Bus settings**: Service name and bus configuration +- **Update behavior**: Polling intervals and pub/sub settings +- **Logging**: Log levels and output configuration + +### D-Bus Configuration Files + +D-Bus security policies are defined in `sonic-dbus-bridge/dbus/`: +- `xyz.openbmc_project.Inventory.Manager.conf` - Inventory management +- `xyz.openbmc_project.ObjectMapper.conf` - Object mapper service +- `xyz.openbmc_project.State.Host.conf` - Host state management +- `xyz.openbmc_project.User.Manager.conf` - User management +- `xyz.openbmc_project.bmcweb.conf` - bmcweb service + +These files are installed to `/etc/dbus-1/system.d/` during package installation. + +## Redfish API Endpoints + +Below are the currently supported Redfish API endpoints and their sample responses. + +### FirmwareInventory Collection + +``` +GET /redfish/v1/UpdateService/FirmwareInventory +``` + +```json +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory", + "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection", + "Members": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bmc" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/switch" + } + ], + "Members@odata.count": 3, + "Name": "Software Inventory Collection" +} +``` + +### FirmwareInventory - BIOS + +``` +GET /redfish/v1/UpdateService/FirmwareInventory/bios +``` + +```json +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios", + "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory", + "Description": "Other image", + "Id": "bios", + "Name": "Software Inventory", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Updateable": false, + "Version": "N/A" +} +``` + +### FirmwareInventory - BMC Firmware + +``` +GET /redfish/v1/UpdateService/FirmwareInventory/bmc +``` + +```json +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bmc", + "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory", + "Description": "BMC image", + "Id": "bmc", + "Name": "Software Inventory", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Managers/bmc" + } + ], + "RelatedItem@odata.count": 1, + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Updateable": false, + "Version": "sonic-redfish-build.0-ddbc425a4" +} +``` + +### FirmwareInventory - Switch + +``` +GET /redfish/v1/UpdateService/FirmwareInventory/switch +``` + +```json +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/switch", + "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory", + "Description": "Host image", + "Id": "switch", + "Name": "Software Inventory", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/system/Bios" + } + ], + "RelatedItem@odata.count": 1, + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Updateable": false, + "Version": "N/A" +} +``` + +### Service Root + +``` +GET /redfish/v1/ +``` + +```json +{ + "@odata.id": "/redfish/v1", + "@odata.type": "#ServiceRoot.v1_15_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "Cables": { + "@odata.id": "/redfish/v1/Cables" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Id": "RootService", + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "ManagerProvidingService": { + "@odata.id": "/redfish/v1/Managers/bmc" + }, + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "Product": "SONiCBMC", + "ProtocolFeaturesSupported": { + "DeepOperations": { + "DeepPATCH": false, + "DeepPOST": false + }, + "ExcerptQuery": false, + "ExpandQuery": { + "ExpandAll": false, + "Levels": false, + "Links": false, + "NoLinks": false + }, + "FilterQuery": false, + "OnlyMemberQuery": true, + "SelectQuery": true + }, + "RedfishVersion": "1.17.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "TelemetryService": { + "@odata.id": "/redfish/v1/TelemetryService" + }, + "UUID": "00000000-0000-0000-0000-000000000000", + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + } +} +``` + +### ComputerSystem.Reset - Power On + +``` +POST /redfish/v1/Systems/system/Actions/ComputerSystem.Reset +Content-Type: application/json + +{"ResetType": "On"} +``` + +This action writes a host transition request to Redis STATE_DB: + +``` +root@sonic:/home/admin# redis-cli -n 6 HGETALL BMC_HOST_REQUEST +1) "request_id" +2) "req_1775040896_000001" +3) "requested_transition" +4) "reset-out" +5) "status" +6) "pending" +7) "timestamp" +8) "1775040896157648224" +``` + +### ComputerSystem.Reset - Graceful Shutdown + +``` +POST /redfish/v1/Systems/system/Actions/ComputerSystem.Reset +Content-Type: application/json + +{"ResetType": "GracefulShutdown"} +``` + +Redis STATE_DB after the request: + +``` +root@sonic:/home/admin# redis-cli -n 6 HGETALL BMC_HOST_REQUEST +1) "request_id" +2) "req_1775041067_000002" +3) "requested_transition" +4) "reset-in" +5) "status" +6) "pending" +7) "timestamp" +8) "1775041067766120204" +``` + +### ComputerSystem.Reset - Power Cycle + +``` +POST /redfish/v1/Systems/system/Actions/ComputerSystem.Reset +Content-Type: application/json + +{"ResetType": "PowerCycle"} +``` + +Redis STATE_DB after the request: + +``` +root@sonic:/home/admin# redis-cli -n 6 HGETALL BMC_HOST_REQUEST +1) "request_id" +2) "req_1775041121_000003" +3) "requested_transition" +4) "reset-cycle" +5) "status" +6) "pending" +7) "timestamp" +8) "1775041121924146637" +``` + +## Troubleshooting + +### Build fails with "debian/changelog: No such file or directory" +Run `make reset` to completely clean the workspace, then rebuild. + +### Permission denied when cleaning +The build creates root-owned files inside Docker. The Makefile uses `sudo rm` to clean them. +Make sure you have sudo access. + +### Docker image build fails +Check your internet connection - the build downloads packages from Debian repositories. + +### Meson subproject download fails +Check internet connection and firewall settings. Meson needs to access GitHub to download dependencies. + +## License + +Apache-2.0 diff --git a/build/Dockerfile.build b/build/Dockerfile.build new file mode 100644 index 0000000..1785ab5 --- /dev/null +++ b/build/Dockerfile.build @@ -0,0 +1,41 @@ +# Build environment for sonic-redfish +FROM debian:trixie + +# Install build dependencies +# Note: Install meson and Python packages via apt (not pip) so dpkg-checkbuilddeps recognizes them +RUN apt-get update && apt-get install -y \ + build-essential \ + debhelper-compat \ + gcc \ + g++ \ + git \ + pkg-config \ + python3 \ + python3-pip \ + python3-venv \ + libsystemd-dev \ + systemd-dev \ + libssl-dev \ + zlib1g-dev \ + libzstd-dev \ + libpam0g-dev \ + libnghttp2-dev \ + libtinyxml2-dev \ + libhiredis-dev \ + libjsoncpp-dev \ + libboost-system-dev \ + libboost-url-dev \ + libboost-dev \ + nlohmann-json3-dev \ + dbus \ + ninja-build \ + cmake \ + meson \ + python3-yaml \ + python3-mako \ + python3-inflection \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +CMD ["/bin/bash"] diff --git a/images/BuildFlowChart.png b/images/BuildFlowChart.png new file mode 100644 index 0000000..89c1e23 Binary files /dev/null and b/images/BuildFlowChart.png differ diff --git a/patches/0001-Integrating-bmcweb-with-SONiC-s-build-system.patch b/patches/0001-Integrating-bmcweb-with-SONiC-s-build-system.patch new file mode 100644 index 0000000..89b90d2 --- /dev/null +++ b/patches/0001-Integrating-bmcweb-with-SONiC-s-build-system.patch @@ -0,0 +1,177 @@ +From b27543a61caee8e7243f18b224a1dc428b1a30f6 Mon Sep 17 00:00:00 2001 +From: shreyansh-nexthop +Date: Tue, 2 Dec 2025 05:32:30 +0000 +Subject: [PATCH 1/5] Integrating bmcweb with SONiC's build system. + +Added Debian packaging files to enable bmcweb to be built as a Debian package in the SONiC build environment. + +* debian/changelog: Package version 1.0.0 +* debian/control: Build dependencies and package metadata +* debian/rules: Meson build configuration with SONiC-specific options + +These packaging files allow bmcweb to integrate with SONiC's dpkg-based build system for ARM64 (aspeed) platform. +--- + debian/changelog | 8 ++++++++ + debian/control | 46 ++++++++++++++++++++++++++++++++++++++++++++ + debian/install | 7 +++++++ + debian/not-installed | 4 ++++ + debian/rules | 20 +++++++++++++++++++ + meson.build | 5 ++++- + 6 files changed, 89 insertions(+), 1 deletion(-) + create mode 100644 debian/changelog + create mode 100644 debian/control + create mode 100644 debian/install + create mode 100644 debian/not-installed + create mode 100755 debian/rules + +diff --git a/debian/changelog b/debian/changelog +new file mode 100644 +index 00000000..21f41f1f +--- /dev/null ++++ b/debian/changelog +@@ -0,0 +1,8 @@ ++bmcweb (1.0.0) stable; urgency=medium ++ ++ * Initial SONiC integration of bmcweb ++ * OpenBMC web server with Redfish support ++ * Adapted for SONiC BMC management ++ ++ -- Nexthop SONiC Team Mon, 10 Nov 2025 00:00:00 +0000 ++ +diff --git a/debian/control b/debian/control +new file mode 100644 +index 00000000..dee8647a +--- /dev/null ++++ b/debian/control +@@ -0,0 +1,46 @@ ++Source: bmcweb ++Section: net ++Priority: optional ++Maintainer: SONiC Team ++Build-Depends: debhelper-compat (= 13), ++ meson (>= 1.3.0), ++ ninja-build, ++ g++ (>= 13), ++ pkg-config, ++ libpam0g-dev, ++ libssl-dev (>= 3.0.0), ++ libsystemd-dev, ++ zlib1g-dev, ++ nlohmann-json3-dev, ++ libnghttp2-dev (>= 1.43.0), ++ libtinyxml2-dev, ++ python3, ++ python3-yaml, ++ python3-mako, ++ python3-inflection ++Standards-Version: 4.7.2 ++ ++Package: bmcweb ++Architecture: any ++Depends: ${shlibs:Depends}, ${misc:Depends} ++Description: OpenBMC webserver for Redfish, KVM, and BMC management ++ bmcweb is a webserver implementation for OpenBMC that provides multiple ++ management interfaces including: ++ * Redfish - RESTful API for hardware management ++ * KVM - Keyboard, Video, Mouse remote console ++ * Virtual Media - Remote ISO/USB mounting ++ * Web UI - Browser-based management interface ++ . ++ This package is adapted for SONiC integration on ASPEED BMC platforms. ++ ++Package: bmcweb-dbg ++Architecture: any ++Section: debug ++Priority: optional ++Depends: bmcweb (= ${binary:Version}), ${misc:Depends} ++Description: BMC web server for OpenBMC (debug symbols) ++ bmcweb is a webserver implementation for OpenBMC that provides ++ Redfish, KVM, and other management interfaces for BMC management. ++ . ++ This package contains the debug symbols. ++ +diff --git a/debian/install b/debian/install +new file mode 100644 +index 00000000..f6982776 +--- /dev/null ++++ b/debian/install +@@ -0,0 +1,6 @@ ++usr/bin/bmcweb ++etc/pam.d/webserver ++usr/lib/systemd/system/bmcweb.service ++usr/lib/systemd/system/bmcweb.socket ++usr/share/www ++ +diff --git a/debian/not-installed b/debian/not-installed +new file mode 100644 +index 00000000..5d12edab +--- /dev/null ++++ b/debian/not-installed +@@ -0,0 +1,22 @@ ++usr/include ++usr/lib/*/libboost_*.a ++usr/lib/*/pkgconfig ++etc/dbus-1/system.d/xyz.openbmc_project.bmcweb.conf ++usr/bin/unzstd ++usr/bin/zstd ++usr/bin/zstd-frugal ++usr/bin/zstdcat ++usr/bin/zstdgrep ++usr/bin/zstdless ++usr/bin/zstdmt ++usr/lib/*/libzstd.so ++usr/lib/*/libzstd.so.1 ++usr/lib/*/libzstd.so.1.5.5 ++usr/share/man/man1/unzstd.1 ++usr/share/man/man1/zstd.1 ++usr/share/man/man1/zstdcat.1 ++usr/share/man/man1/zstdgrep.1 ++usr/share/man/man1/zstdless.1 ++usr/share/man/man1/zstdmt.1 ++usr/lib/*/libsdbusplus.so* ++ +diff --git a/debian/rules b/debian/rules +new file mode 100755 +index 00000000..a9ae5b06 +--- /dev/null ++++ b/debian/rules +@@ -0,0 +1,20 @@ ++#!/usr/bin/make -f ++ ++%: ++ dh $@ --buildsystem=meson ++ ++override_dh_auto_configure: ++ dh_auto_configure -- --wrap-mode=default ++ ++# dh_auto_install installs to debian/tmp/, then dh_install copies ++# files listed in debian/install to debian/bmcweb/ ++ ++# Todo: Disabling tests for now, later remove it. ++override_dh_auto_test: ++ # Skip tests during package build ++ ++override_dh_strip: ++ dh_strip --dbg-package=bmcweb-dbg ++ ++override_dh_shlibdeps: ++ dh_shlibdeps -l$(CURDIR)/debian/tmp/usr/lib/aarch64-linux-gnu --dpkg-shlibdeps-params=--ignore-missing-info +diff --git a/meson.build b/meson.build +index 93c26b72..d533eeeb 100644 +--- a/meson.build ++++ b/meson.build +@@ -67,7 +67,10 @@ incdir = [ + ] + + # Add compiler arguments +-boost_flags = ['-Wno-unused-parameter'] ++# -Wno-error=type-limits: Boost 1.87.0 has a bug in process/src/ext/env.cpp ++# where it compares char to EOF (-1), which is always true on ARM64 ++# (char is unsigned on ARM64) ++boost_flags = ['-Wno-unused-parameter', '-Wno-error=type-limits'] + nghttp2_flags = [] + if (cxx.get_id() == 'clang') + if (cxx.version().version_compare('<17.0')) +-- +2.34.1 + diff --git a/patches/0002-Add-Product-field-to-Redfish-service-root.patch b/patches/0002-Add-Product-field-to-Redfish-service-root.patch new file mode 100644 index 0000000..3cf18b2 --- /dev/null +++ b/patches/0002-Add-Product-field-to-Redfish-service-root.patch @@ -0,0 +1,29 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: shreyansh-nexthop +Date: Tue, 17 Mar 2026 18:30:00 +0000 +Subject: [PATCH] Add Product field to Redfish service root + +Add "Product": "SONiCBMC" field to the /redfish/v1 service root response +to identify the BMC product name. + +This helps clients identify that they are connecting to a SONiC BMC +implementation. +--- + redfish-core/lib/service_root.hpp | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/redfish-core/lib/service_root.hpp b/redfish-core/lib/service_root.hpp +index 00000000..11111111 100644 +--- a/redfish-core/lib/service_root.hpp ++++ b/redfish-core/lib/service_root.hpp +@@ -52,6 +52,7 @@ inline void handleServiceRootGetImpl( + asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1"; + asyncResp->res.jsonValue["Id"] = "RootService"; + asyncResp->res.jsonValue["Name"] = "Root Service"; ++ asyncResp->res.jsonValue["Product"] = "SONiCBMC"; + asyncResp->res.jsonValue["RedfishVersion"] = "1.17.0"; + asyncResp->res.jsonValue["Links"]["Sessions"]["@odata.id"] = + "/redfish/v1/SessionService/Sessions"; +-- +2.34.1 + diff --git a/patches/series b/patches/series new file mode 100644 index 0000000..b6122ef --- /dev/null +++ b/patches/series @@ -0,0 +1,10 @@ +# Patch series for bmcweb SONiC integration +# This series applies on bmcweb commit 6926d430 +# +# Format: one patch filename per line +# Lines starting with # are comments +# Patches are applied in order from top to bottom + +0001-Integrating-bmcweb-with-SONiC-s-build-system.patch +0002-Add-Product-field-to-Redfish-service-root.patch + diff --git a/sonic-dbus-bridge/config/config.yaml b/sonic-dbus-bridge/config/config.yaml new file mode 100644 index 0000000..5a51ace --- /dev/null +++ b/sonic-dbus-bridge/config/config.yaml @@ -0,0 +1,68 @@ + +# #################################### +# SONiC D-Bus Bridge Configuration +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2026 Nexthop AI +# Copyright (C) 2024 SONiC Project +# Author: Nexthop AI +# Author: SONiC Project +# License file: sonic-redfish/LICENSE +# #################################### + +# Redis connection settings +redis: + config_db: + host: localhost + port: 6379 + db: 4 + timeout_ms: 5000 + state_db: + host: localhost + port: 6379 + db: 6 + timeout_ms: 5000 + retry: + max_attempts: 5 + initial_backoff_ms: 1000 + max_backoff_ms: 30000 + + # Platform data sources + platform: + # ${PLATFORM} will be expanded from environment + json_path: /usr/share/sonic/device/${PLATFORM}/platform.json + fru_eeprom_paths: + - /sys/bus/i2c/devices/0-0050/eeprom + - /sys/bus/i2c/devices/1-0051/eeprom + - /sys/bus/i2c/devices/2-0052/eeprom + auto_detect_fru: true + + # D-Bus settings + dbus: + service_name: xyz.openbmc_project.Inventory + system_bus: true + + # Update behavior + update: + poll_interval_sec: 30 + enable_redis_pubsub: false # Not available on ASPEED yet + + # Logging + logging: + level: INFO # DEBUG, INFO, WARNING, ERROR + use_systemd_journal: true + file_path: /var/log/sonic-dbus-bridge.log + max_file_size_mb: 10 + + # Feature flags + features: + enable_sensors: false # Phase 2 + enable_power_control: false # Phase 2 + enable_persistence: false # Phase 2 + + # Persistence (when enabled) + persistence: + enabled: false + state_file: /var/lib/sonic-dbus-bridge/state.json + save_interval_sec: 300 # Save every 5 minutes + + diff --git a/sonic-dbus-bridge/dbus/xyz.openbmc_project.Inventory.Manager.conf b/sonic-dbus-bridge/dbus/xyz.openbmc_project.Inventory.Manager.conf new file mode 100644 index 0000000..3b731c3 --- /dev/null +++ b/sonic-dbus-bridge/dbus/xyz.openbmc_project.Inventory.Manager.conf @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/sonic-dbus-bridge/dbus/xyz.openbmc_project.ObjectMapper.conf b/sonic-dbus-bridge/dbus/xyz.openbmc_project.ObjectMapper.conf new file mode 100644 index 0000000..9fbc96b --- /dev/null +++ b/sonic-dbus-bridge/dbus/xyz.openbmc_project.ObjectMapper.conf @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/sonic-dbus-bridge/dbus/xyz.openbmc_project.State.Host.conf b/sonic-dbus-bridge/dbus/xyz.openbmc_project.State.Host.conf new file mode 100644 index 0000000..28724ae --- /dev/null +++ b/sonic-dbus-bridge/dbus/xyz.openbmc_project.State.Host.conf @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/sonic-dbus-bridge/dbus/xyz.openbmc_project.User.Manager.conf b/sonic-dbus-bridge/dbus/xyz.openbmc_project.User.Manager.conf new file mode 100644 index 0000000..2a72a8b --- /dev/null +++ b/sonic-dbus-bridge/dbus/xyz.openbmc_project.User.Manager.conf @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/sonic-dbus-bridge/dbus/xyz.openbmc_project.bmcweb.conf b/sonic-dbus-bridge/dbus/xyz.openbmc_project.bmcweb.conf new file mode 100644 index 0000000..6d7df7b --- /dev/null +++ b/sonic-dbus-bridge/dbus/xyz.openbmc_project.bmcweb.conf @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/sonic-dbus-bridge/debian/changelog b/sonic-dbus-bridge/debian/changelog new file mode 100644 index 0000000..90507d7 --- /dev/null +++ b/sonic-dbus-bridge/debian/changelog @@ -0,0 +1,11 @@ +sonic-dbus-bridge (1.0.0) unstable; urgency=medium + + * Initial release + * Implements SONiC to D-Bus inventory bridge for ASPEED BMC + * Supports /redfish/v1/Chassis and /redfish/v1/Systems endpoints + * Data sources: Redis (CONFIG_DB/STATE_DB), platform.json, FRU EEPROMs + * Graceful degradation when data sources unavailable + * Optimized for ASPEED ARM64 BMC with limited resources + + -- SONiC Team Mon, 09 March 2026 00:00:00 +0000 + diff --git a/sonic-dbus-bridge/debian/control b/sonic-dbus-bridge/debian/control new file mode 100644 index 0000000..ef3dfba --- /dev/null +++ b/sonic-dbus-bridge/debian/control @@ -0,0 +1,31 @@ +Source: sonic-dbus-bridge +Section: net +Priority: optional +Maintainer: SONiC Team +Build-Depends: debhelper-compat (= 13), + meson (>= 0.57.0), + pkg-config, + libsystemd-dev, + libhiredis-dev, + libjsoncpp-dev, + libboost-dev, + libboost-system-dev +Standards-Version: 4.6.2 +Homepage: https://github.com/sonic-net/sonic-redfish + +Package: sonic-dbus-bridge +Architecture: any +Depends: ${misc:Depends}, + libsystemd0, + libhiredis1.1.0, + libjsoncpp26, + libboost-system1.83.0 | libboost-system1.88.0, + dbus +Description: SONiC to D-Bus inventory bridge for ASPEED BMC + Bridges SONiC data sources (Redis, platform.json, FRU EEPROMs) + to OpenBMC-compatible D-Bus interfaces, enabling bmcweb to + serve Redfish endpoints without modification. + . + This daemon is specifically designed for ASPEED ARM64 BMC + platforms running SONiC with limited database/service availability. + diff --git a/sonic-dbus-bridge/debian/rules b/sonic-dbus-bridge/debian/rules new file mode 100755 index 0000000..78a4afa --- /dev/null +++ b/sonic-dbus-bridge/debian/rules @@ -0,0 +1,27 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export DEB_CFLAGS_MAINT_APPEND = -Wall -Wextra +export DEB_CXXFLAGS_MAINT_APPEND = -Wall -Wextra + +%: + dh $@ --buildsystem=meson + +override_dh_auto_configure: + dh_auto_configure -- --wrap-mode=default + +override_dh_auto_install: + dh_auto_install + # Create state directory + install -d debian/sonic-dbus-bridge/var/lib/sonic-dbus-bridge + +override_dh_installsystemd: + dh_installsystemd --name=sonic-dbus-bridge --no-start + +override_dh_shlibdeps: + dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info + diff --git a/sonic-dbus-bridge/include/bridge_app.hpp b/sonic-dbus-bridge/include/bridge_app.hpp new file mode 100644 index 0000000..3b213dc --- /dev/null +++ b/sonic-dbus-bridge/include/bridge_app.hpp @@ -0,0 +1,188 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include "redis_adapter.hpp" +#include "platform_json_adapter.hpp" +#include "fru_adapter.hpp" +#include "dbus_exporter.hpp" +#include "update_engine.hpp" +#include "config_manager.hpp" +#include "object_mapper.hpp" +#include "user_mgr.hpp" +#include "state_manager.hpp" +#include "redis_state_subscriber.hpp" +#include +#include +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + + /** + * @brief Main bridge application + * + * Coordinates all components: + * - Initializes data sources (Redis, platform.json, FRU) + * - Builds initial inventory model + * - Creates D-Bus objects + * - Runs event loop with periodic updates + */ + class BridgeApp + { + public: + /** + * @brief Construct a new Bridge App + * + * @param configPath Path to configuration file + */ + explicit BridgeApp(const std::string& configPath); + + /** + * @brief Initialize the application + * + * - Load configuration + * - Connect to data sources + * - Build initial inventory + * - Create D-Bus objects + * + * @return true on success + * @return false on fatal error + */ + bool initialize(); + + /** + * @brief Run the application + * + * Enters main event loop (blocking) + * + * @return Exit code (0 = success) + */ + int run(); + + /** + * @brief Shutdown the application + */ + void shutdown(); + + private: + std::string configPath_; + std::unique_ptr configMgr_; + + // Boost ASIO + boost::asio::io_context io_; + boost::asio::signal_set signals_; + + // D-Bus connections (one per service for proper object separation) + // Each service has its own connection so busctl tree shows only its objects + std::shared_ptr inventoryConn_; + std::unique_ptr inventoryServer_; + + std::shared_ptr mapperConn_; + std::unique_ptr mapperServer_; + + std::shared_ptr userConn_; + std::unique_ptr userServer_; + + std::shared_ptr stateConn_; + std::unique_ptr stateServer_; + + // Data source adapters + std::shared_ptr redisAdapter_; + std::unique_ptr platformAdapter_; + std::unique_ptr fruAdapter_; + + // Core components + std::shared_ptr dbusExporter_; + std::unique_ptr updateEngine_; + std::unique_ptr objectMapper_; + + // User management + std::unique_ptr userMgr_; + + // State management + std::unique_ptr stateManager_; + + // Event-driven Redis subscriber + std::unique_ptr redisSubscriber_; + + // Current inventory model + InventoryModel currentModel_; + + // Health tracking + std::map healthStatus_; + + /** + * @brief Load configuration file + */ + bool loadConfiguration(); + + /** + * @brief Connect to D-Bus system bus + */ + bool connectDbus(); + + /** + * @brief Initialize data sources + */ + void initializeDataSources(); + + /** + * @brief Build initial inventory model + */ + InventoryModel buildInitialModel(); + + /** + * @brief Create D-Bus objects + */ + void createDbusObjects(); + + /** + * @brief Create state objects + */ + void createStateObjects(); + + /** + * @brief Start update engine + */ + void startUpdateEngine(); + + /** + * @brief Handle signals (SIGTERM, SIGINT) + */ + void handleSignal(const boost::system::error_code& ec, int signal); + + /** + * @brief Update health status + */ + void updateHealth(DataSource source, DataSourceHealth health); + + /** + * @brief Log health report + */ + void logHealthReport(); + + /** + * @brief Initialize user management subsystem + * + * Creates UserMgr instance using the shared object server. + * Non-fatal if it fails - bridge continues without user management. + */ + void initializeUserManager(); + }; + +} // namespace sonic::dbus_bridge + + + diff --git a/sonic-dbus-bridge/include/config_manager.hpp b/sonic-dbus-bridge/include/config_manager.hpp new file mode 100644 index 0000000..9629a4a --- /dev/null +++ b/sonic-dbus-bridge/include/config_manager.hpp @@ -0,0 +1,84 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Configuration manager + * + * Loads and provides access to daemon configuration + */ +class ConfigManager +{ + public: + /** + * @brief Load configuration from YAML file + * + * @param configPath Path to config.yaml + * @return true on success + */ + bool load(const std::string& configPath); + + // Redis configuration + const std::string& getConfigDbHost() const { return configDbHost_; } + int getConfigDbPort() const { return configDbPort_; } + int getConfigDbIndex() const { return configDbIndex_; } + + const std::string& getStateDbHost() const { return stateDbHost_; } + int getStateDbPort() const { return stateDbPort_; } + int getStateDbIndex() const { return stateDbIndex_; } + + // Platform configuration + const std::string& getPlatformJsonPath() const { return platformJsonPath_; } + const std::vector& getFruEepromPaths() const { return fruEepromPaths_; } + + // Update configuration + int getPollIntervalSec() const { return pollIntervalSec_; } + + // Logging configuration + const std::string& getLogLevel() const { return logLevel_; } + + // D-Bus configuration + const std::string& getDbusServiceName() const { return dbusServiceName_; } + + private: + // Redis + std::string configDbHost_{"127.0.0.1"}; // Use IP instead of localhost for reliability + int configDbPort_{6379}; + int configDbIndex_{4}; + + std::string stateDbHost_{"127.0.0.1"}; // Use IP instead of localhost for reliability + int stateDbPort_{6379}; + int stateDbIndex_{6}; + + // Platform + std::string platformJsonPath_{"/usr/share/sonic/device/${PLATFORM}/platform.json"}; + std::vector fruEepromPaths_{ + "/sys/bus/i2c/devices/0-0050/eeprom", + "/sys/bus/i2c/devices/1-0051/eeprom" + }; + + // Update + int pollIntervalSec_{30}; + + // Logging + std::string logLevel_{"INFO"}; + + // D-Bus + std::string dbusServiceName_{"xyz.openbmc_project.Inventory.Manager"}; +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/dbus_exporter.hpp b/sonic-dbus-bridge/include/dbus_exporter.hpp new file mode 100644 index 0000000..9171c77 --- /dev/null +++ b/sonic-dbus-bridge/include/dbus_exporter.hpp @@ -0,0 +1,98 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief D-Bus object exporter + * + * Creates and manages D-Bus objects that bmcweb expects: + * - /xyz/openbmc_project/inventory/system/chassis + * - /xyz/openbmc_project/inventory/system/system0 + * - /xyz/openbmc_project/state/chassis0 + * - /xyz/openbmc_project/software/ + */ +class DBusExporter +{ + public: + /** + * @brief Construct a new DBus Exporter + * + * @param inventoryServer Object server for inventory objects (chassis, system, state) + */ + explicit DBusExporter(sdbusplus::asio::object_server& inventoryServer); + + /** + * @brief Destructor - cleanup is automatic (RAII) + */ + ~DBusExporter() = default; + + /** + * @brief Create all D-Bus objects from inventory model + * + * @param model Complete inventory model + * @return true on success, false on error + */ + bool createObjects(const InventoryModel& model); + + /** + * @brief Update D-Bus objects with new model + * + * Only updates properties that have changed + * + * @param model New inventory model + * @return true on success, false on error + */ + bool updateObjects(const InventoryModel& model); + + private: + sdbusplus::asio::object_server& inventoryServer_; + + // Current model (for change detection) + InventoryModel currentModel_; + + // D-Bus interfaces (managed by shared_ptr, cleanup is automatic) + std::map> interfaces_; + + /** + * @brief Create chassis inventory object + */ + bool createChassisObject(const ChassisInfo& chassis); + + /** + * @brief Create system inventory object + */ + bool createSystemObject(const SystemInfo& system); + + /** + * @brief Create chassis state object + */ + bool createChassisStateObject(const ChassisState& state); + + /** + * @brief Create firmware version objects under /xyz/openbmc_project/software/ + * + * Creates one D-Bus object per firmware entry with: + * - xyz.openbmc_project.Software.Version (Purpose, Version) + * - xyz.openbmc_project.Software.Activation (Activation, RequestedActivation) + */ + bool createFirmwareObjects(const std::vector& versions); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/fru_adapter.hpp b/sonic-dbus-bridge/include/fru_adapter.hpp new file mode 100644 index 0000000..65cf907 --- /dev/null +++ b/sonic-dbus-bridge/include/fru_adapter.hpp @@ -0,0 +1,89 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Adapter for reading FRU EEPROMs + * + * Reads FRU data from sysfs I2C EEPROM devices and parses + * ONIE TLV format to extract serial number, part number, etc. + */ +class FruAdapter +{ + public: + /** + * @brief Construct a new FRU Adapter + * + * @param eepromPaths List of possible EEPROM paths to try + */ + explicit FruAdapter(const std::vector& eepromPaths); + + /** + * @brief Scan and read FRU EEPROM + * + * Tries each configured path until one succeeds + * + * @return true if FRU data read successfully + * @return false if all paths failed + */ + bool scanAndLoad(); + + /** + * @brief Check if FRU data was loaded successfully + */ + bool isLoaded() const { return loaded_; } + + /** + * @brief Get FRU information + * + * @return FruInfo structure (fields may be empty if not in EEPROM) + */ + FruInfo getFruInfo() const { return fruInfo_; } + + private: + std::vector eepromPaths_; + bool loaded_{false}; + FruInfo fruInfo_; + + /** + * @brief Read FRU EEPROM from a specific path + * + * @param path EEPROM device path + * @return true if read and parsed successfully + */ + bool readEeprom(const std::string& path); + + /** + * @brief Parse ONIE TLV format + * + * @param data Raw EEPROM data + * @return FruInfo structure + */ + FruInfo parseTlv(const std::vector& data); + + /** + * @brief Validate TLV CRC + * + * @param data Raw EEPROM data + * @return true if CRC is valid + */ + bool validateCrc(const std::vector& data); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/inventory_model.hpp b/sonic-dbus-bridge/include/inventory_model.hpp new file mode 100644 index 0000000..b710d74 --- /dev/null +++ b/sonic-dbus-bridge/include/inventory_model.hpp @@ -0,0 +1,82 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Inventory model builder and normalizer + * + * Merges data from multiple sources (FRU, Redis, platform.json) + * with precedence: FRU > CONFIG_DB > platform.json > defaults + */ +class InventoryModelBuilder +{ + public: + /** + * @brief Build inventory model from all sources + * + * @param fruInfo FRU EEPROM data (optional) + * @param deviceMetadata CONFIG_DB metadata (optional) + * @param platformDesc platform.json description (optional) + * @param chassisState STATE_DB chassis state (optional) + * @return Complete InventoryModel + */ + static InventoryModel build( + const std::optional& fruInfo, + const std::optional& deviceMetadata, + const std::optional& platformDesc, + const std::optional& chassisState); + + private: + /** + * @brief Build chassis info with fallback priority + */ + static ChassisInfo buildChassisInfo( + const std::optional& fruInfo, + const std::optional& deviceMetadata, + const std::optional& platformDesc); + + /** + * @brief Build system info with fallback priority + */ + static SystemInfo buildSystemInfo( + const std::optional& fruInfo, + const std::optional& deviceMetadata); + + /** + * @brief Build PSU list from platform.json + */ + static std::vector buildPsuList( + const std::optional& platformDesc); + + /** + * @brief Build fan list from platform.json + */ + static std::vector buildFanList( + const std::optional& platformDesc); + +}; + +/** + * @brief Compare two inventory models for changes + * + * @param oldModel Previous model + * @param newModel New model + * @return true if models differ + */ +bool hasChanged(const InventoryModel& oldModel, const InventoryModel& newModel); + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/logger.hpp b/sonic-dbus-bridge/include/logger.hpp new file mode 100644 index 0000000..df8fb20 --- /dev/null +++ b/sonic-dbus-bridge/include/logger.hpp @@ -0,0 +1,288 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sonic::dbus_bridge::logger +{ + + // Log levels matching syslog priorities + enum class LogLevel : int + { + DEBUG = LOG_DEBUG, // 7 + INFO = LOG_INFO, // 6 + NOTICE = LOG_NOTICE, // 5 + WARNING = LOG_WARNING, // 4 + ERR = LOG_ERR, // 3 + CRIT = LOG_CRIT // 2 + }; + + // Global logger state + struct LoggerState + { + std::atomic currentLevel{LogLevel::INFO}; + std::atomic fileLoggingEnabled{false}; + std::mutex fileMutex; + FILE* logFile = nullptr; + static constexpr const char* logFilePath = "/var/log/sonic-dbus-bridge.log"; + + void enableFileLogging() + { + std::lock_guard lock(fileMutex); + if (!logFile) + { + logFile = fopen(logFilePath, "a"); + if (logFile) + { + fileLoggingEnabled.store(true, std::memory_order_release); + syslog(6, "Logger: File logging enabled to %s", logFilePath); // LOG_INFO = 6 + } + else + { + syslog(3, "Logger: Failed to open log file %s: %s", // LOG_ERR = 3 + logFilePath, strerror(errno)); + } + } + } + + void disableFileLogging() + { + std::lock_guard lock(fileMutex); + if (logFile) + { + fclose(logFile); + logFile = nullptr; + fileLoggingEnabled.store(false, std::memory_order_release); + // Delete the log file + unlink(logFilePath); + syslog(6, "Logger: File logging disabled and log file deleted"); // LOG_INFO = 6 + } + } + + void writeToFile(const char* message) + { + std::lock_guard lock(fileMutex); + if (logFile && fileLoggingEnabled.load(std::memory_order_acquire)) + { + // Get current timestamp + time_t now = time(nullptr); + char timestamp[64]; + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", + localtime(&now)); + + fprintf(logFile, "[%s] %s\n", timestamp, message); + fflush(logFile); + } + } + + ~LoggerState() + { + if (logFile) + { + fclose(logFile); + } + } + }; + + // Get the global logger state + inline LoggerState& getLoggerState() + { + static LoggerState state; + return state; + } + + // Check if a log level should be logged + inline bool shouldLog(int level) + { + return level <= static_cast(getLoggerState().currentLevel.load( + std::memory_order_acquire)); + } + + // Initialize logger from environment variable + inline void initFromEnv() + { + const char* levelStr = std::getenv("SONIC_DBUS_BRIDGE_LOG_LEVEL"); + if (levelStr) + { + if (strcmp(levelStr, "DEBUG") == 0) + { + getLoggerState().currentLevel.store(LogLevel::DEBUG, + std::memory_order_release); + } + else if (strcmp(levelStr, "INFO") == 0) + { + getLoggerState().currentLevel.store(LogLevel::INFO, + std::memory_order_release); + } + else if (strcmp(levelStr, "NOTICE") == 0) + { + getLoggerState().currentLevel.store(LogLevel::NOTICE, + std::memory_order_release); + } + else if (strcmp(levelStr, "WARNING") == 0) + { + getLoggerState().currentLevel.store(LogLevel::WARNING, + std::memory_order_release); + } + else if (strcmp(levelStr, "ERR") == 0) + { + getLoggerState().currentLevel.store(LogLevel::ERR, + std::memory_order_release); + } + else if (strcmp(levelStr, "CRIT") == 0) + { + getLoggerState().currentLevel.store(LogLevel::CRIT, + std::memory_order_release); + } + } + } + +} // namespace sonic::dbus_bridge::logger + +// Undefine syslog constants so we can use them as macro names +// We'll use the numeric values directly in our macros +#undef LOG_DEBUG +#undef LOG_INFO +#undef LOG_NOTICE +#undef LOG_WARNING +#undef LOG_ERR +#undef LOG_CRIT + +// Define numeric constants for syslog levels (from syslog.h) +#define SYSLOG_DEBUG 7 +#define SYSLOG_INFO 6 +#define SYSLOG_NOTICE 5 +#define SYSLOG_WARNING 4 +#define SYSLOG_ERR 3 +#define SYSLOG_CRIT 2 + +// Public API macros for logging +#define LOG_DEBUG(fmt, ...) \ + do \ +{ \ + if (::sonic::dbus_bridge::logger::shouldLog(SYSLOG_DEBUG)) \ + { \ + syslog(SYSLOG_DEBUG, fmt, ##__VA_ARGS__); \ + if (::sonic::dbus_bridge::logger::getLoggerState() \ + .fileLoggingEnabled.load(std::memory_order_acquire)) \ + { \ + char _buf[1024]; \ + snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + ::sonic::dbus_bridge::logger::getLoggerState().writeToFile(_buf);\ + } \ + } \ +} while (0) + +#define LOG_INFO(fmt, ...) \ + do \ +{ \ + if (::sonic::dbus_bridge::logger::shouldLog(SYSLOG_INFO)) \ + { \ + syslog(SYSLOG_INFO, fmt, ##__VA_ARGS__); \ + if (::sonic::dbus_bridge::logger::getLoggerState() \ + .fileLoggingEnabled.load(std::memory_order_acquire)) \ + { \ + char _buf[1024]; \ + snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + ::sonic::dbus_bridge::logger::getLoggerState().writeToFile(_buf);\ + } \ + } \ +} while (0) + +#define LOG_NOTICE(fmt, ...) \ + do \ +{ \ + if (::sonic::dbus_bridge::logger::shouldLog(SYSLOG_NOTICE)) \ + { \ + syslog(SYSLOG_NOTICE, fmt, ##__VA_ARGS__); \ + if (::sonic::dbus_bridge::logger::getLoggerState() \ + .fileLoggingEnabled.load(std::memory_order_acquire)) \ + { \ + char _buf[1024]; \ + snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + ::sonic::dbus_bridge::logger::getLoggerState().writeToFile(_buf);\ + } \ + } \ +} while (0) + +#define LOG_WARNING(fmt, ...) \ + do \ +{ \ + if (::sonic::dbus_bridge::logger::shouldLog(SYSLOG_WARNING)) \ + { \ + syslog(SYSLOG_WARNING, fmt, ##__VA_ARGS__); \ + if (::sonic::dbus_bridge::logger::getLoggerState() \ + .fileLoggingEnabled.load(std::memory_order_acquire)) \ + { \ + char _buf[1024]; \ + snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + ::sonic::dbus_bridge::logger::getLoggerState().writeToFile(_buf);\ + } \ + } \ +} while (0) + +#define LOG_WARN(fmt, ...) LOG_WARNING(fmt, ##__VA_ARGS__) + +#define LOG_ERROR(fmt, ...) \ + do \ +{ \ + if (::sonic::dbus_bridge::logger::shouldLog(SYSLOG_ERR)) \ + { \ + syslog(SYSLOG_ERR, fmt, ##__VA_ARGS__); \ + if (::sonic::dbus_bridge::logger::getLoggerState() \ + .fileLoggingEnabled.load(std::memory_order_acquire)) \ + { \ + char _buf[1024]; \ + snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + ::sonic::dbus_bridge::logger::getLoggerState().writeToFile(_buf);\ + } \ + } \ +} while (0) + +#define LOG_ERR(fmt, ...) LOG_ERROR(fmt, ##__VA_ARGS__) + +#define LOG_CRITICAL(fmt, ...) \ + do \ +{ \ + if (::sonic::dbus_bridge::logger::shouldLog(SYSLOG_CRIT)) \ + { \ + syslog(SYSLOG_CRIT, fmt, ##__VA_ARGS__); \ + if (::sonic::dbus_bridge::logger::getLoggerState() \ + .fileLoggingEnabled.load(std::memory_order_acquire)) \ + { \ + char _buf[1024]; \ + snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ + ::sonic::dbus_bridge::logger::getLoggerState().writeToFile(_buf);\ + } \ + } \ +} while (0) + +#define LOG_CRIT(fmt, ...) LOG_CRITICAL(fmt, ##__VA_ARGS__) + +// Initialization macro (call from bridge_app.cpp) +#define LOGGER_INIT() ::sonic::dbus_bridge::logger::initFromEnv() + +// Signal handler helpers (to be called from bridge_app.cpp signal handlers) +#define LOGGER_ENABLE_FILE_LOGGING() \ + ::sonic::dbus_bridge::logger::getLoggerState().enableFileLogging() + +#define LOGGER_DISABLE_FILE_LOGGING() \ + ::sonic::dbus_bridge::logger::getLoggerState().disableFileLogging() + + diff --git a/sonic-dbus-bridge/include/object_mapper.hpp b/sonic-dbus-bridge/include/object_mapper.hpp new file mode 100644 index 0000000..916e7f9 --- /dev/null +++ b/sonic-dbus-bridge/include/object_mapper.hpp @@ -0,0 +1,106 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include + +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Minimal implementation of xyz.openbmc_project.ObjectMapper + * + * This is a very small, in-process mapper that only knows about + * objects exported by sonic-dbus-bridge itself. It is intentionally + * limited to the subset of methods used by bmcweb: + * - GetObject + * - GetSubTree + * - GetSubTreePaths + * - GetAssociatedSubTreePaths (stub, returns empty set) + */ +class ObjectMapperService +{ + public: + explicit ObjectMapperService(sdbusplus::asio::object_server& server); + ~ObjectMapperService() = default; + + /** + * @brief Register the ObjectMapper D-Bus object/vtable + * + * Must be called after the process has acquired the + * xyz.openbmc_project.ObjectMapper bus name. + */ + bool initialize(); + + /** + * @brief Register or update a D-Bus object in the mapper registry. + * + * @param path Object path + * @param interfaces Interfaces implemented at this path + * @param serviceName Service name that owns this object (optional, + * defaults to inventoryServiceName_) + */ + void registerObject(const std::string& path, + const std::vector& interfaces, + const std::string& serviceName = ""); + + /** + * @brief Unregister a D-Bus object from the mapper registry. + * + * @param path Object path to remove + */ + void unregisterObject(const std::string& path); + + private: + sdbusplus::asio::object_server& server_; + + // Registry: object path -> {interfaces, serviceName} + struct ObjectInfo + { + std::vector interfaces; + std::string serviceName; + }; + std::map objects_; + + // D-Bus interface for ObjectMapper + std::shared_ptr mapperIface_; + + // Helpers + static bool pathIsUnder(const std::string& root, const std::string& path); + + // Method implementations (return types match sdbusplus method registration) + using GetObjectResult = + std::map>; + using GetSubTreeResult = + std::map>>; + using GetSubTreePathsResult = std::vector; + + GetObjectResult getObject(const std::string& path, + const std::vector& interfaces); + + GetSubTreeResult getSubTree(const std::string& subtree, int32_t depth, + const std::vector& interfaces); + + GetSubTreePathsResult getSubTreePaths(const std::string& subtree, + int32_t depth, + const std::vector& interfaces); + + GetSubTreePathsResult getAssociatedSubTreePaths( + const std::string& associatedPath, const std::string& subtree, + int32_t depth, const std::vector& interfaces); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/platform_json_adapter.hpp b/sonic-dbus-bridge/include/platform_json_adapter.hpp new file mode 100644 index 0000000..19841ed --- /dev/null +++ b/sonic-dbus-bridge/include/platform_json_adapter.hpp @@ -0,0 +1,92 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Adapter for reading platform.json + * + * Parses /usr/share/sonic/device//platform.json + * to extract hardware topology information. + */ +class PlatformJsonAdapter +{ + public: + /** + * @brief Construct a new Platform Json Adapter + * + * @param platformJsonPath Path to platform.json (can contain ${PLATFORM}) + */ + explicit PlatformJsonAdapter(const std::string& platformJsonPath); + + /** + * @brief Load and parse platform.json + * + * @return true if file loaded and parsed successfully + * @return false if file not found or parse error + */ + bool load(); + + /** + * @brief Check if platform.json was loaded successfully + */ + bool isLoaded() const { return loaded_; } + + /** + * @brief Get platform description + * + * @return PlatformDescription with chassis/fan/PSU/thermal info + */ + PlatformDescription getPlatformDescription() const; + + /** + * @brief Get chassis name from platform.json + */ + std::optional getChassisName() const; + + /** + * @brief Get chassis part number from platform.json + */ + std::optional getChassisPartNumber() const; + + /** + * @brief Get chassis hardware version from platform.json + */ + std::optional getChassisHardwareVersion() const; + + private: + std::string platformJsonPath_; + bool loaded_{false}; + PlatformDescription description_; + + /** + * @brief Expand environment variables in path + * + * Replaces ${PLATFORM} with value from environment + */ + std::string expandPath(const std::string& path) const; + + /** + * @brief Parse JSON file + * + * @param path File path + * @return true on success + */ + bool parseJson(const std::string& path); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/redis_adapter.hpp b/sonic-dbus-bridge/include/redis_adapter.hpp new file mode 100644 index 0000000..1b4ae35 --- /dev/null +++ b/sonic-dbus-bridge/include/redis_adapter.hpp @@ -0,0 +1,145 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Redis client adapter for SONiC databases + * + * Connects to CONFIG_DB (DB 4) and STATE_DB (DB 6) and provides + * methods to read device metadata and chassis state. + */ +class RedisAdapter +{ + public: + /** + * @brief Construct a new Redis Adapter + * + * @param configDbHost CONFIG_DB host (default: localhost) + * @param configDbPort CONFIG_DB port (default: 6379) + * @param stateDbHost STATE_DB host (default: localhost) + * @param stateDbPort STATE_DB port (default: 6379) + */ + RedisAdapter(const std::string& configDbHost = "localhost", + int configDbPort = 6379, + const std::string& stateDbHost = "localhost", + int stateDbPort = 6379); + + ~RedisAdapter(); + + // Disable copy + RedisAdapter(const RedisAdapter&) = delete; + RedisAdapter& operator=(const RedisAdapter&) = delete; + + /** + * @brief Connect to Redis databases + * + * @return true if at least one database connected successfully + * @return false if all connections failed + */ + bool connect(); + + /** + * @brief Check if CONFIG_DB is connected + */ + bool isConfigDbConnected() const { return configDbContext_ != nullptr; } + + /** + * @brief Check if STATE_DB is connected + */ + bool isStateDbConnected() const { return stateDbContext_ != nullptr; } + + /** + * @brief Get device metadata from CONFIG_DB + * + * Reads DEVICE_METADATA|localhost hash + * + * @return DeviceMetadata structure (fields may be empty if unavailable) + */ + DeviceMetadata getDeviceMetadata(); + + /** + * @brief Get chassis state from STATE_DB + * + * Reads CHASSIS_STATE_TABLE|chassis0 hash + * + * @return ChassisState structure (defaults to "on" if unavailable) + */ + ChassisState getChassisState(); + + /** + * @brief Get firmware versions from STATE_DB + * + * Reads BMC_FW_INVENTORY|* keys from STATE_DB on the switch + * Returns placeholder "N/A" for entries + * not yet published. + * + * @return Vector of firmware version entries + */ + std::vector getFirmwareVersions(); + + private: + std::string configDbHost_; + int configDbPort_; + std::string stateDbHost_; + int stateDbPort_; + + redisContext* configDbContext_{nullptr}; + redisContext* stateDbContext_{nullptr}; + + /** + * @brief Connect to a specific Redis database + * + * @param host Redis host + * @param port Redis port + * @param dbIndex Database index (4 for CONFIG_DB, 6 for STATE_DB) + * @return redisContext* on success, nullptr on failure + */ + redisContext* connectToDb(const std::string& host, int port, int dbIndex); + + /** + * @brief Get all fields from a Redis hash + * + * @param ctx Redis context + * @param key Hash key + * @return Map of field->value, empty if key doesn't exist + */ + std::map hgetall(redisContext* ctx, + const std::string& key); + + /** + * @brief Get a single field from a Redis hash + * + * @param ctx Redis context + * @param key Hash key + * @param field Field name + * @return Field value if exists, nullopt otherwise + */ + std::optional hget(redisContext* ctx, + const std::string& key, + const std::string& field); + + /** + * @brief Free Redis reply + */ + void freeReply(void* reply); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/redis_state_publisher.hpp b/sonic-dbus-bridge/include/redis_state_publisher.hpp new file mode 100644 index 0000000..e2ed72a --- /dev/null +++ b/sonic-dbus-bridge/include/redis_state_publisher.hpp @@ -0,0 +1,108 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Redis State Publisher for BMC host state management + * + * Publishes host transition requests to Redis STATE_DB and updates + * the SWITCH_HOST_STATE table. Uses hiredis for Redis communication. + */ +class RedisStatePublisher +{ +public: + RedisStatePublisher(); + ~RedisStatePublisher(); + + // Disable copy + RedisStatePublisher(const RedisStatePublisher&) = delete; + RedisStatePublisher& operator=(const RedisStatePublisher&) = delete; + + /** + * @brief Connect to Redis STATE_DB (DB 6) + * + * @param host Redis host (default: localhost) + * @param port Redis port (default: 6379) + * @return true if connection successful + */ + bool connect(const std::string& host = "localhost", int port = 6379); + + /** + * @brief Check if connected to Redis + */ + bool isConnected() const { return stateDbContext_ != nullptr; } + + /** + * @brief Publish a host transition request to Redis + * + * Creates an entry in BMC_HOST_REQUEST table with: + * - requested_transition: the transition type + * - request_id: unique identifier + * - timestamp: current time + * - status: "pending" + * + * @param transition Transition type (e.g., "Reboot", "On", "Off") + * @return request_id on success, empty string on failure + */ + std::string publishHostRequest(const std::string& transition); + + /** + * @brief Update SWITCH_HOST_STATE table + * + * @param deviceState "POWERED_ON" or "POWERED_OFF" + * @param deviceStatus "REACHABLE" or "UNREACHABLE" + * @return true if update successful + */ + bool updateSwitchHostState(const std::string& deviceState, + const std::string& deviceStatus); + + /** + * @brief Update BMC_HOST_REQUEST status field + * + * @param requestId Request ID to update + * @param status New status ("pending", "processing", "completed", "failed") + * @return true if update successful + */ + bool updateRequestStatus(const std::string& requestId, + const std::string& status); + +private: + redisContext* stateDbContext_; + std::mutex redisMutex_; // Protect Redis operations + uint64_t requestCounter_; + + /** + * @brief Generate unique request ID + * Format: req__ + */ + std::string generateRequestId(); + + /** + * @brief Set a single field in a Redis hash + */ + bool hset(const std::string& key, const std::string& field, const std::string& value); + + /** + * @brief Set multiple fields in a Redis hash + */ + bool hmset(const std::string& key, const std::map& fields); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/redis_state_subscriber.hpp b/sonic-dbus-bridge/include/redis_state_subscriber.hpp new file mode 100644 index 0000000..1ee42f6 --- /dev/null +++ b/sonic-dbus-bridge/include/redis_state_subscriber.hpp @@ -0,0 +1,116 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Redis State Subscriber using keyspace notifications + * + * Subscribes to Redis keyspace notifications for multiple keys + * and invokes callbacks when any subscribed key is updated. + */ +class RedisStateSubscriber +{ +public: + /** + * @brief Callback function type + * Parameters: (key, field, value) + * - key: Redis key that changed (e.g., "SWITCH_HOST_STATE", "DEVICE_METADATA") + * - field: Hash field that changed (e.g., "device_state", "serial_number") + * - value: New value of the field + */ + using KeyspaceCallback = std::function; + + RedisStateSubscriber(); + ~RedisStateSubscriber(); + + // Disable copy + RedisStateSubscriber(const RedisStateSubscriber&) = delete; + RedisStateSubscriber& operator=(const RedisStateSubscriber&) = delete; + + /** + * @brief Start subscriber thread (single key - backward compatible) + * + * Connects to Redis STATE_DB and subscribes to keyspace notifications + * for a single key. + * + * @param host Redis host + * @param port Redis port + * @param callback Function to call when state changes + * @return true if started successfully + */ + bool start(const std::string& host, int port, KeyspaceCallback callback); + + /** + * @brief Start subscriber thread (multiple keys) + * + * Connects to Redis STATE_DB and subscribes to keyspace notifications + * for multiple keys. + * + * @param host Redis host + * @param port Redis port + * @param keys List of Redis keys to subscribe to + * @param callback Function to call when any key changes + * @return true if started successfully + */ + bool startMultiKey(const std::string& host, int port, + const std::vector& keys, + KeyspaceCallback callback); + + /** + * @brief Stop subscriber thread + */ + void stop(); + + /** + * @brief Check if subscriber is running + */ + bool isRunning() const { return running_; } + +private: + redisContext* subContext_; + redisContext* getContext_; // Separate context for HGETALL + std::thread subscriberThread_; + std::atomic running_; + KeyspaceCallback callback_; + + /** + * @brief Subscriber thread main loop + */ + void subscriberLoop(); + + /** + * @brief Handle keyspace notification + * @param channel Channel name (e.g., "__keyspace@6__:SWITCH_HOST_STATE") + */ + void handleKeyspaceNotification(const std::string& channel); + + /** + * @brief Get all fields from a hash + * @param key Redis key + * @return Map of field->value + */ + std::map hgetall(const std::string& key); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/state_manager.hpp b/sonic-dbus-bridge/include/state_manager.hpp new file mode 100644 index 0000000..59995e2 --- /dev/null +++ b/sonic-dbus-bridge/include/state_manager.hpp @@ -0,0 +1,134 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "redis_state_publisher.hpp" + +namespace sonic::dbus_bridge +{ + +/** + * @brief State manager for system state transitions + * + * Handles system reset/reboot/power actions via D-Bus properties. + * Implements OpenBMC xyz.openbmc_project.State.Host interface. + * + * Flow: + * 1. bmcweb writes to RequestedHostTransition property + * 2. Property setter callback validates and queues action + * 3. Async executor processes queue using Boost.Asio timer + * 4. Publish to Redis STATE_DB via RedisStatePublisher + * 5. Read back from Redis to verify write + * 6. Update CurrentHostState property + * 7. Emit D-Bus PropertiesChanged signal + */ +class StateManager +{ + public: + /** + * @brief Construct a new State Manager + * + * @param server sdbusplus object server + * @param io Boost ASIO io_context for async operations + */ + StateManager(sdbusplus::asio::object_server& server, + boost::asio::io_context& io); + + /** + * @brief Destructor - cleanup is automatic (RAII) + */ + ~StateManager() = default; + + /** + * @brief Create D-Bus state objects + * + * Creates /xyz/openbmc_project/state/host0 with + * xyz.openbmc_project.State.Host interface + * + * @return true on success, false on error + */ + bool createStateObjects(); + + private: + sdbusplus::asio::object_server& server_; + boost::asio::io_context& io_; + + // D-Bus interface + std::shared_ptr hostStateIface_; + + // State tracking + std::string currentHostState_; + std::string lastRequestedTransition_; + + // Redis publisher for state changes + std::unique_ptr redisPublisher_; + + // Action queue for async processing + struct ActionRequest + { + std::string transition; + std::chrono::steady_clock::time_point timestamp; + }; + std::queue actionQueue_; + std::unique_ptr actionTimer_; + bool actionInProgress_{false}; + + // Maximum queue size to prevent overflow + static constexpr size_t MAX_QUEUE_SIZE = 10; + + /** + * @brief Process next action in queue + * + * Called when an action is queued or when previous action completes. + * Non-blocking - schedules async execution via timer. + */ + void processNextAction(); + + /** + * @brief Execute host transition action + * + * @param transition D-Bus transition value + */ + void executeHostTransition(const std::string& transition); + + /** + * @brief Update host state and emit signal + * + * @param newState New host state value + */ + void updateHostState(const std::string& newState); + + /** + * @brief Map D-Bus transition to script command + * + * @param transition D-Bus transition value + * @return Script command argument, or empty string if invalid + */ + std::string transitionToScriptCommand(const std::string& transition); + + /** + * @brief Validate transition value + * + * @param transition D-Bus transition value + * @return true if valid, false otherwise + */ + bool isValidTransition(const std::string& transition); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/types.hpp b/sonic-dbus-bridge/include/types.hpp new file mode 100644 index 0000000..b6512b9 --- /dev/null +++ b/sonic-dbus-bridge/include/types.hpp @@ -0,0 +1,191 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief FRU (Field Replaceable Unit) information from EEPROM + */ +struct FruInfo +{ + std::optional serialNumber; + std::optional partNumber; + std::optional manufacturer; + std::optional model; + std::optional hardwareVersion; + std::optional manufactureDate; + std::optional productName; +}; + +/** + * @brief Device metadata from CONFIG_DB + */ +struct DeviceMetadata +{ + std::optional platform; + std::optional hwsku; + std::optional hostname; + std::optional mac; + std::optional type; + std::optional manufacturer; + std::optional serialNumber; + std::optional partNumber; + std::optional model; +}; + +/** + * @brief Chassis state from STATE_DB + */ +struct ChassisState +{ + std::string powerState{"on"}; // "on" or "off" +}; + +/** + * @brief Data source for a field + */ +enum class FieldSource +{ + Redis, + FruEeprom, + PlatformJson, + Default +}; + +/** + * @brief Normalized chassis information + */ +struct ChassisInfo +{ + std::string serialNumber{"Unknown"}; + std::string partNumber{"Unknown"}; + std::string manufacturer{"Unknown"}; + std::string model{"Unknown"}; + std::string hardwareVersion{"Unknown"}; + std::string chassisType{"RackMount"}; + bool present{true}; + std::string prettyName{"SONiC Chassis"}; + + // Source tracking + FieldSource serialNumberSource{FieldSource::Default}; + FieldSource partNumberSource{FieldSource::Default}; + FieldSource manufacturerSource{FieldSource::Default}; + FieldSource modelSource{FieldSource::Default}; +}; + +/** + * @brief Normalized system information + */ +struct SystemInfo +{ + std::string serialNumber{"Unknown"}; + std::string manufacturer{"Unknown"}; + std::string model{"Unknown"}; + std::string hostname{"sonic"}; + bool present{true}; + std::string prettyName{"SONiC System"}; +}; + +/** + * @brief PSU information + */ +struct PsuInfo +{ + std::string name; + std::string serialNumber{"Unknown"}; + std::string model{"Unknown"}; + bool present{false}; +}; + +/** + * @brief Fan information + */ +struct FanInfo +{ + std::string name; + bool present{false}; +}; + +/** + * @brief Platform description from platform.json + */ +struct PlatformDescription +{ + std::string chassisName; + std::optional chassisPartNumber; + std::optional chassisHardwareVersion; + std::vector fanNames; + std::vector psuNames; + std::vector thermalNames; +}; + +/** + * @brief Firmware version purpose + */ +enum class FirmwarePurpose +{ + BMC, + Host, + Other +}; + +/** + * @brief Firmware version information for FirmwareInventory + */ +struct FirmwareVersionInfo +{ + std::string id; // Unique identifier + std::string version; // Version string + FirmwarePurpose purpose{FirmwarePurpose::Other}; +}; + +/** + * @brief Complete inventory model + */ +struct InventoryModel +{ + ChassisInfo chassis; + SystemInfo system; + ChassisState chassisState; + std::vector psus; + std::vector fans; + std::vector firmwareVersions; +}; + +/** + * @brief Data source health status + */ +enum class DataSourceHealth +{ + Healthy, + Degraded, + Unavailable +}; + +/** + * @brief Data source types + */ +enum class DataSource +{ + RedisConfigDb, + RedisStateDb, + PlatformJson, + FruEeprom +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/update_engine.hpp b/sonic-dbus-bridge/include/update_engine.hpp new file mode 100644 index 0000000..52ba04c --- /dev/null +++ b/sonic-dbus-bridge/include/update_engine.hpp @@ -0,0 +1,111 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "types.hpp" +#include "redis_adapter.hpp" +#include "dbus_exporter.hpp" +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +/** + * @brief Update engine for event-driven and periodic updates + * + * Supports both event-driven updates (via Redis keyspace notifications) + * and periodic polling (as fallback). Updates D-Bus objects when changes + * are detected. + */ +class UpdateEngine +{ + public: + using UpdateCallback = std::function; + + /** + * @brief Construct a new Update Engine + * + * @param io Boost ASIO io_context + * @param redisAdapter Redis adapter + * @param dbusExporter D-Bus exporter + * @param pollIntervalSec Polling interval in seconds (0 = disable polling) + */ + UpdateEngine(boost::asio::io_context& io, + std::shared_ptr redisAdapter, + std::shared_ptr dbusExporter, + int pollIntervalSec); + + /** + * @brief Start periodic polling (if enabled) + */ + void start(); + + /** + * @brief Stop periodic polling + */ + void stop(); + + /** + * @brief Set callback for update events + * + * Called whenever data is updated from sources + */ + void setUpdateCallback(UpdateCallback callback) + { + updateCallback_ = std::move(callback); + } + + /** + * @brief Handle Redis field change event (event-driven) + * + * Called by RedisStateSubscriber when a Redis key changes. + * Updates only the affected D-Bus properties. + * + * @param key Redis key that changed (e.g., "DEVICE_METADATA", "CHASSIS_STATE") + * @param field Redis field that changed (e.g., "serial_number", "power_state") + * @param value New value of the field + */ + void onRedisFieldChange(const std::string& key, + const std::string& field, + const std::string& value); + + private: + boost::asio::io_context& io_; + std::shared_ptr redisAdapter_; + std::shared_ptr dbusExporter_; + int pollIntervalSec_; + boost::asio::steady_timer timer_; + bool running_{false}; + UpdateCallback updateCallback_; + + // Cached data for change detection + std::optional cachedMetadata_; + std::optional cachedState_; + + /** + * @brief Poll timer handler + */ + void onPollTimer(const boost::system::error_code& ec); + + /** + * @brief Perform update cycle + */ + void doUpdate(); + + /** + * @brief Schedule next poll + */ + void scheduleNextPoll(); +}; + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/include/user_mgr.hpp b/sonic-dbus-bridge/include/user_mgr.hpp new file mode 100644 index 0000000..942f778 --- /dev/null +++ b/sonic-dbus-bridge/include/user_mgr.hpp @@ -0,0 +1,104 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include "users.hpp" +#include "object_mapper.hpp" + +#include + +#include +#include +#include +#include + +namespace sonic +{ +namespace user +{ + +using UserInfoMap = std::map, bool>>; + +class UserMgr +{ + public: + /** @brief Constructs UserMgr object. + * + * @param[in] server - sdbusplus asio object server + * @param[in] path - D-Bus path + * @param[in] objectMapper - ObjectMapper service for registration (optional) + */ + UserMgr(sdbusplus::asio::object_server& server, const char* path, + sonic::dbus_bridge::ObjectMapperService* objectMapper = nullptr); + + /** @brief Get reference to user objects map + * + * @return const reference to usersList map + */ + const std::unordered_map>& getUsers() const + { + return usersList; + } + + /** @brief get user info + * Returns user properties for the given user name + * + * @param[in] userName - Name of the user + * @return - map of user properties + */ + UserInfoMap getUserInfo(const std::string& userName); + + private: + /** @brief sdbusplus asio object server */ + sdbusplus::asio::object_server& server; + + /** @brief object path */ + const std::string path; + + /** @brief ObjectMapper service for user registration */ + sonic::dbus_bridge::ObjectMapperService* objectMapper_; + + /** @brief User.Manager D-Bus interface */ + std::shared_ptr userMgrIface; + + /** @brief privilege manager container */ + const std::vector privMgr = {"priv-admin", "priv-operator", + "priv-user"}; + + /** @brief all groups that can be assigned to users */ + const std::vector allGroups = {"redfish"}; + + /** @brief map container to hold users object (only admin) */ + std::unordered_map> usersList; + + /** @brief initialize the user manager objects + * Creates D-Bus object only for the admin user + */ + void initUserObjects(void); + + /** @brief check if user is enabled + * + * @param[in] userName - name of the user + * + * @return true if enabled, false otherwise + */ + bool isUserEnabled(const std::string& userName); + + /** @brief check if user exists in usersList + * + * @param[in] userName - name of the user + * + * @return true if user exists, false otherwise + */ + bool isUserExist(const std::string& userName); +}; + +} // namespace user +} // namespace sonic diff --git a/sonic-dbus-bridge/include/users.hpp b/sonic-dbus-bridge/include/users.hpp new file mode 100644 index 0000000..0ddae89 --- /dev/null +++ b/sonic-dbus-bridge/include/users.hpp @@ -0,0 +1,97 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#pragma once + +#include + +#include +#include +#include + +namespace sonic +{ +namespace user +{ + +// Place where all user objects has to be created +constexpr auto usersObjPath = "/xyz/openbmc_project/user"; + +class UserMgr; // Forward declaration for UserMgr + +/** @class Users + * @brief Lists User objects and its properties + */ +class Users +{ + public: + Users() = delete; + Users(const Users&) = delete; + Users& operator=(const Users&) = delete; + Users(Users&&) = delete; + Users& operator=(Users&&) = delete; + + /** @brief Constructs Users object. + * + * @param[in] server - sdbusplus asio object server + * @param[in] path - D-Bus path + * @param[in] groups - users group list + * @param[in] priv - users privilege + * @param[in] enabled - user enabled state + * @param[in] parent - user manager - parent object + */ + Users(sdbusplus::asio::object_server& server, const std::string& path, + std::vector groups, const std::string& priv, + bool enabled, UserMgr& parent); + + /** @brief Destructor - removes D-Bus interfaces */ + ~Users(); + + /** @brief Get user name + * + * @return user name string + */ + std::string getUserName() const + { + return userName; + } + + /** @brief Get user privilege */ + const std::string& getUserPrivilege() const { return userPrivilege; } + + /** @brief Get user groups */ + const std::vector& getUserGroups() const { return userGroups; } + + /** @brief Get user enabled state */ + bool getUserEnabled() const { return userEnabled; } + + /** @brief Get user locked for failed attempt state */ + bool getUserLockedForFailedAttempt() const { return userLockedForFailedAttempt; } + + /** @brief Get user password expired state */ + bool getUserPasswordExpired() const { return userPasswordExpired; } + + private: + std::string userName; + UserMgr& manager; + sdbusplus::asio::object_server& server; + + // User attributes + std::string userPrivilege; + std::vector userGroups; + bool userEnabled; + bool userLockedForFailedAttempt = false; + bool userPasswordExpired = false; + + // D-Bus interface for User.Attributes + std::shared_ptr userIface; +}; + +} // namespace user +} // namespace sonic diff --git a/sonic-dbus-bridge/meson.build b/sonic-dbus-bridge/meson.build new file mode 100644 index 0000000..47a98a8 --- /dev/null +++ b/sonic-dbus-bridge/meson.build @@ -0,0 +1,114 @@ +project( + 'sonic-dbus-bridge', + 'cpp', + version: '1.0.0', + meson_version: '>=0.57.0', + default_options: [ + 'warning_level=3', + 'werror=true', + 'cpp_std=c++23', + 'buildtype=debugoptimized', + ], +) + +# Configuration for user management +conf_data = configuration_data() +conf_data.set_quoted( + 'USER_MANAGER_BUSNAME', + 'xyz.openbmc_project.User.Manager', + description: 'The D-Bus busname for user management.', +) +conf_data.set_quoted( + 'INVENTORY_MANAGER_BUSNAME', + 'xyz.openbmc_project.Inventory.Manager', + description: 'The D-Bus busname for inventory management.', +) +conf_data.set_quoted( + 'OBJECT_MAPPER_BUSNAME', + 'xyz.openbmc_project.ObjectMapper', + description: 'The D-Bus busname for object mapper.', +) +conf_data.set_quoted( + 'STATE_HOST_BUSNAME', + 'xyz.openbmc_project.State.Host', + description: 'The D-Bus busname for host state management.', +) + +# Enable root user management by default +conf_data.set('ENABLE_ROOT_USER_MGMT', true) + +conf_header = configure_file(output: 'config.h', configuration: conf_data) +# Dependencies +systemd_dep = dependency('libsystemd') +hiredis_dep = dependency('hiredis') +jsoncpp_dep = dependency('jsoncpp') +boost_dep = dependency('boost', modules: ['system']) +threads_dep = dependency('threads') +sdbusplus_dep = dependency('sdbusplus') + +# Source files +sources = files( + 'src/main.cpp', + 'src/bridge_app.cpp', + 'src/redis_adapter.cpp', + 'src/redis_state_publisher.cpp', + 'src/redis_state_subscriber.cpp', + 'src/platform_json_adapter.cpp', + 'src/fru_adapter.cpp', + 'src/inventory_model.cpp', + 'src/dbus_exporter.cpp', + 'src/update_engine.cpp', + 'src/config_manager.cpp', + 'src/object_mapper.cpp', + 'src/state_manager.cpp', + # User management + 'src/user_mgr.cpp', + 'src/users.cpp', + 'src/state_manager.cpp', +) + + +# Include directories +inc = include_directories('include') + +# Executable +executable( + 'sonic-dbus-bridge', + sources, + conf_header, + include_directories: inc, + dependencies: [ + systemd_dep, + hiredis_dep, + jsoncpp_dep, + boost_dep, + threads_dep, + sdbusplus_dep, + ], + link_args: ['-lcrypt'], + install: true, + install_dir: get_option('bindir'), +) + +# Install configuration file +install_data( + 'config/config.yaml', + install_dir: get_option('sysconfdir') / 'sonic-dbus-bridge', +) + +# sonic-dbus-bridge now runs inside docker-bmcweb container (managed by supervisord) +# No systemd service needed + +# Install D-Bus policy files +install_data( + 'dbus/xyz.openbmc_project.Inventory.Manager.conf', + 'dbus/xyz.openbmc_project.ObjectMapper.conf', + 'dbus/xyz.openbmc_project.User.Manager.conf', + install_dir: get_option('sysconfdir') / 'dbus-1' / 'system.d', +) + +# Create state directory +meson.add_install_script('sh', '-c', + 'mkdir -p $DESTDIR' + get_option('prefix') / 'var' / 'lib' / 'sonic-dbus-bridge' +) + diff --git a/sonic-dbus-bridge/meson.options b/sonic-dbus-bridge/meson.options new file mode 100644 index 0000000..e6c75df --- /dev/null +++ b/sonic-dbus-bridge/meson.options @@ -0,0 +1,9 @@ +# sonic-dbus-bridge meson options + +option( + 'root_user_mgmt', + type: 'feature', + value: 'disabled', + description: 'Include root user in user management' +) + diff --git a/sonic-dbus-bridge/src/bridge_app.cpp b/sonic-dbus-bridge/src/bridge_app.cpp new file mode 100644 index 0000000..b6ef902 --- /dev/null +++ b/sonic-dbus-bridge/src/bridge_app.cpp @@ -0,0 +1,516 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "bridge_app.hpp" +#include "inventory_model.hpp" +#include "logger.hpp" +#include "config.h" +#include +#include + +namespace sonic::dbus_bridge +{ + +// Signal handlers for runtime log file dumping +static void handleLogSignal(int signum) +{ + if (signum == SIGUSR1) + { + // Enable file logging + LOGGER_ENABLE_FILE_LOGGING(); + } + else if (signum == SIGUSR2) + { + // Disable file logging + LOGGER_DISABLE_FILE_LOGGING(); + } +} + +BridgeApp::BridgeApp(const std::string& configPath) + : configPath_(configPath), signals_(io_, SIGINT, SIGTERM) +{ + // Initialize logger from environment variable + LOGGER_INIT(); + + // Register signal handlers for runtime log file dumping + // SIGUSR1: start dumping logs to /tmp/sonic-dbus-bridge.log + // SIGUSR2: stop dumping logs and delete the file + signal(SIGUSR1, handleLogSignal); + signal(SIGUSR2, handleLogSignal); +} + +bool BridgeApp::initialize() +{ + LOG_INFO("Initializing SONiC D-Bus Bridge..."); + + // Load configuration + if (!loadConfiguration()) + { + LOG_ERROR("Failed to load configuration"); + return false; + } + + // Connect to D-Bus and claim all service names + if (!connectDbus()) + { + LOG_ERROR("Failed to connect to D-Bus"); + return false; + } + + // Initialize ObjectMapper service for bmcweb discovery (uses mapper connection) + objectMapper_ = std::make_unique(*mapperServer_); + if (!objectMapper_->initialize()) + { + LOG_ERROR("Failed to initialize ObjectMapper service"); + return false; + } + + // Initialize data sources + initializeDataSources(); + + // Build initial inventory model + currentModel_ = buildInitialModel(); + + // Create D-Bus objects (inventory) + createDbusObjects(); + + // Create state objects + createStateObjects(); + + // Initialize user management (non-fatal if it fails) + initializeUserManager(); + + // Start update engine + startUpdateEngine(); + + // Setup signal handlers + signals_.async_wait([this](const boost::system::error_code& ec, int signal) { + handleSignal(ec, signal); + }); + + logHealthReport(); + + return true; +} + +int BridgeApp::run() +{ + LOG_INFO("Running main event loop..."); + + try + { + // sdbusplus::asio::connection handles D-Bus event integration automatically + io_.run(); + } + catch (const std::exception& e) + { + LOG_ERROR("Event loop error: %s", e.what()); + return 1; + } + + LOG_INFO("Exiting..."); + return 0; +} + +void BridgeApp::shutdown() +{ + LOG_INFO("Shutting down..."); + + if (redisSubscriber_) + { + LOG_INFO("Stopping Redis subscriber..."); + redisSubscriber_->stop(); + } + + if (updateEngine_) + { + updateEngine_->stop(); + } + + // sdbusplus connection cleanup is automatic (RAII) + io_.stop(); +} + +bool BridgeApp::loadConfiguration() +{ + configMgr_ = std::make_unique(); + return configMgr_->load(configPath_); +} + +bool BridgeApp::connectDbus() +{ + try + { + sd_bus* bus = nullptr; + int r; + + // Inventory Manager connection + LOG_INFO("Requesting D-Bus name: %s", INVENTORY_MANAGER_BUSNAME); + r = sd_bus_open_system(&bus); + if (r < 0) + { + LOG_ERROR("Failed to open system bus for Inventory: %s", strerror(-r)); + return false; + } + inventoryConn_ = std::make_shared(io_, bus); + inventoryConn_->request_name(INVENTORY_MANAGER_BUSNAME); + inventoryServer_ = std::make_unique(inventoryConn_); + + // ObjectMapper connection + LOG_INFO("Requesting D-Bus name: %s", OBJECT_MAPPER_BUSNAME); + bus = nullptr; + r = sd_bus_open_system(&bus); + if (r < 0) + { + LOG_ERROR("Failed to open system bus for ObjectMapper: %s", strerror(-r)); + return false; + } + mapperConn_ = std::make_shared(io_, bus); + mapperConn_->request_name(OBJECT_MAPPER_BUSNAME); + mapperServer_ = std::make_unique(mapperConn_); + + // User Manager connection + LOG_INFO("Requesting D-Bus name: %s", USER_MANAGER_BUSNAME); + bus = nullptr; + r = sd_bus_open_system(&bus); + if (r < 0) + { + LOG_ERROR("Failed to open system bus for User Manager: %s", strerror(-r)); + return false; + } + userConn_ = std::make_shared(io_, bus); + userConn_->request_name(USER_MANAGER_BUSNAME); + userServer_ = std::make_unique(userConn_); + + LOG_INFO("Connected to D-Bus successfully (2 connections)"); + + // State.Host connection + LOG_INFO("Requesting D-Bus name: %s", STATE_HOST_BUSNAME); + bus = nullptr; + r = sd_bus_open_system(&bus); + if (r < 0) + { + LOG_ERROR("Failed to open system bus for State.Host: %s", strerror(-r)); + return false; + } + stateConn_ = std::make_shared(io_, bus); + stateConn_->request_name(STATE_HOST_BUSNAME); + stateServer_ = std::make_unique(stateConn_); + + LOG_INFO("Connected to D-Bus successfully (5 connections)"); + return true; + } + catch (const std::exception& e) + { + LOG_ERROR("Failed to connect to D-Bus: %s", e.what()); + return false; + } +} + +void BridgeApp::initializeDataSources() +{ + LOG_INFO("Initializing data sources..."); + + // Initialize Redis adapter + redisAdapter_ = std::make_shared( + configMgr_->getConfigDbHost(), + configMgr_->getConfigDbPort(), + configMgr_->getStateDbHost(), + configMgr_->getStateDbPort() + ); + + if (redisAdapter_->connect()) + { + updateHealth(DataSource::RedisConfigDb, + redisAdapter_->isConfigDbConnected() ? + DataSourceHealth::Healthy : DataSourceHealth::Unavailable); + updateHealth(DataSource::RedisStateDb, + redisAdapter_->isStateDbConnected() ? + DataSourceHealth::Healthy : DataSourceHealth::Unavailable); + } + else + { + updateHealth(DataSource::RedisConfigDb, DataSourceHealth::Unavailable); + updateHealth(DataSource::RedisStateDb, DataSourceHealth::Unavailable); + } + + // Initialize platform.json adapter + platformAdapter_ = std::make_unique( + configMgr_->getPlatformJsonPath() + ); + + if (platformAdapter_->load()) + { + updateHealth(DataSource::PlatformJson, DataSourceHealth::Healthy); + } + else + { + updateHealth(DataSource::PlatformJson, DataSourceHealth::Unavailable); + } + + // Initialize FRU adapter + fruAdapter_ = std::make_unique( + configMgr_->getFruEepromPaths() + ); + + if (fruAdapter_->scanAndLoad()) + { + updateHealth(DataSource::FruEeprom, DataSourceHealth::Healthy); + } + else + { + updateHealth(DataSource::FruEeprom, DataSourceHealth::Unavailable); + } +} + +InventoryModel BridgeApp::buildInitialModel() +{ + LOG_INFO("Building initial inventory model..."); + + std::optional fruInfo; + if (fruAdapter_->isLoaded()) + { + fruInfo = fruAdapter_->getFruInfo(); + } + + std::optional deviceMetadata; + if (redisAdapter_->isConfigDbConnected()) + { + deviceMetadata = redisAdapter_->getDeviceMetadata(); + } + + std::optional platformDesc; + if (platformAdapter_->isLoaded()) + { + platformDesc = platformAdapter_->getPlatformDescription(); + } + + std::optional chassisState; + if (redisAdapter_->isStateDbConnected()) + { + chassisState = redisAdapter_->getChassisState(); + } + + auto model = InventoryModelBuilder::build(fruInfo, deviceMetadata, platformDesc, chassisState); + + // Read firmware versions for FirmwareInventory + model.firmwareVersions = redisAdapter_->getFirmwareVersions(); + + return model; +} + +void BridgeApp::createDbusObjects() +{ + // Use inventory connection for inventory/state objects + dbusExporter_ = std::make_shared(*inventoryServer_); + if (!dbusExporter_->createObjects(currentModel_)) + { + LOG_WARNING("Failed to create some D-Bus objects"); + } + + // Register the objects we own with the local ObjectMapper so that + // bmcweb can discover them using GetSubTree* calls. + if (objectMapper_) + { + // Chassis inventory object + objectMapper_->registerObject( + "/xyz/openbmc_project/inventory/system/chassis", + {"xyz.openbmc_project.Inventory.Item.Chassis", + "xyz.openbmc_project.Inventory.Decorator.Asset", + "xyz.openbmc_project.Inventory.Decorator.Model"}); + + // System inventory object (no Item.System interface yet, + // only common decorators) + objectMapper_->registerObject( + "/xyz/openbmc_project/inventory/system/system0", + {"xyz.openbmc_project.Inventory.Decorator.Asset", + "xyz.openbmc_project.Inventory.Decorator.Model"}); + + // Chassis state object + objectMapper_->registerObject( + "/xyz/openbmc_project/state/chassis0", + {"xyz.openbmc_project.State.Chassis"}); + + // Firmware inventory objects (for /redfish/v1/UpdateService/FirmwareInventory) + for (const auto& fw : currentModel_.firmwareVersions) + { + std::string fwPath = "/xyz/openbmc_project/software/" + fw.id; + objectMapper_->registerObject( + fwPath, + {"xyz.openbmc_project.Software.Version", + "xyz.openbmc_project.Software.Activation"}); + } + } +} + +void BridgeApp::createStateObjects() +{ + // Use dedicated State.Host connection for state objects + stateManager_ = std::make_unique(*stateServer_, io_); + + if (!stateManager_->createStateObjects()) + { + LOG_WARNING("Failed to create state objects"); + return; + } + + // Register the state object with the local ObjectMapper so that + // bmcweb can discover it using GetSubTree* calls. + if (objectMapper_) + { + objectMapper_->registerObject( + "/xyz/openbmc_project/state/host0", + {"xyz.openbmc_project.State.Host"}); + } +} + +void BridgeApp::startUpdateEngine() +{ + // Create UpdateEngine with polling disabled - event-driven mode only + updateEngine_ = std::make_unique( + io_, + redisAdapter_, + dbusExporter_, + 0 // Disable polling - use event-driven updates only + ); + + updateEngine_->setUpdateCallback([this]() { + LOG_INFO("Inventory updated"); + }); + + updateEngine_->start(); + + // Start event-driven Redis subscriber for multiple keys + LOG_INFO("Starting event-driven Redis subscriber..."); + + redisSubscriber_ = std::make_unique(); + + // Subscribe to multiple Redis keys for event-driven updates + std::vector keysToSubscribe = { + "DEVICE_METADATA", // Serial number, platform, hostname + "CHASSIS_STATE", // Power state + "SWITCH_HOST_STATE" // Host state (for future use) + }; + + // Register callback to UpdateEngine + auto callback = [this](const std::string& key, + const std::string& field, + const std::string& value) { + // Forward Redis events to UpdateEngine + updateEngine_->onRedisFieldChange(key, field, value); + }; + + // Start subscriber with multiple keys (use STATE_DB connection) + bool started = redisSubscriber_->startMultiKey( + configMgr_->getStateDbHost(), + configMgr_->getStateDbPort(), + keysToSubscribe, + callback + ); + + if (!started) + { + LOG_ERROR("FATAL: Failed to start event-driven Redis subscriber"); + LOG_ERROR("Cannot continue without event-driven updates"); + throw std::runtime_error("Failed to start event-driven Redis subscriber"); + } + + LOG_INFO("Event-driven Redis subscriber started successfully"); + LOG_INFO("Subscribed to %zu Redis keys for instant updates", keysToSubscribe.size()); +} + +void BridgeApp::handleSignal(const boost::system::error_code& ec, int signal) +{ + if (!ec) + { + LOG_INFO("Received signal %d", signal); + shutdown(); + } +} + +void BridgeApp::updateHealth(DataSource source, DataSourceHealth health) +{ + healthStatus_[source] = health; +} + +void BridgeApp::logHealthReport() +{ + LOG_INFO("=== Data Source Health Report ==="); + + auto printHealth = [](const std::string& name, DataSourceHealth health) { + const char* status = "Unknown"; + switch (health) + { + case DataSourceHealth::Healthy: + status = "Healthy"; + LOG_INFO(" %s: %s", name.c_str(), status); + break; + case DataSourceHealth::Degraded: + status = "Degraded"; + LOG_WARNING(" %s: %s", name.c_str(), status); + break; + case DataSourceHealth::Unavailable: + status = "Unavailable"; + LOG_WARNING(" %s: %s", name.c_str(), status); + break; + } + }; + + printHealth("CONFIG_DB", healthStatus_[DataSource::RedisConfigDb]); + printHealth("STATE_DB", healthStatus_[DataSource::RedisStateDb]); + printHealth("platform.json", healthStatus_[DataSource::PlatformJson]); + printHealth("FRU EEPROM", healthStatus_[DataSource::FruEeprom]); + + LOG_INFO("=================================="); +} + +void BridgeApp::initializeUserManager() +{ + LOG_INFO("Initializing user management..."); + + try + { + // Create user manager using user connection (separate from inventory/mapper) + // (scans /etc/passwd on construction) + userMgr_ = std::make_unique( + *userServer_, "/xyz/openbmc_project/user", objectMapper_.get()); + + // Register user manager with ObjectMapper for bmcweb discovery + if (objectMapper_) + { + objectMapper_->registerObject( + "/xyz/openbmc_project/user", + {USER_MANAGER_BUSNAME}, + USER_MANAGER_BUSNAME); + + // Register each existing user object with User.Manager service name. + // User objects are read-only; creation/deletion is handled outside + // of sonic-dbus-bridge. + for (const auto& [username, userObj] : userMgr_->getUsers()) + { + std::string userPath = "/xyz/openbmc_project/user/" + username; + objectMapper_->registerObject( + userPath, + {"xyz.openbmc_project.User.Attributes"}, + USER_MANAGER_BUSNAME); + } + } + + LOG_INFO("User management initialized successfully"); + } + catch (const std::exception& e) + { + LOG_ERROR("Failed to initialize user management: %s", e.what()); + LOG_WARNING("User management not available"); + userMgr_.reset(); + } +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/config_manager.cpp b/sonic-dbus-bridge/src/config_manager.cpp new file mode 100644 index 0000000..4017404 --- /dev/null +++ b/sonic-dbus-bridge/src/config_manager.cpp @@ -0,0 +1,37 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "config_manager.hpp" +#include "logger.hpp" +#include +#include + +namespace sonic::dbus_bridge +{ + +bool ConfigManager::load(const std::string& configPath) +{ + std::ifstream configFile(configPath); + if (!configFile.is_open()) + { + LOG_WARNING("Could not open config file: %s, using defaults", + configPath.c_str()); + return true; // Use defaults + } + + // For now, just use defaults + // TODO: Implement YAML parsing (requires yaml-cpp dependency) + // This is acceptable for MVP - config file is optional + + LOG_INFO("Using default configuration"); + return true; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/dbus_exporter.cpp b/sonic-dbus-bridge/src/dbus_exporter.cpp new file mode 100644 index 0000000..5898283 --- /dev/null +++ b/sonic-dbus-bridge/src/dbus_exporter.cpp @@ -0,0 +1,259 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "dbus_exporter.hpp" +#include "logger.hpp" + +namespace sonic::dbus_bridge +{ + +// D-Bus interface names (OpenBMC standard) +constexpr const char* IFACE_INVENTORY_CHASSIS = "xyz.openbmc_project.Inventory.Item.Chassis"; +constexpr const char* IFACE_DECORATOR_ASSET = "xyz.openbmc_project.Inventory.Decorator.Asset"; +constexpr const char* IFACE_DECORATOR_MODEL = "xyz.openbmc_project.Inventory.Decorator.Model"; +constexpr const char* IFACE_STATE_CHASSIS = "xyz.openbmc_project.State.Chassis"; +constexpr const char* IFACE_SOFTWARE_VERSION = "xyz.openbmc_project.Software.Version"; +constexpr const char* IFACE_SOFTWARE_ACTIVATION = "xyz.openbmc_project.Software.Activation"; + +// D-Bus object paths +constexpr const char* OBJ_PATH_CHASSIS = "/xyz/openbmc_project/inventory/system/chassis"; +constexpr const char* OBJ_PATH_SYSTEM = "/xyz/openbmc_project/inventory/system/system0"; +constexpr const char* OBJ_PATH_CHASSIS_STATE = "/xyz/openbmc_project/state/chassis0"; + +DBusExporter::DBusExporter(sdbusplus::asio::object_server& inventoryServer) + : inventoryServer_(inventoryServer) +{ +} + +bool DBusExporter::createObjects(const InventoryModel& model) +{ + LOG_INFO("Creating D-Bus objects..."); + + if (!createChassisObject(model.chassis)) + { + LOG_ERROR("Failed to create chassis object"); + return false; + } + + if (!createSystemObject(model.system)) + { + LOG_ERROR("Failed to create system object"); + return false; + } + + if (!createChassisStateObject(model.chassisState)) + { + LOG_ERROR("Failed to create chassis state object"); + return false; + } + + if (!model.firmwareVersions.empty()) + { + if (!createFirmwareObjects(model.firmwareVersions)) + { + LOG_WARNING("Failed to create some firmware inventory objects"); + } + } + + currentModel_ = model; + + LOG_INFO("D-Bus objects created successfully"); + return true; +} + +bool DBusExporter::updateObjects(const InventoryModel& model) +{ + // For now, just update the model + // Property change signals would be emitted here + currentModel_ = model; + return true; +} + +bool DBusExporter::createChassisObject(const ChassisInfo& chassis) +{ + // Store chassis data in currentModel_ for property getters + currentModel_.chassis = chassis; + + // Item.Chassis interface (REQUIRED by bmcweb for chassis discovery!) + auto chassisIface = inventoryServer_.add_interface(OBJ_PATH_CHASSIS, IFACE_INVENTORY_CHASSIS); + chassisIface->register_property_r( + "Type", std::string(""), + sdbusplus::vtable::property_::const_, + [](const auto&) { + return "xyz.openbmc_project.Inventory.Item.Chassis.ChassisType.RackMount"; + }); + chassisIface->initialize(); + interfaces_[std::string(OBJ_PATH_CHASSIS) + ":" + IFACE_INVENTORY_CHASSIS] = chassisIface; + + // Decorator.Asset interface + auto assetIface = inventoryServer_.add_interface(OBJ_PATH_CHASSIS, IFACE_DECORATOR_ASSET); + assetIface->register_property_r( + "SerialNumber", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.chassis.serialNumber; }); + assetIface->register_property_r( + "PartNumber", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.chassis.partNumber; }); + assetIface->register_property_r( + "Manufacturer", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.chassis.manufacturer; }); + assetIface->initialize(); + interfaces_[std::string(OBJ_PATH_CHASSIS) + ":" + IFACE_DECORATOR_ASSET] = assetIface; + + // Decorator.Model interface + auto modelIface = inventoryServer_.add_interface(OBJ_PATH_CHASSIS, IFACE_DECORATOR_MODEL); + modelIface->register_property_r( + "Model", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.chassis.model; }); + modelIface->initialize(); + interfaces_[std::string(OBJ_PATH_CHASSIS) + ":" + IFACE_DECORATOR_MODEL] = modelIface; + + LOG_INFO("Created chassis object at %s", OBJ_PATH_CHASSIS); + return true; +} + +bool DBusExporter::createSystemObject(const SystemInfo& system) +{ + // Store system data in currentModel_ for property getters + currentModel_.system = system; + + // Decorator.Asset interface + auto assetIface = inventoryServer_.add_interface(OBJ_PATH_SYSTEM, IFACE_DECORATOR_ASSET); + assetIface->register_property_r( + "SerialNumber", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.system.serialNumber; }); + assetIface->register_property_r( + "Manufacturer", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.system.manufacturer; }); + assetIface->initialize(); + interfaces_[std::string(OBJ_PATH_SYSTEM) + ":" + IFACE_DECORATOR_ASSET] = assetIface; + + // Decorator.Model interface + auto modelIface = inventoryServer_.add_interface(OBJ_PATH_SYSTEM, IFACE_DECORATOR_MODEL); + modelIface->register_property_r( + "Model", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { return currentModel_.system.model; }); + modelIface->initialize(); + interfaces_[std::string(OBJ_PATH_SYSTEM) + ":" + IFACE_DECORATOR_MODEL] = modelIface; + + LOG_INFO("Created system object at %s", OBJ_PATH_SYSTEM); + return true; +} + +bool DBusExporter::createChassisStateObject(const ChassisState& state) +{ + // Store state data in currentModel_ for property getters + currentModel_.chassisState = state; + + // State.Chassis interface + auto stateIface = inventoryServer_.add_interface(OBJ_PATH_CHASSIS_STATE, IFACE_STATE_CHASSIS); + stateIface->register_property_r( + "CurrentPowerState", std::string(""), + sdbusplus::vtable::property_::const_, + [this](const auto&) { + return (currentModel_.chassisState.powerState == "on") + ? "xyz.openbmc_project.State.Chassis.PowerState.On" + : "xyz.openbmc_project.State.Chassis.PowerState.Off"; + }); + stateIface->initialize(); + interfaces_[std::string(OBJ_PATH_CHASSIS_STATE) + ":" + IFACE_STATE_CHASSIS] = stateIface; + + LOG_INFO("Created chassis state object at %s", OBJ_PATH_CHASSIS_STATE); + return true; +} + +bool DBusExporter::createFirmwareObjects( + const std::vector& versions) +{ + currentModel_.firmwareVersions = versions; + + for (size_t i = 0; i < versions.size(); i++) + { + const auto& fw = versions[i]; + std::string objPath = "/xyz/openbmc_project/software/" + fw.id; + + try + { + std::string purposeStr; + switch (fw.purpose) + { + case FirmwarePurpose::BMC: + purposeStr = "xyz.openbmc_project.Software.Version.VersionPurpose.BMC"; + break; + case FirmwarePurpose::Host: + purposeStr = "xyz.openbmc_project.Software.Version.VersionPurpose.Host"; + break; + default: + purposeStr = "xyz.openbmc_project.Software.Version.VersionPurpose.Other"; + break; + } + + // Software.Version interface + auto versionIface = inventoryServer_.add_interface(objPath, + IFACE_SOFTWARE_VERSION); + versionIface->register_property_r( + "Version", std::string(""), + sdbusplus::vtable::property_::const_, + [this, idx = i](const auto&) { + if (idx < currentModel_.firmwareVersions.size()) + { + return currentModel_.firmwareVersions[idx].version; + } + return std::string("Unknown"); + }); + versionIface->register_property_r( + "Purpose", std::string(""), + sdbusplus::vtable::property_::const_, + [purposeStr](const auto&) { + return purposeStr; + }); + versionIface->initialize(); + interfaces_[objPath + ":" + IFACE_SOFTWARE_VERSION] = versionIface; + + // Software.Activation interface + auto activationIface = inventoryServer_.add_interface(objPath, + IFACE_SOFTWARE_ACTIVATION); + activationIface->register_property_r( + "Activation", std::string(""), + sdbusplus::vtable::property_::const_, + [](const auto&) { + return std::string( + "xyz.openbmc_project.Software.Activation.Activations.Active"); + }); + activationIface->register_property_r( + "RequestedActivation", std::string(""), + sdbusplus::vtable::property_::const_, + [](const auto&) { + return std::string( + "xyz.openbmc_project.Software.Activation.RequestedActivations.None"); + }); + activationIface->initialize(); + interfaces_[objPath + ":" + IFACE_SOFTWARE_ACTIVATION] = activationIface; + + LOG_INFO("Created firmware object at %s (version=%s, purpose=%s)", + objPath.c_str(), fw.version.c_str(), purposeStr.c_str()); + } + catch (const std::exception& e) + { + LOG_ERROR("Failed to create firmware object at %s: %s", + objPath.c_str(), e.what()); + } + } + + return true; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/fru_adapter.cpp b/sonic-dbus-bridge/src/fru_adapter.cpp new file mode 100644 index 0000000..aa81337 --- /dev/null +++ b/sonic-dbus-bridge/src/fru_adapter.cpp @@ -0,0 +1,157 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "fru_adapter.hpp" +#include "logger.hpp" +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +// ONIE TLV type codes +constexpr uint8_t TLV_CODE_PRODUCT_NAME = 0x21; +constexpr uint8_t TLV_CODE_PART_NUMBER = 0x22; +constexpr uint8_t TLV_CODE_SERIAL_NUMBER = 0x23; +constexpr uint8_t TLV_CODE_MAC_BASE = 0x24; +constexpr uint8_t TLV_CODE_MANUFACTURE_DATE = 0x25; +constexpr uint8_t TLV_CODE_DEVICE_VERSION = 0x26; +constexpr uint8_t TLV_CODE_PLATFORM_NAME = 0x28; +constexpr uint8_t TLV_CODE_ONIE_VERSION = 0x29; +constexpr uint8_t TLV_CODE_MANUFACTURER = 0x2B; +constexpr uint8_t TLV_CODE_COUNTRY_CODE = 0x2C; +constexpr uint8_t TLV_CODE_VENDOR = 0x2D; +constexpr uint8_t TLV_CODE_MODEL = 0x2E; +constexpr uint8_t TLV_CODE_CRC32 = 0xFE; + +FruAdapter::FruAdapter(const std::vector& eepromPaths) + : eepromPaths_(eepromPaths) +{ +} + +bool FruAdapter::scanAndLoad() +{ + LOG_INFO("Scanning for FRU EEPROMs..."); + + for (const auto& path : eepromPaths_) + { + LOG_DEBUG("Trying FRU EEPROM: %s", path.c_str()); + if (readEeprom(path)) + { + LOG_INFO("Successfully read FRU from: %s", path.c_str()); + loaded_ = true; + return true; + } + } + + LOG_WARNING("No FRU EEPROM found"); + return false; +} + +bool FruAdapter::readEeprom(const std::string& path) +{ + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) + { + return false; + } + + // Read entire EEPROM + std::vector data((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + if (data.size() < 11) // Minimum TLV header size + { + return false; + } + + // Validate TLV header + if (data[0] != 'T' || data[1] != 'l' || data[2] != 'v' || + data[3] != 'I' || data[4] != 'n' || data[5] != 'f' || + data[6] != 'o' || data[7] != 0x00) + { + LOG_DEBUG("Invalid TLV header in EEPROM"); + return false; + } + + // Parse TLV data + fruInfo_ = parseTlv(data); + return true; +} + +FruInfo FruAdapter::parseTlv(const std::vector& data) +{ + FruInfo info; + + // Skip header (8 bytes) and version (1 byte) + size_t offset = 9; + + // Total length is at bytes 9-10 (big-endian) + uint16_t totalLen = (data[9] << 8) | data[10]; + offset = 11; + + while (offset < data.size() && offset < (11 + static_cast(totalLen))) + { + if (offset + 2 > data.size()) break; + + uint8_t type = data[offset]; + uint8_t len = data[offset + 1]; + offset += 2; + + if (offset + len > data.size()) break; + + std::string value(reinterpret_cast(&data[offset]), len); + + switch (type) + { + case TLV_CODE_PRODUCT_NAME: + info.productName = value; + break; + case TLV_CODE_PART_NUMBER: + info.partNumber = value; + break; + case TLV_CODE_SERIAL_NUMBER: + info.serialNumber = value; + break; + case TLV_CODE_MANUFACTURE_DATE: + info.manufactureDate = value; + break; + case TLV_CODE_DEVICE_VERSION: + info.hardwareVersion = value; + break; + case TLV_CODE_MANUFACTURER: + info.manufacturer = value; + break; + case TLV_CODE_MODEL: + info.model = value; + break; + case TLV_CODE_CRC32: + // End of TLV + return info; + default: + // Unknown type, skip + break; + } + + offset += len; + } + + return info; +} + +bool FruAdapter::validateCrc(const std::vector& /* data */) +{ + // TODO: Implement CRC32 validation + // For MVP, skip CRC validation + return true; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/inventory_model.cpp b/sonic-dbus-bridge/src/inventory_model.cpp new file mode 100644 index 0000000..4fe423b --- /dev/null +++ b/sonic-dbus-bridge/src/inventory_model.cpp @@ -0,0 +1,267 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "inventory_model.hpp" +#include "logger.hpp" + +namespace sonic::dbus_bridge +{ + +// Helper function to convert FieldSource to string +static const char* fieldSourceToString(FieldSource source) +{ + switch (source) + { + case FieldSource::Redis: return "Redis"; + case FieldSource::FruEeprom: return "FRU EEPROM"; + case FieldSource::PlatformJson: return "platform.json"; + case FieldSource::Default: return "Default"; + default: return "Unknown"; + } +} + +InventoryModel InventoryModelBuilder::build( + const std::optional& fruInfo, + const std::optional& deviceMetadata, + const std::optional& platformDesc, + const std::optional& chassisState) +{ + InventoryModel model; + + model.chassis = buildChassisInfo(fruInfo, deviceMetadata, platformDesc); + model.system = buildSystemInfo(fruInfo, deviceMetadata); + model.chassisState = chassisState.value_or(ChassisState{}); + model.psus = buildPsuList(platformDesc); + model.fans = buildFanList(platformDesc); + + return model; +} + +ChassisInfo InventoryModelBuilder::buildChassisInfo( + const std::optional& fruInfo, + const std::optional& deviceMetadata, + const std::optional& platformDesc) +{ + ChassisInfo chassis; + + // Serial number: Redis > FRU > platform.json > default + if (deviceMetadata && deviceMetadata->serialNumber) + { + chassis.serialNumber = *deviceMetadata->serialNumber; + chassis.serialNumberSource = FieldSource::Redis; + } + else if (fruInfo && fruInfo->serialNumber) + { + chassis.serialNumber = *fruInfo->serialNumber; + chassis.serialNumberSource = FieldSource::FruEeprom; + } + // No platform.json fallback for serial number + + // Part number: Redis > FRU > platform.json > hwsku > default + if (deviceMetadata && deviceMetadata->partNumber) + { + chassis.partNumber = *deviceMetadata->partNumber; + chassis.partNumberSource = FieldSource::Redis; + } + else if (fruInfo && fruInfo->partNumber) + { + chassis.partNumber = *fruInfo->partNumber; + chassis.partNumberSource = FieldSource::FruEeprom; + } + else if (platformDesc && platformDesc->chassisPartNumber) + { + chassis.partNumber = *platformDesc->chassisPartNumber; + chassis.partNumberSource = FieldSource::PlatformJson; + } + else if (deviceMetadata && deviceMetadata->hwsku) + { + chassis.partNumber = *deviceMetadata->hwsku; + chassis.partNumberSource = FieldSource::Redis; + } + + // Manufacturer: Redis > FRU > platform.json > default + if (deviceMetadata && deviceMetadata->manufacturer) + { + chassis.manufacturer = *deviceMetadata->manufacturer; + chassis.manufacturerSource = FieldSource::Redis; + } + else if (fruInfo && fruInfo->manufacturer) + { + chassis.manufacturer = *fruInfo->manufacturer; + chassis.manufacturerSource = FieldSource::FruEeprom; + } + // No platform.json fallback for manufacturer + + // Model: Redis model > Redis platform > FRU > platform.json > default + if (deviceMetadata && deviceMetadata->model) + { + chassis.model = *deviceMetadata->model; + chassis.modelSource = FieldSource::Redis; + } + else if (deviceMetadata && deviceMetadata->platform) + { + chassis.model = *deviceMetadata->platform; + chassis.modelSource = FieldSource::Redis; + } + else if (fruInfo && fruInfo->model) + { + chassis.model = *fruInfo->model; + chassis.modelSource = FieldSource::FruEeprom; + } + // No platform.json fallback for model + + // Hardware version: Redis > FRU > platform.json > default + if (fruInfo && fruInfo->hardwareVersion) + { + chassis.hardwareVersion = *fruInfo->hardwareVersion; + } + else if (platformDesc && platformDesc->chassisHardwareVersion) + { + chassis.hardwareVersion = *platformDesc->chassisHardwareVersion; + } + + // Pretty name: platform.json > FRU product name > default + if (platformDesc && !platformDesc->chassisName.empty()) + { + chassis.prettyName = platformDesc->chassisName; + } + else if (fruInfo && fruInfo->productName) + { + chassis.prettyName = *fruInfo->productName; + } + + // Log data sources + LOG_INFO("Chassis Data Sources:"); + LOG_INFO(" SerialNumber: \"%s\" (from %s)", + chassis.serialNumber.c_str(), fieldSourceToString(chassis.serialNumberSource)); + LOG_INFO(" PartNumber: \"%s\" (from %s)", + chassis.partNumber.c_str(), fieldSourceToString(chassis.partNumberSource)); + LOG_INFO(" Manufacturer: \"%s\" (from %s)", + chassis.manufacturer.c_str(), fieldSourceToString(chassis.manufacturerSource)); + LOG_INFO(" Model: \"%s\" (from %s)", + chassis.model.c_str(), fieldSourceToString(chassis.modelSource)); + + return chassis; +} + +SystemInfo InventoryModelBuilder::buildSystemInfo( + const std::optional& fruInfo, + const std::optional& deviceMetadata) +{ + SystemInfo system; + + // Serial number: FRU > CONFIG_DB > default + if (fruInfo && fruInfo->serialNumber) + { + system.serialNumber = *fruInfo->serialNumber; + } + else if (deviceMetadata && deviceMetadata->serialNumber) + { + system.serialNumber = *deviceMetadata->serialNumber; + } + + // Manufacturer: FRU > CONFIG_DB > default + if (fruInfo && fruInfo->manufacturer) + { + system.manufacturer = *fruInfo->manufacturer; + } + else if (deviceMetadata && deviceMetadata->manufacturer) + { + system.manufacturer = *deviceMetadata->manufacturer; + } + + // Model: FRU > CONFIG_DB > default + if (fruInfo && fruInfo->model) + { + system.model = *fruInfo->model; + } + else if (deviceMetadata && deviceMetadata->platform) + { + system.model = *deviceMetadata->platform; + } + + // Hostname: CONFIG_DB > default + if (deviceMetadata && deviceMetadata->hostname) + { + system.hostname = *deviceMetadata->hostname; + system.prettyName = *deviceMetadata->hostname; + } + + return system; +} + +std::vector InventoryModelBuilder::buildPsuList( + const std::optional& platformDesc) +{ + std::vector psus; + + if (platformDesc) + { + for (const auto& name : platformDesc->psuNames) + { + PsuInfo psu; + psu.name = name; + psu.present = false; // Will be updated by sensors later + psus.push_back(psu); + } + } + + return psus; +} + +std::vector InventoryModelBuilder::buildFanList( + const std::optional& platformDesc) +{ + std::vector fans; + + if (platformDesc) + { + for (const auto& name : platformDesc->fanNames) + { + FanInfo fan; + fan.name = name; + fan.present = false; // Will be updated by sensors later + fans.push_back(fan); + } + } + + return fans; +} + +bool hasChanged(const InventoryModel& oldModel, const InventoryModel& newModel) +{ + // Compare chassis info + if (oldModel.chassis.serialNumber != newModel.chassis.serialNumber || + oldModel.chassis.partNumber != newModel.chassis.partNumber || + oldModel.chassis.manufacturer != newModel.chassis.manufacturer || + oldModel.chassis.model != newModel.chassis.model) + { + return true; + } + + // Compare system info + if (oldModel.system.serialNumber != newModel.system.serialNumber || + oldModel.system.manufacturer != newModel.system.manufacturer || + oldModel.system.model != newModel.system.model || + oldModel.system.hostname != newModel.system.hostname) + { + return true; + } + + // Compare chassis state + if (oldModel.chassisState.powerState != newModel.chassisState.powerState) + { + return true; + } + + return false; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/main.cpp b/sonic-dbus-bridge/src/main.cpp new file mode 100644 index 0000000..603f150 --- /dev/null +++ b/sonic-dbus-bridge/src/main.cpp @@ -0,0 +1,95 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "bridge_app.hpp" +#include "logger.hpp" +#include +#include +#include + +using namespace sonic::dbus_bridge; + +namespace +{ +constexpr const char* VERSION = "1.0.0"; +constexpr const char* DEFAULT_CONFIG_PATH = "/etc/sonic-dbus-bridge/config.yaml"; + +void printUsage(const char* progName) +{ + std::cout << "Usage: " << progName << " [OPTIONS]\n\n" + << "SONiC to D-Bus Inventory Bridge\n\n" + << "Options:\n" + << " -c, --config PATH Config file path (default: " << DEFAULT_CONFIG_PATH << ")\n" + << " -v, --version Print version and exit\n" + << " -h, --help Show this help message\n" + << std::endl; +} + +void printVersion() +{ + std::cout << "sonic-dbus-bridge version " << VERSION << std::endl; +} + +} // anonymous namespace + +int main(int argc, char* argv[]) +{ + openlog("sonic-dbus-bridge", LOG_PID | LOG_NDELAY, LOG_DAEMON); + std::string configPath = DEFAULT_CONFIG_PATH; + + // Parse command-line arguments + static struct option longOptions[] = { + {"config", required_argument, nullptr, 'c'}, + {"version", no_argument, nullptr, 'v'}, + {"help", no_argument, nullptr, 'h'}, + {nullptr, 0, nullptr, 0} + }; + + int opt; + while ((opt = getopt_long(argc, argv, "c:vh", longOptions, nullptr)) != -1) + { + switch (opt) + { + case 'c': + configPath = optarg; + break; + case 'v': + printVersion(); + return EXIT_SUCCESS; + case 'h': + printUsage(argv[0]); + return EXIT_SUCCESS; + default: + printUsage(argv[0]); + return EXIT_FAILURE; + } + } + + try + { + LOG_INFO("Starting sonic-dbus-bridge with config: %s", configPath.c_str()); + + BridgeApp app(configPath); + + if (!app.initialize()) + { + LOG_ERROR("Failed to initialize application"); + return EXIT_FAILURE; + } + + LOG_INFO("Initialization complete, entering main loop..."); + return app.run(); + } + catch (const std::exception& e) + { + LOG_ERROR("Fatal error: %s", e.what()); + return EXIT_FAILURE; + } +} + diff --git a/sonic-dbus-bridge/src/object_mapper.cpp b/sonic-dbus-bridge/src/object_mapper.cpp new file mode 100644 index 0000000..31420f6 --- /dev/null +++ b/sonic-dbus-bridge/src/object_mapper.cpp @@ -0,0 +1,249 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "object_mapper.hpp" +#include "config.h" +#include "logger.hpp" + +#include + +namespace sonic::dbus_bridge +{ + +namespace +{ + +constexpr const char* OBJMAPPER_PATH = "/xyz/openbmc_project/object_mapper"; +constexpr const char* OBJMAPPER_IFACE = "xyz.openbmc_project.ObjectMapper"; + +} // namespace + +ObjectMapperService::ObjectMapperService(sdbusplus::asio::object_server& server) + : server_(server) +{ +} + +bool ObjectMapperService::initialize() +{ + mapperIface_ = server_.add_interface(OBJMAPPER_PATH, OBJMAPPER_IFACE); + + mapperIface_->register_method( + "GetObject", + [this](const std::string& path, + const std::vector& interfaces) { + return getObject(path, interfaces); + }); + + mapperIface_->register_method( + "GetSubTree", + [this](const std::string& subtree, int32_t depth, + const std::vector& interfaces) { + return getSubTree(subtree, depth, interfaces); + }); + + mapperIface_->register_method( + "GetSubTreePaths", + [this](const std::string& subtree, int32_t depth, + const std::vector& interfaces) { + return getSubTreePaths(subtree, depth, interfaces); + }); + + mapperIface_->register_method( + "GetAssociatedSubTreePaths", + [this](const std::string& associatedPath, const std::string& subtree, + int32_t depth, const std::vector& interfaces) { + return getAssociatedSubTreePaths(associatedPath, subtree, depth, + interfaces); + }); + + mapperIface_->initialize(); + + LOG_INFO("Registered minimal ObjectMapper at %s", OBJMAPPER_PATH); + return true; +} + +void ObjectMapperService::registerObject( + const std::string& path, const std::vector& interfaces, + const std::string& serviceName) +{ + objects_[path] = {interfaces, + serviceName.empty() ? INVENTORY_MANAGER_BUSNAME + : serviceName}; +} + +void ObjectMapperService::unregisterObject(const std::string& path) +{ + objects_.erase(path); +} + +bool ObjectMapperService::pathIsUnder(const std::string& root, + const std::string& path) +{ + if (root.empty() || root == "/") + { + return true; + } + + // Normalize root by removing trailing slash + std::string normalizedRoot = root; + while (!normalizedRoot.empty() && normalizedRoot.back() == '/') + { + normalizedRoot.pop_back(); + } + + if (normalizedRoot.empty()) + { + return true; + } + + if (path.size() < normalizedRoot.size()) + { + return false; + } + if (path.compare(0, normalizedRoot.size(), normalizedRoot) != 0) + { + return false; + } + if (path.size() == normalizedRoot.size()) + { + return true; + } + return path[normalizedRoot.size()] == '/'; +} + +ObjectMapperService::GetObjectResult ObjectMapperService::getObject( + const std::string& path, const std::vector& interfaces) +{ + GetObjectResult result; + + auto it = objects_.find(path); + if (it == objects_.end()) + { + return result; + } + + const auto& objInfo = it->second; + + // Filter interfaces if filter list is provided + std::vector matchedIfaces; + if (interfaces.empty()) + { + matchedIfaces = objInfo.interfaces; + } + else + { + for (const auto& iface : objInfo.interfaces) + { + if (std::find(interfaces.begin(), interfaces.end(), iface) != + interfaces.end()) + { + matchedIfaces.push_back(iface); + } + } + } + + if (!matchedIfaces.empty()) + { + result[objInfo.serviceName] = matchedIfaces; + } + + return result; +} + +ObjectMapperService::GetSubTreeResult ObjectMapperService::getSubTree( + const std::string& subtree, int32_t /*depth*/, + const std::vector& interfaces) +{ + GetSubTreeResult result; + + for (const auto& [path, objInfo] : objects_) + { + if (!pathIsUnder(subtree, path)) + { + continue; + } + + // Filter interfaces if filter list is provided + std::vector matchedIfaces; + if (interfaces.empty()) + { + matchedIfaces = objInfo.interfaces; + } + else + { + for (const auto& iface : objInfo.interfaces) + { + if (std::find(interfaces.begin(), interfaces.end(), iface) != + interfaces.end()) + { + matchedIfaces.push_back(iface); + } + } + } + + if (!matchedIfaces.empty()) + { + result[path][objInfo.serviceName] = matchedIfaces; + } + } + + return result; +} + +ObjectMapperService::GetSubTreePathsResult ObjectMapperService::getSubTreePaths( + const std::string& subtree, int32_t /*depth*/, + const std::vector& interfaces) +{ + GetSubTreePathsResult result; + + for (const auto& [path, objInfo] : objects_) + { + if (!pathIsUnder(subtree, path)) + { + continue; + } + + // Check if interfaces match (if filter provided) + bool matches = interfaces.empty(); + if (!matches) + { + for (const auto& iface : objInfo.interfaces) + { + if (std::find(interfaces.begin(), interfaces.end(), iface) != + interfaces.end()) + { + matches = true; + break; + } + } + } + + if (matches) + { + result.push_back(path); + } + } + + return result; +} + +ObjectMapperService::GetSubTreePathsResult + ObjectMapperService::getAssociatedSubTreePaths( + const std::string& /*associatedPath*/, const std::string& /*subtree*/, + int32_t /*depth*/, const std::vector& /*interfaces*/) +{ + // We don't currently create any association objects, so simply + // return an empty array of paths. This is sufficient for bmcweb's + // chassis connectivity helpers, which treat empty results as + // "no associations" without raising errors. + return {}; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/platform_json_adapter.cpp b/sonic-dbus-bridge/src/platform_json_adapter.cpp new file mode 100644 index 0000000..b32afcb --- /dev/null +++ b/sonic-dbus-bridge/src/platform_json_adapter.cpp @@ -0,0 +1,174 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "platform_json_adapter.hpp" +#include "logger.hpp" +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +PlatformJsonAdapter::PlatformJsonAdapter(const std::string& platformJsonPath) + : platformJsonPath_(platformJsonPath) +{ +} + +bool PlatformJsonAdapter::load() +{ + std::string expandedPath = expandPath(platformJsonPath_); + + LOG_INFO("Loading platform.json from: %s", expandedPath.c_str()); + + if (!parseJson(expandedPath)) + { + LOG_WARNING("Failed to load platform.json, using defaults"); + return false; + } + + loaded_ = true; + return true; +} + +std::string PlatformJsonAdapter::expandPath(const std::string& path) const +{ + std::string result = path; + + // Replace ${PLATFORM} with environment variable + size_t pos = result.find("${PLATFORM}"); + if (pos != std::string::npos) + { + const char* platform = std::getenv("PLATFORM"); + if (platform) + { + result.replace(pos, 11, platform); + } + else + { + // Default platform for ASPEED + result.replace(pos, 11, "arm64-aspeed_ast2700-r0"); + } + } + + return result; +} + +bool PlatformJsonAdapter::parseJson(const std::string& path) +{ + std::ifstream file(path); + if (!file.is_open()) + { + return false; + } + + Json::Value root; + Json::CharReaderBuilder builder; + std::string errs; + + if (!Json::parseFromStream(builder, file, &root, &errs)) + { + LOG_ERROR("JSON parse error: %s", errs.c_str()); + return false; + } + + // Extract chassis name + if (root.isMember("chassis") && root["chassis"].isMember("name")) + { + description_.chassisName = root["chassis"]["name"].asString(); + } + + // Extract chassis part number + if (root.isMember("chassis") && root["chassis"].isMember("part_number")) + { + description_.chassisPartNumber = root["chassis"]["part_number"].asString(); + } + + // Extract chassis hardware version + if (root.isMember("chassis") && root["chassis"].isMember("hardware_version")) + { + description_.chassisHardwareVersion = root["chassis"]["hardware_version"].asString(); + } + + // Extract PSU names + if (root.isMember("chassis") && root["chassis"].isMember("psus")) + { + const Json::Value& psus = root["chassis"]["psus"]; + for (const auto& psu : psus) + { + if (psu.isMember("name")) + { + description_.psuNames.push_back(psu["name"].asString()); + } + } + } + + // Extract fan names + if (root.isMember("chassis") && root["chassis"].isMember("fans")) + { + const Json::Value& fans = root["chassis"]["fans"]; + for (const auto& fan : fans) + { + if (fan.isMember("name")) + { + description_.fanNames.push_back(fan["name"].asString()); + } + } + } + + // Extract thermal names + if (root.isMember("chassis") && root["chassis"].isMember("thermals")) + { + const Json::Value& thermals = root["chassis"]["thermals"]; + for (const auto& thermal : thermals) + { + if (thermal.isMember("name")) + { + description_.thermalNames.push_back(thermal["name"].asString()); + } + } + } + + return true; +} + +PlatformDescription PlatformJsonAdapter::getPlatformDescription() const +{ + return description_; +} + +std::optional PlatformJsonAdapter::getChassisName() const +{ + if (!loaded_ || description_.chassisName.empty()) + { + return std::nullopt; + } + return description_.chassisName; +} + +std::optional PlatformJsonAdapter::getChassisPartNumber() const +{ + if (!loaded_) + { + return std::nullopt; + } + return description_.chassisPartNumber; +} + +std::optional PlatformJsonAdapter::getChassisHardwareVersion() const +{ + if (!loaded_) + { + return std::nullopt; + } + return description_.chassisHardwareVersion; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/redis_adapter.cpp b/sonic-dbus-bridge/src/redis_adapter.cpp new file mode 100644 index 0000000..43c73ad --- /dev/null +++ b/sonic-dbus-bridge/src/redis_adapter.cpp @@ -0,0 +1,373 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "redis_adapter.hpp" +#include "logger.hpp" +#include +#include + +namespace sonic::dbus_bridge +{ + +RedisAdapter::RedisAdapter(const std::string& configDbHost, int configDbPort, + const std::string& stateDbHost, int stateDbPort) + : configDbHost_(configDbHost), configDbPort_(configDbPort), + stateDbHost_(stateDbHost), stateDbPort_(stateDbPort) +{ +} + +RedisAdapter::~RedisAdapter() +{ + if (configDbContext_) + { + redisFree(configDbContext_); + } + if (stateDbContext_) + { + redisFree(stateDbContext_); + } +} + +bool RedisAdapter::connect() +{ + LOG_INFO("Connecting to Redis databases..."); + + // Connect to CONFIG_DB (DB 4) + configDbContext_ = connectToDb(configDbHost_, configDbPort_, 4); + if (configDbContext_) + { + LOG_INFO("Connected to CONFIG_DB"); + } + else + { + LOG_WARNING("Failed to connect to CONFIG_DB"); + } + + // Connect to STATE_DB (DB 6) + stateDbContext_ = connectToDb(stateDbHost_, stateDbPort_, 6); + if (stateDbContext_) + { + LOG_INFO("Connected to STATE_DB"); + } + else + { + LOG_WARNING("Failed to connect to STATE_DB"); + } + + // Return true if at least one connection succeeded + return (configDbContext_ != nullptr) || (stateDbContext_ != nullptr); +} + +redisContext* RedisAdapter::connectToDb(const std::string& host, int port, int dbIndex) +{ + struct timeval timeout = {2, 0}; // 2 seconds + redisContext* ctx = nullptr; + bool connected = false; + + // Try TCP connection first (most reliable for SONiC) + LOG_DEBUG("Attempting TCP connection to %s:%d...", host.c_str(), port); + ctx = redisConnectWithTimeout(host.c_str(), port, timeout); + + if (!ctx) + { + LOG_ERROR("TCP: Failed to allocate Redis context (out of memory?)"); + } + else if (ctx->err) + { + LOG_DEBUG("TCP: Connection failed: %s (errno: %d)", ctx->errstr, ctx->err); + redisFree(ctx); + ctx = nullptr; + } + else + { + LOG_INFO("Connected to Redis via TCP: %s:%d", host.c_str(), port); + connected = true; + } + + // If TCP failed, try Unix socket as fallback + if (!connected) + { + const char* unixSockets[] = { + "/var/run/redis/redis.sock", + "/run/redis/redis.sock", + "/var/run/redis.sock", + nullptr + }; + + for (int i = 0; unixSockets[i] != nullptr && !connected; i++) + { + LOG_DEBUG("Attempting Unix socket connection to %s...", unixSockets[i]); + ctx = redisConnectUnixWithTimeout(unixSockets[i], timeout); + + if (!ctx) + { + LOG_ERROR("Unix socket: Failed to allocate Redis context"); + } + else if (ctx->err) + { + LOG_DEBUG("Unix socket: Connection failed: %s (errno: %d)", ctx->errstr, ctx->err); + redisFree(ctx); + ctx = nullptr; + } + else + { + LOG_INFO("Connected to Redis via Unix socket: %s", unixSockets[i]); + connected = true; + } + } + } + + if (!connected || !ctx) + { + LOG_ERROR("All Redis connection attempts failed"); + return nullptr; + } + + // Select database + LOG_DEBUG("Selecting Redis database %d...", dbIndex); + redisReply* reply = static_cast( + redisCommand(ctx, "SELECT %d", dbIndex)); + + if (!reply) + { + LOG_ERROR("Failed to send SELECT command (connection lost?)"); + redisFree(ctx); + return nullptr; + } + + if (reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR("Failed to select DB %d: %s", dbIndex, reply->str); + freeReplyObject(reply); + redisFree(ctx); + return nullptr; + } + + freeReplyObject(reply); + LOG_DEBUG("Selected database %d", dbIndex); + return ctx; +} + +DeviceMetadata RedisAdapter::getDeviceMetadata() +{ + DeviceMetadata metadata; + + if (!configDbContext_) + { + return metadata; // Return empty metadata + } + + // Read DEVICE_METADATA|localhost hash + auto fields = hgetall(configDbContext_, "DEVICE_METADATA|localhost"); + + if (!fields.empty()) + { + if (fields.count("platform")) metadata.platform = fields["platform"]; + if (fields.count("hwsku")) metadata.hwsku = fields["hwsku"]; + if (fields.count("hostname")) metadata.hostname = fields["hostname"]; + if (fields.count("mac")) metadata.mac = fields["mac"]; + if (fields.count("type")) metadata.type = fields["type"]; + if (fields.count("manufacturer")) metadata.manufacturer = fields["manufacturer"]; + if (fields.count("serial_number")) metadata.serialNumber = fields["serial_number"]; + if (fields.count("part_number")) metadata.partNumber = fields["part_number"]; + if (fields.count("model")) metadata.model = fields["model"]; + } + + return metadata; +} + +ChassisState RedisAdapter::getChassisState() +{ + ChassisState state; + + if (!stateDbContext_) + { + return state; // Return default state (on) + } + + // Try to read chassis state from STATE_DB + auto powerState = hget(stateDbContext_, "CHASSIS_STATE|chassis0", "power_state"); + if (powerState) + { + state.powerState = *powerState; + } + + return state; +} + +std::map RedisAdapter::hgetall(redisContext* ctx, + const std::string& key) +{ + std::map result; + + redisReply* reply = static_cast( + redisCommand(ctx, "HGETALL %s", key.c_str())); + + if (!reply) + { + return result; + } + + if (reply->type == REDIS_REPLY_ARRAY && reply->elements > 0) + { + for (size_t i = 0; i < reply->elements; i += 2) + { + if (i + 1 < reply->elements) + { + std::string field(reply->element[i]->str, reply->element[i]->len); + std::string value(reply->element[i+1]->str, reply->element[i+1]->len); + result[field] = value; + } + } + } + + freeReplyObject(reply); + return result; +} + +std::optional RedisAdapter::hget(redisContext* ctx, + const std::string& key, + const std::string& field) +{ + redisReply* reply = static_cast( + redisCommand(ctx, "HGET %s %s", key.c_str(), field.c_str())); + + if (!reply) + { + return std::nullopt; + } + + std::optional result; + if (reply->type == REDIS_REPLY_STRING) + { + result = std::string(reply->str, reply->len); + } + + freeReplyObject(reply); + return result; +} + +static std::string readSonicVersionField(const std::string& field) +{ + std::ifstream f("/etc/sonic/sonic_version.yml"); + if (!f.is_open()) + { + return ""; + } + std::string line; + while (std::getline(f, line)) + { + auto pos = line.find(':'); + if (pos == std::string::npos) + { + continue; + } + std::string key = line.substr(0, pos); + // Trim whitespace from key + while (!key.empty() && (key.front() == ' ' || key.front() == '\t')) + key.erase(key.begin()); + while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) + key.pop_back(); + + if (key != field) + { + continue; + } + + std::string val = line.substr(pos + 1); + // Trim whitespace and quotes + while (!val.empty() && (val.front() == ' ' || val.front() == '\t' || val.front() == '\'')) + val.erase(val.begin()); + while (!val.empty() && (val.back() == ' ' || val.back() == '\t' || val.back() == '\'' || val.back() == '\r')) + val.pop_back(); + return val; + } + return ""; +} + +std::vector RedisAdapter::getFirmwareVersions() +{ + std::vector versions; + + // 1. SONiC OS version — from switch's Redis STATE_DB + { + std::string sonicVersion = "N/A"; + if (stateDbContext_) + { + auto ver = hget(stateDbContext_, "BMC_FW_INVENTORY|SONIC_OS", "version"); + if (ver && !ver->empty()) + { + sonicVersion = *ver; + } + else + { + LOG_WARNING("FirmwareInventory: switch not found in STATE_DB"); + } + } + FirmwareVersionInfo fw; + fw.id = "switch"; + fw.version = sonicVersion; + fw.purpose = FirmwarePurpose::Host; + versions.push_back(fw); + LOG_INFO("FirmwareInventory: switch = %s", sonicVersion.c_str()); + } + + // 2. BMC firmware version — local to BMC, read from /etc/sonic/sonic_version.yml + { + std::string bmcVer = readSonicVersionField("build_version"); + if (bmcVer.empty()) + { + bmcVer = "N/A"; + LOG_WARNING("FirmwareInventory: BMC version not found in /etc/sonic/sonic_version.yml"); + } + FirmwareVersionInfo fw; + fw.id = "bmc"; + fw.version = bmcVer; + fw.purpose = FirmwarePurpose::BMC; + versions.push_back(fw); + LOG_INFO("FirmwareInventory: bmc = %s", bmcVer.c_str()); + } + + // 3. BIOS version — from switch's Redis STATE_DB + { + std::string biosVer = "N/A"; + if (stateDbContext_) + { + auto ver = hget(stateDbContext_, "BMC_FW_INVENTORY|BIOS", "version"); + if (ver && !ver->empty()) + { + biosVer = *ver; + } + else + { + LOG_WARNING("FirmwareInventory: bios not found in STATE_DB"); + } + } + FirmwareVersionInfo fw; + fw.id = "bios"; + fw.version = biosVer; + fw.purpose = FirmwarePurpose::Other; + versions.push_back(fw); + LOG_INFO("FirmwareInventory: bios = %s", biosVer.c_str()); + } + + LOG_INFO("FirmwareInventory: Total %zu firmware entries", versions.size()); + return versions; +} + +void RedisAdapter::freeReply(void* reply) +{ + if (reply) + { + freeReplyObject(reply); + } +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/redis_state_publisher.cpp b/sonic-dbus-bridge/src/redis_state_publisher.cpp new file mode 100644 index 0000000..424c5ec --- /dev/null +++ b/sonic-dbus-bridge/src/redis_state_publisher.cpp @@ -0,0 +1,325 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "redis_state_publisher.hpp" +#include "logger.hpp" +#include +#include +#include +#include + +namespace sonic::dbus_bridge +{ + +RedisStatePublisher::RedisStatePublisher() + : stateDbContext_(nullptr), requestCounter_(0) +{ + LOG_INFO( "[RedisStatePublisher] Constructor called"); +} + +RedisStatePublisher::~RedisStatePublisher() +{ + LOG_INFO( "[RedisStatePublisher] Destructor called"); + if (stateDbContext_) + { + LOG_INFO( "[RedisStatePublisher] Closing Redis connection"); + redisFree(stateDbContext_); + stateDbContext_ = nullptr; + } +} + +bool RedisStatePublisher::connect(const std::string& host, int port) +{ + LOG_INFO( "[RedisStatePublisher] Connecting to Redis at %s:%d", host.c_str(), port); + + struct timeval timeout = {2, 0}; // 2 seconds timeout + bool connected = false; + + // Try TCP first (same pattern as RedisAdapter) + stateDbContext_ = redisConnectWithTimeout(host.c_str(), port, timeout); + if (!stateDbContext_) + { + LOG_ERROR( "[RedisStatePublisher] TCP: Redis connection failed: allocation error"); + } + else if (stateDbContext_->err) + { + LOG_DEBUG( "[RedisStatePublisher] TCP: connection failed: %s (errno: %d)", + stateDbContext_->errstr, stateDbContext_->err); + redisFree(stateDbContext_); + stateDbContext_ = nullptr; + } + else + { + LOG_INFO( "[RedisStatePublisher] Connected to Redis via TCP: %s:%d", + host.c_str(), port); + connected = true; + } + + // If TCP failed, fall back to Unix domain sockets under /var/run/redis + if (!connected) + { + const char* unixSockets[] = { + "/var/run/redis/redis.sock", + "/run/redis/redis.sock", + "/var/run/redis.sock", + nullptr + }; + + for (int i = 0; unixSockets[i] != nullptr && !connected; ++i) + { + LOG_DEBUG( "[RedisStatePublisher] Unix socket: attempting %s", unixSockets[i]); + stateDbContext_ = redisConnectUnixWithTimeout(unixSockets[i], timeout); + + if (!stateDbContext_) + { + LOG_ERROR( "[RedisStatePublisher] Unix socket: failed to allocate Redis context"); + } + else if (stateDbContext_->err) + { + LOG_DEBUG( "[RedisStatePublisher] Unix socket: connection failed: %s (errno: %d)", + stateDbContext_->errstr, stateDbContext_->err); + redisFree(stateDbContext_); + stateDbContext_ = nullptr; + } + else + { + LOG_INFO( "[RedisStatePublisher] Connected to Redis via Unix socket: %s", + unixSockets[i]); + connected = true; + } + } + } + + if (!connected || !stateDbContext_) + { + LOG_ERROR( "[RedisStatePublisher] All Redis connection attempts failed"); + return false; + } + + // Select STATE_DB (DB 6) + LOG_INFO( "[RedisStatePublisher] Selecting STATE_DB (DB 6)"); + redisReply* reply = (redisReply*)redisCommand(stateDbContext_, "SELECT 6"); + + if (!reply) + { + LOG_ERROR( "[RedisStatePublisher] SELECT command failed: connection lost"); + redisFree(stateDbContext_); + stateDbContext_ = nullptr; + return false; + } + + if (reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStatePublisher] SELECT command failed: %s", reply->str); + freeReplyObject(reply); + redisFree(stateDbContext_); + stateDbContext_ = nullptr; + return false; + } + + freeReplyObject(reply); + LOG_INFO( "[RedisStatePublisher] STATE_DB (DB 6) selected successfully"); + + return true; +} + +std::string RedisStatePublisher::generateRequestId() +{ + auto now = std::chrono::system_clock::now(); + auto timestamp = std::chrono::duration_cast( + now.time_since_epoch()).count(); + + std::lock_guard lock(redisMutex_); + requestCounter_++; + + std::ostringstream oss; + oss << "req_" << timestamp << "_" << std::setfill('0') << std::setw(6) << requestCounter_; + + std::string requestId = oss.str(); + LOG_DEBUG( "[RedisStatePublisher] Generated request ID: %s", requestId.c_str()); + + return requestId; +} + +std::string RedisStatePublisher::publishHostRequest(const std::string& transition) +{ + LOG_INFO( "[RedisStatePublisher] ========================================"); + LOG_INFO( "[RedisStatePublisher] Publishing host transition request"); + LOG_INFO( "[RedisStatePublisher] Transition: %s", transition.c_str()); + + std::string requestId = generateRequestId(); + auto timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + + std::map fields = { + {"requested_transition", transition}, + {"request_id", requestId}, + {"timestamp", std::to_string(timestamp)}, + {"status", "pending"} + }; + + LOG_INFO( "[RedisStatePublisher] Request details:"); + LOG_INFO( "[RedisStatePublisher] - request_id: %s", requestId.c_str()); + LOG_INFO( "[RedisStatePublisher] - requested_transition: %s", transition.c_str()); + LOG_INFO( "[RedisStatePublisher] - timestamp: %ld", timestamp); + LOG_INFO( "[RedisStatePublisher] - status: pending"); + + if (!hmset("BMC_HOST_REQUEST", fields)) + { + LOG_ERROR( "[RedisStatePublisher] Failed to publish host request to Redis"); + LOG_ERROR( "[RedisStatePublisher] ========================================"); + return ""; + } + + LOG_INFO( "[RedisStatePublisher] Host request published successfully to BMC_HOST_REQUEST"); + LOG_INFO( "[RedisStatePublisher] ========================================"); + + return requestId; +} + +bool RedisStatePublisher::updateSwitchHostState(const std::string& deviceState, + const std::string& deviceStatus) +{ + LOG_INFO( "[RedisStatePublisher] ========================================"); + LOG_INFO( "[RedisStatePublisher] Updating SWITCH_HOST_STATE"); + LOG_INFO( "[RedisStatePublisher] - device_state: %s", deviceState.c_str()); + LOG_INFO( "[RedisStatePublisher] - device_status: %s", deviceStatus.c_str()); + + std::map fields = { + {"device_state", deviceState}, + {"device_status", deviceStatus} + }; + + bool result = hmset("SWITCH_HOST_STATE", fields); + + if (result) + { + LOG_INFO( "[RedisStatePublisher] SWITCH_HOST_STATE updated successfully"); + } + else + { + LOG_ERROR( "[RedisStatePublisher] Failed to update SWITCH_HOST_STATE"); + } + + LOG_INFO( "[RedisStatePublisher] ========================================"); + return result; +} + +bool RedisStatePublisher::updateRequestStatus(const std::string& requestId, + const std::string& status) +{ + LOG_INFO( "[RedisStatePublisher] Updating request status"); + LOG_INFO( "[RedisStatePublisher] - request_id: %s", requestId.c_str()); + LOG_INFO( "[RedisStatePublisher] - status: %s", status.c_str()); + + bool result = hset("BMC_HOST_REQUEST", "status", status); + + if (result) + { + LOG_INFO( "[RedisStatePublisher] Request status updated successfully"); + } + else + { + LOG_ERROR( "[RedisStatePublisher] Failed to update request status"); + } + + return result; +} + +bool RedisStatePublisher::hset(const std::string& key, + const std::string& field, + const std::string& value) +{ + LOG_DEBUG( "[RedisStatePublisher] HSET %s %s %s", key.c_str(), field.c_str(), value.c_str()); + + std::lock_guard lock(redisMutex_); + + if (!stateDbContext_) + { + LOG_ERROR( "[RedisStatePublisher] HSET failed: not connected to Redis"); + return false; + } + + redisReply* reply = (redisReply*)redisCommand(stateDbContext_, + "HSET %s %s %s", key.c_str(), field.c_str(), value.c_str()); + + if (!reply) + { + LOG_ERROR( "[RedisStatePublisher] HSET failed: connection lost (key=%s, field=%s)", + key.c_str(), field.c_str()); + return false; + } + + if (reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStatePublisher] HSET failed: %s (key=%s, field=%s)", + reply->str, key.c_str(), field.c_str()); + freeReplyObject(reply); + return false; + } + + LOG_DEBUG( "[RedisStatePublisher] HSET successful (key=%s, field=%s)", key.c_str(), field.c_str()); + freeReplyObject(reply); + return true; +} + +bool RedisStatePublisher::hmset(const std::string& key, + const std::map& fields) +{ + LOG_DEBUG( "[RedisStatePublisher] HMSET %s with %zu fields", key.c_str(), fields.size()); + + std::lock_guard lock(redisMutex_); + + if (!stateDbContext_) + { + LOG_ERROR( "[RedisStatePublisher] HMSET failed: not connected to Redis"); + return false; + } + + // Build HMSET command arguments + std::vector argv; + std::vector argvlen; + + argv.push_back("HMSET"); + argvlen.push_back(5); + + argv.push_back(key.c_str()); + argvlen.push_back(key.length()); + + for (const auto& [field, value] : fields) + { + argv.push_back(field.c_str()); + argvlen.push_back(field.length()); + argv.push_back(value.c_str()); + argvlen.push_back(value.length()); + + LOG_DEBUG( "[RedisStatePublisher] %s = %s", field.c_str(), value.c_str()); + } + + redisReply* reply = (redisReply*)redisCommandArgv(stateDbContext_, + argv.size(), argv.data(), argvlen.data()); + + if (!reply) + { + LOG_ERROR( "[RedisStatePublisher] HMSET failed: connection lost (key=%s)", key.c_str()); + return false; + } + + if (reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStatePublisher] HMSET failed: %s (key=%s)", reply->str, key.c_str()); + freeReplyObject(reply); + return false; + } + + LOG_DEBUG( "[RedisStatePublisher] HMSET successful (key=%s, %zu fields)", key.c_str(), fields.size()); + freeReplyObject(reply); + return true; +} + +} // namespace sonic::dbus_bridge diff --git a/sonic-dbus-bridge/src/redis_state_subscriber.cpp b/sonic-dbus-bridge/src/redis_state_subscriber.cpp new file mode 100644 index 0000000..13277c3 --- /dev/null +++ b/sonic-dbus-bridge/src/redis_state_subscriber.cpp @@ -0,0 +1,473 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "redis_state_subscriber.hpp" +#include "logger.hpp" +#include + +namespace +{ + +// Helper to connect to Redis using TCP first, then fall back to common Unix socket paths. +// This mirrors the resilient strategy used by RedisAdapter so that the bridge can work +// in containers which only have the Redis Unix socket mounted (e.g., docker-sonic-redfish +// with bridge networking). +redisContext* connectRedisWithFallback(const std::string& host, + int port, + const char* contextName) +{ + struct timeval timeout = {2, 0}; // 2 seconds + redisContext* ctx = nullptr; + bool connected = false; + + // Try TCP first + LOG_DEBUG("[RedisStateSubscriber] Attempting TCP connection (%s) to %s:%d...", + contextName, host.c_str(), port); + ctx = redisConnectWithTimeout(host.c_str(), port, timeout); + + if (!ctx) + { + LOG_ERROR("[RedisStateSubscriber] TCP (%s): Failed to allocate Redis context", + contextName); + } + else if (ctx->err) + { + LOG_DEBUG("[RedisStateSubscriber] TCP (%s): Connection failed: %s (errno: %d)", + contextName, ctx->errstr, ctx->err); + redisFree(ctx); + ctx = nullptr; + } + else + { + LOG_INFO("[RedisStateSubscriber] Connected to Redis via TCP (%s): %s:%d", + contextName, host.c_str(), port); + connected = true; + } + + // If TCP failed, try common Unix socket locations + if (!connected) + { + const char* unixSockets[] = { + "/var/run/redis/redis.sock", + "/run/redis/redis.sock", + "/var/run/redis.sock", + nullptr + }; + + for (int i = 0; unixSockets[i] != nullptr && !connected; ++i) + { + LOG_DEBUG("[RedisStateSubscriber] Attempting Unix socket (%s) connection to %s...", + contextName, unixSockets[i]); + + ctx = redisConnectUnixWithTimeout(unixSockets[i], timeout); + + if (!ctx) + { + LOG_ERROR("[RedisStateSubscriber] Unix socket (%s): Failed to allocate Redis context", + contextName); + } + else if (ctx->err) + { + LOG_DEBUG("[RedisStateSubscriber] Unix socket (%s): Connection failed: %s (errno: %d)", + contextName, ctx->errstr, ctx->err); + redisFree(ctx); + ctx = nullptr; + } + else + { + LOG_INFO("[RedisStateSubscriber] Connected to Redis via Unix socket (%s): %s", + contextName, unixSockets[i]); + connected = true; + } + } + } + + if (!connected || !ctx) + { + LOG_ERROR("[RedisStateSubscriber] All Redis connection attempts (%s) failed", + contextName); + return nullptr; + } + + return ctx; +} + +} // anonymous namespace + +namespace sonic::dbus_bridge +{ + +RedisStateSubscriber::RedisStateSubscriber() + : subContext_(nullptr), getContext_(nullptr), running_(false) +{ + LOG_INFO( "[RedisStateSubscriber] Constructor called"); +} + +RedisStateSubscriber::~RedisStateSubscriber() +{ + LOG_INFO( "[RedisStateSubscriber] Destructor called"); + stop(); +} + +bool RedisStateSubscriber::start(const std::string& host, int port, + KeyspaceCallback callback) +{ + LOG_INFO( "[RedisStateSubscriber] ========================================"); + LOG_INFO( "[RedisStateSubscriber] Starting subscriber"); + LOG_INFO( "[RedisStateSubscriber] Redis: %s:%d", host.c_str(), port); + + if (running_) + { + LOG_WARNING( "[RedisStateSubscriber] Subscriber already running"); + return false; + } + + callback_ = callback; + + // Create subscription context (TCP + Unix socket fallback) + subContext_ = connectRedisWithFallback(host, port, "subscribe"); + if (!subContext_) + { + LOG_ERROR( "[RedisStateSubscriber] Subscription connection failed"); + return false; + } + + LOG_INFO( "[RedisStateSubscriber] Subscription connection established"); + + // Create GET context (for HGETALL) with the same fallback logic + getContext_ = connectRedisWithFallback(host, port, "get"); + if (!getContext_) + { + LOG_ERROR( "[RedisStateSubscriber] GET connection failed"); + redisFree(subContext_); + subContext_ = nullptr; + return false; + } + + LOG_INFO( "[RedisStateSubscriber] GET connection established"); + + // Select STATE_DB (DB 6) for both contexts + redisReply* reply = (redisReply*)redisCommand(subContext_, "SELECT 6"); + if (!reply || reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to select STATE_DB on subscription context"); + if (reply) freeReplyObject(reply); + redisFree(subContext_); + redisFree(getContext_); + subContext_ = nullptr; + getContext_ = nullptr; + return false; + } + freeReplyObject(reply); + + reply = (redisReply*)redisCommand(getContext_, "SELECT 6"); + if (!reply || reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to select STATE_DB on GET context"); + if (reply) freeReplyObject(reply); + redisFree(subContext_); + redisFree(getContext_); + subContext_ = nullptr; + getContext_ = nullptr; + return false; + } + freeReplyObject(reply); + + LOG_INFO( "[RedisStateSubscriber] STATE_DB (DB 6) selected on both contexts"); + + // Subscribe to keyspace notifications for SWITCH_HOST_STATE + LOG_INFO( "[RedisStateSubscriber] Subscribing to __keyspace@6__:SWITCH_HOST_STATE"); + reply = (redisReply*)redisCommand(subContext_, + "SUBSCRIBE __keyspace@6__:SWITCH_HOST_STATE"); + + if (!reply || reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to subscribe to keyspace notifications"); + if (reply) freeReplyObject(reply); + redisFree(subContext_); + redisFree(getContext_); + subContext_ = nullptr; + getContext_ = nullptr; + return false; + } + freeReplyObject(reply); + + LOG_INFO( "[RedisStateSubscriber] Subscribed successfully"); + + // Start subscriber thread + running_ = true; + subscriberThread_ = std::thread(&RedisStateSubscriber::subscriberLoop, this); + + LOG_INFO( "[RedisStateSubscriber] Subscriber thread started"); + LOG_INFO( "[RedisStateSubscriber] ========================================"); + + return true; +} + +bool RedisStateSubscriber::startMultiKey(const std::string& host, int port, + const std::vector& keys, + KeyspaceCallback callback) +{ + LOG_INFO( "[RedisStateSubscriber] ========================================"); + LOG_INFO( "[RedisStateSubscriber] Starting multi-key subscriber"); + LOG_INFO( "[RedisStateSubscriber] Host: %s, Port: %d", host.c_str(), port); + LOG_INFO( "[RedisStateSubscriber] Subscribing to %zu keys", keys.size()); + + if (running_) + { + LOG_WARNING( "[RedisStateSubscriber] Already running"); + return false; + } + + if (keys.empty()) + { + LOG_ERROR( "[RedisStateSubscriber] No keys provided"); + return false; + } + + callback_ = callback; + + // Create two Redis contexts: one for subscribing, one for getting data. + // Use TCP + Unix socket fallback so we can connect in bridge-networked containers + // that only expose Redis via Unix sockets. + subContext_ = connectRedisWithFallback(host, port, "subscribe"); + if (!subContext_) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to connect to Redis (subscribe)"); + return false; + } + + getContext_ = connectRedisWithFallback(host, port, "get"); + if (!getContext_) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to connect to Redis (get)"); + redisFree(subContext_); + subContext_ = nullptr; + return false; + } + + LOG_INFO( "[RedisStateSubscriber] Connected to Redis"); + + // Select STATE_DB (DB 6) on both contexts + redisReply* reply = (redisReply*)redisCommand(subContext_, "SELECT 6"); + if (!reply || reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to select STATE_DB on subscribe context"); + if (reply) freeReplyObject(reply); + redisFree(subContext_); + redisFree(getContext_); + subContext_ = nullptr; + getContext_ = nullptr; + return false; + } + freeReplyObject(reply); + + reply = (redisReply*)redisCommand(getContext_, "SELECT 6"); + if (!reply || reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to select STATE_DB on get context"); + if (reply) freeReplyObject(reply); + redisFree(subContext_); + redisFree(getContext_); + subContext_ = nullptr; + getContext_ = nullptr; + return false; + } + freeReplyObject(reply); + + LOG_INFO( "[RedisStateSubscriber] STATE_DB (DB 6) selected on both contexts"); + + // Subscribe to keyspace notifications for all keys + for (const auto& key : keys) + { + std::string channel = "__keyspace@6__:" + key; + LOG_INFO( "[RedisStateSubscriber] Subscribing to %s", channel.c_str()); + + reply = (redisReply*)redisCommand(subContext_, "SUBSCRIBE %s", channel.c_str()); + + if (!reply || reply->type == REDIS_REPLY_ERROR) + { + LOG_ERROR( "[RedisStateSubscriber] Failed to subscribe to %s", channel.c_str()); + if (reply) freeReplyObject(reply); + redisFree(subContext_); + redisFree(getContext_); + subContext_ = nullptr; + getContext_ = nullptr; + return false; + } + freeReplyObject(reply); + } + + LOG_INFO( "[RedisStateSubscriber] Subscribed to all %zu keys successfully", keys.size()); + + // Start subscriber thread + running_ = true; + subscriberThread_ = std::thread(&RedisStateSubscriber::subscriberLoop, this); + + LOG_INFO( "[RedisStateSubscriber] Subscriber thread started"); + LOG_INFO( "[RedisStateSubscriber] ========================================"); + + return true; +} + +void RedisStateSubscriber::stop() +{ + if (!running_) + { + return; + } + + LOG_INFO( "[RedisStateSubscriber] Stopping subscriber"); + + running_ = false; + + if (subscriberThread_.joinable()) + { + subscriberThread_.join(); + LOG_INFO( "[RedisStateSubscriber] Subscriber thread joined"); + } + + if (subContext_) + { + redisFree(subContext_); + subContext_ = nullptr; + } + + if (getContext_) + { + redisFree(getContext_); + getContext_ = nullptr; + } + + LOG_INFO( "[RedisStateSubscriber] Subscriber stopped"); +} + +void RedisStateSubscriber::subscriberLoop() +{ + LOG_INFO( "[RedisStateSubscriber] Subscriber loop started"); + + while (running_) + { + redisReply* reply; + if (redisGetReply(subContext_, (void**)&reply) != REDIS_OK) + { + LOG_ERROR( "[RedisStateSubscriber] Redis subscriber error: %s", subContext_->errstr); + break; + } + + if (!reply) + { + LOG_WARNING( "[RedisStateSubscriber] Received null reply"); + continue; + } + + // Expected format: ["message", channel, message] + if (reply->type == REDIS_REPLY_ARRAY && reply->elements == 3) + { + std::string messageType = reply->element[0]->str; + std::string channel = reply->element[1]->str; + std::string message = reply->element[2]->str; + + LOG_DEBUG( "[RedisStateSubscriber] Received: type=%s, channel=%s, message=%s", + messageType.c_str(), channel.c_str(), message.c_str()); + + if (messageType == "message" && message == "hset") + { + LOG_INFO( "[RedisStateSubscriber] HSET detected on %s", channel.c_str()); + handleKeyspaceNotification(channel); + } + } + + freeReplyObject(reply); + } + + LOG_INFO( "[RedisStateSubscriber] Subscriber loop ended"); +} + +void RedisStateSubscriber::handleKeyspaceNotification(const std::string& channel) +{ + LOG_INFO( "[RedisStateSubscriber] ========================================"); + LOG_INFO( "[RedisStateSubscriber] Handling keyspace notification"); + LOG_INFO( "[RedisStateSubscriber] Channel: %s", channel.c_str()); + + // Channel format: __keyspace@6__:SWITCH_HOST_STATE + // Extract key name + size_t pos = channel.find_last_of(':'); + if (pos == std::string::npos) + { + LOG_WARNING( "[RedisStateSubscriber] Invalid channel format: %s", channel.c_str()); + return; + } + + std::string key = channel.substr(pos + 1); + LOG_INFO( "[RedisStateSubscriber] Key: %s", key.c_str()); + + // Get all fields from the hash + std::map fields = hgetall(key); + + if (fields.empty()) + { + LOG_WARNING( "[RedisStateSubscriber] No fields found for key: %s", key.c_str()); + LOG_INFO( "[RedisStateSubscriber] ========================================"); + return; + } + + LOG_INFO( "[RedisStateSubscriber] Retrieved %zu fields from %s", fields.size(), key.c_str()); + + // Invoke callback for each field + for (const auto& [field, value] : fields) + { + LOG_INFO( "[RedisStateSubscriber] %s = %s", field.c_str(), value.c_str()); + + if (callback_) + { + callback_(key, field, value); + } + } + + LOG_INFO( "[RedisStateSubscriber] ========================================"); +} + +std::map RedisStateSubscriber::hgetall(const std::string& key) +{ + std::map result; + + LOG_DEBUG( "[RedisStateSubscriber] HGETALL %s", key.c_str()); + + redisReply* reply = (redisReply*)redisCommand(getContext_, "HGETALL %s", key.c_str()); + + if (!reply) + { + LOG_ERROR( "[RedisStateSubscriber] HGETALL failed: connection lost"); + return result; + } + + if (reply->type != REDIS_REPLY_ARRAY) + { + LOG_ERROR( "[RedisStateSubscriber] HGETALL failed: unexpected reply type %d", reply->type); + freeReplyObject(reply); + return result; + } + + // Parse field-value pairs + for (size_t i = 0; i + 1 < reply->elements; i += 2) + { + std::string field = reply->element[i]->str; + std::string value = reply->element[i + 1]->str; + result[field] = value; + + LOG_DEBUG( "[RedisStateSubscriber] HGETALL result: %s = %s", field.c_str(), value.c_str()); + } + + freeReplyObject(reply); + return result; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/state_manager.cpp b/sonic-dbus-bridge/src/state_manager.cpp new file mode 100644 index 0000000..fccb8c2 --- /dev/null +++ b/sonic-dbus-bridge/src/state_manager.cpp @@ -0,0 +1,298 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "state_manager.hpp" +#include "logger.hpp" +#include "redis_state_publisher.hpp" +#include + +namespace sonic::dbus_bridge +{ + +namespace +{ + +// D-Bus interface names +constexpr const char* IFACE_STATE_HOST = "xyz.openbmc_project.State.Host"; + +// D-Bus object paths +constexpr const char* OBJ_PATH_HOST = "/xyz/openbmc_project/state/host0"; + +// Host transition values +constexpr const char* HOST_TRANS_ON = "xyz.openbmc_project.State.Host.Transition.On"; +constexpr const char* HOST_TRANS_OFF = "xyz.openbmc_project.State.Host.Transition.Off"; +constexpr const char* HOST_TRANS_REBOOT = "xyz.openbmc_project.State.Host.Transition.Reboot"; +constexpr const char* HOST_TRANS_FORCE_WARM_REBOOT = "xyz.openbmc_project.State.Host.Transition.ForceWarmReboot"; +constexpr const char* HOST_TRANS_POWER_CYCLE = "xyz.openbmc_project.State.Host.Transition.PowerCycle"; + +// Host state values +constexpr const char* HOST_STATE_OFF = "xyz.openbmc_project.State.Host.HostState.Off"; +constexpr const char* HOST_STATE_TRANSITIONING = "xyz.openbmc_project.State.Host.HostState.TransitioningToRunning"; +constexpr const char* HOST_STATE_RUNNING = "xyz.openbmc_project.State.Host.HostState.Running"; + +// Async execution delay (milliseconds) +constexpr int EXEC_DELAY_MS = 100; + +} // namespace + +StateManager::StateManager(sdbusplus::asio::object_server& server, + boost::asio::io_context& io) + : server_(server), io_(io), + currentHostState_(HOST_STATE_RUNNING), + redisPublisher_(std::make_unique()), + actionTimer_(std::make_unique(io)) +{ + // Connect to Redis STATE_DB + LOG_INFO("StateManager: Connecting to Redis STATE_DB..."); + if (!redisPublisher_->connect()) + { + LOG_ERROR("StateManager: Failed to connect to Redis STATE_DB"); + } + else + { + LOG_INFO("StateManager: Connected to Redis STATE_DB successfully"); + } +} + +bool StateManager::createStateObjects() +{ + LOG_INFO( "Creating state objects..."); + + try + { + // Create xyz.openbmc_project.State.Host interface + hostStateIface_ = server_.add_interface(OBJ_PATH_HOST, IFACE_STATE_HOST); + + // Register RequestedHostTransition property (read-write) + hostStateIface_->register_property_rw( + "RequestedHostTransition", + sdbusplus::vtable::property_::emits_change, + [this](const std::string& newValue, const auto&) { + // Property setter callback + LOG_INFO( "=== Property Change Detected ==="); + LOG_INFO( "RequestedHostTransition = %s", newValue.c_str()); + + // Validate transition value + if (!isValidTransition(newValue)) + { + LOG_ERROR( "Invalid transition value: %s", newValue.c_str()); + throw std::invalid_argument("Invalid transition value"); + } + + // Check queue overflow + if (actionQueue_.size() >= MAX_QUEUE_SIZE) + { + LOG_ERROR( "Action queue full (size: %zu), rejecting request", + actionQueue_.size()); + throw std::runtime_error("Action queue full"); + } + + // Store last requested transition + lastRequestedTransition_ = newValue; + + // Queue action for async execution + ActionRequest request; + request.transition = newValue; + request.timestamp = std::chrono::steady_clock::now(); + actionQueue_.push(request); + + LOG_INFO( "Action queued (queue size: %zu)", actionQueue_.size()); + + // Trigger async processing + processNextAction(); + + return 1; // Success + }, + [this](const auto&) { + // Property getter callback + return lastRequestedTransition_; + }); + + // Register CurrentHostState property (read-only) + hostStateIface_->register_property_r( + "CurrentHostState", + sdbusplus::vtable::property_::emits_change, + [this](const auto&) { + return currentHostState_; + }); + + // Initialize the interface + hostStateIface_->initialize(); + + LOG_INFO( "Created state object at %s", OBJ_PATH_HOST); + LOG_INFO( "Initial state: %s", currentHostState_.c_str()); + return true; + } + catch (const std::exception& e) + { + LOG_ERROR( "Failed to create state objects: %s", e.what()); + return false; + } +} + +void StateManager::processNextAction() +{ + // Check if action already in progress + if (actionInProgress_) + { + LOG_DEBUG( "Action already in progress, waiting..."); + return; + } + + // Check if queue is empty + if (actionQueue_.empty()) + { + return; + } + + // Mark action as in progress + actionInProgress_ = true; + + // Get next action from queue + ActionRequest action = actionQueue_.front(); + actionQueue_.pop(); + + LOG_INFO( "Processing action: %s (remaining in queue: %zu)", + action.transition.c_str(), actionQueue_.size()); + + // Update state to transitioning + updateHostState(HOST_STATE_TRANSITIONING); + + // Schedule async execution using timer (non-blocking) + actionTimer_->expires_after(std::chrono::milliseconds(EXEC_DELAY_MS)); + actionTimer_->async_wait([this, transition = action.transition]( + const boost::system::error_code& ec) { + if (ec == boost::asio::error::operation_aborted) + { + LOG_WARNING( "Action timer cancelled"); + actionInProgress_ = false; + return; + } + + if (ec) + { + LOG_ERROR( "Action timer error: %s", ec.message().c_str()); + actionInProgress_ = false; + updateHostState(HOST_STATE_RUNNING); + processNextAction(); // Try next action + return; + } + + // Execute the transition + executeHostTransition(transition); + + // Mark action as complete + actionInProgress_ = false; + + // Process next action in queue + processNextAction(); + }); +} + +void StateManager::executeHostTransition(const std::string& transition) +{ + LOG_INFO("=== Executing Host Transition ==="); + LOG_INFO("Transition: %s", transition.c_str()); + + // Check if Redis publisher is connected + if (!redisPublisher_ || !redisPublisher_->isConnected()) + { + LOG_ERROR("Redis publisher not connected, cannot publish transition"); + updateHostState(HOST_STATE_RUNNING); + return; + } + + // Map D-Bus transition to simple transition name + std::string transitionName = transitionToScriptCommand(transition); + if (transitionName.empty()) + { + LOG_ERROR("Failed to map transition to transition name"); + updateHostState(HOST_STATE_RUNNING); + return; + } + + // Publish to Redis STATE_DB + LOG_INFO("Publishing transition '%s' to Redis STATE_DB...", transitionName.c_str()); + std::string requestId = redisPublisher_->publishHostRequest(transitionName); + + if (requestId.empty()) + { + LOG_ERROR("Failed to publish host request to Redis"); + updateHostState(HOST_STATE_RUNNING); + return; + } + + LOG_INFO("Host request published successfully to BMC_HOST_REQUEST"); + LOG_INFO("Request ID: %s", requestId.c_str()); + + // Update state based on transition + if (transition == HOST_TRANS_OFF) + { + updateHostState(HOST_STATE_OFF); + } + else + { + updateHostState(HOST_STATE_RUNNING); + } +} + + +void StateManager::updateHostState(const std::string& newState) +{ + if (currentHostState_ == newState) + { + return; // No change + } + + LOG_INFO( "=== State Change ==="); + LOG_INFO( "Old state: %s", currentHostState_.c_str()); + LOG_INFO( "New state: %s", newState.c_str()); + + currentHostState_ = newState; + + // Emit PropertiesChanged signal + if (hostStateIface_) + { + hostStateIface_->signal_property("CurrentHostState"); + } +} + +std::string StateManager::transitionToScriptCommand(const std::string& transition) +{ + if (transition == HOST_TRANS_ON) + { + return "reset-out"; + } + else if (transition == HOST_TRANS_OFF) + { + return "reset-in"; + } + else if (transition == HOST_TRANS_REBOOT || transition == HOST_TRANS_POWER_CYCLE || + transition == HOST_TRANS_FORCE_WARM_REBOOT) + { + return "reset-cycle"; + } + else + { + LOG_ERROR( "Unknown transition: %s", transition.c_str()); + return ""; + } +} + +bool StateManager::isValidTransition(const std::string& transition) +{ + return transition == HOST_TRANS_ON || + transition == HOST_TRANS_OFF || + transition == HOST_TRANS_REBOOT || + transition == HOST_TRANS_FORCE_WARM_REBOOT || + transition == HOST_TRANS_POWER_CYCLE; +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/update_engine.cpp b/sonic-dbus-bridge/src/update_engine.cpp new file mode 100644 index 0000000..98915b3 --- /dev/null +++ b/sonic-dbus-bridge/src/update_engine.cpp @@ -0,0 +1,232 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "update_engine.hpp" +#include "inventory_model.hpp" +#include "logger.hpp" + +namespace sonic::dbus_bridge +{ + +UpdateEngine::UpdateEngine(boost::asio::io_context& io, + std::shared_ptr redisAdapter, + std::shared_ptr dbusExporter, + int pollIntervalSec) + : io_(io), redisAdapter_(redisAdapter), dbusExporter_(dbusExporter), + pollIntervalSec_(pollIntervalSec), timer_(io) +{ +} + +void UpdateEngine::start() +{ + if (running_) + { + return; + } + + if (pollIntervalSec_ > 0) + { + LOG_INFO("Starting update engine (poll interval: %ds)", pollIntervalSec_); + running_ = true; + scheduleNextPoll(); + } + else + { + LOG_INFO("Update engine started (event-driven mode, polling disabled)"); + running_ = true; + } +} + +void UpdateEngine::stop() +{ + if (!running_) + { + return; + } + + LOG_INFO("Stopping update engine"); + running_ = false; + timer_.cancel(); +} + +void UpdateEngine::onPollTimer(const boost::system::error_code& ec) +{ + if (ec == boost::asio::error::operation_aborted) + { + return; // Timer was cancelled + } + + if (ec) + { + LOG_ERROR("Poll timer error: %s", ec.message().c_str()); + scheduleNextPoll(); + return; + } + + doUpdate(); + scheduleNextPoll(); +} + +void UpdateEngine::doUpdate() +{ + try + { + // Read current data from Redis + auto metadata = redisAdapter_->getDeviceMetadata(); + auto state = redisAdapter_->getChassisState(); + + // Check if anything changed + bool changed = false; + + if (!cachedMetadata_ || + cachedMetadata_->serialNumber != metadata.serialNumber || + cachedMetadata_->platform != metadata.platform || + cachedMetadata_->hostname != metadata.hostname) + { + changed = true; + cachedMetadata_ = metadata; + } + + if (!cachedState_ || + cachedState_->powerState != state.powerState) + { + changed = true; + cachedState_ = state; + } + + if (changed) + { + LOG_INFO("Detected changes, updating D-Bus objects..."); + + // Build new model (without FRU/platform.json - those don't change at runtime) + InventoryModel newModel = InventoryModelBuilder::build( + std::nullopt, // FRU doesn't change + metadata, + std::nullopt, // platform.json doesn't change + state + ); + + // Update D-Bus objects + dbusExporter_->updateObjects(newModel); + + // Notify callback + if (updateCallback_) + { + updateCallback_(); + } + } + } + catch (const std::exception& e) + { + LOG_ERROR("Update error: %s", e.what()); + } +} + +void UpdateEngine::scheduleNextPoll() +{ + if (!running_ || pollIntervalSec_ <= 0) + { + return; + } + + timer_.expires_after(std::chrono::seconds(pollIntervalSec_)); + timer_.async_wait([this](const boost::system::error_code& ec) { + onPollTimer(ec); + }); +} + +void UpdateEngine::onRedisFieldChange(const std::string& key, + const std::string& field, + const std::string& value) +{ + LOG_INFO("[UpdateEngine] Redis field changed: %s.%s = %s", + key.c_str(), field.c_str(), value.c_str()); + + try + { + bool needsUpdate = false; + + // Handle DEVICE_METADATA changes + if (key == "DEVICE_METADATA") + { + // Re-read entire DEVICE_METADATA to get all fields + auto metadata = redisAdapter_->getDeviceMetadata(); + + // Check if this field actually changed from cached value + if (!cachedMetadata_ || + cachedMetadata_->serialNumber != metadata.serialNumber || + cachedMetadata_->platform != metadata.platform || + cachedMetadata_->hostname != metadata.hostname) + { + LOG_INFO("[UpdateEngine] DEVICE_METADATA changed, updating D-Bus"); + cachedMetadata_ = metadata; + needsUpdate = true; + } + } + // Handle CHASSIS_STATE changes + else if (key == "CHASSIS_STATE") + { + // Re-read entire CHASSIS_STATE to get all fields + auto state = redisAdapter_->getChassisState(); + + // Check if power state changed + if (!cachedState_ || cachedState_->powerState != state.powerState) + { + LOG_INFO("[UpdateEngine] CHASSIS_STATE changed, updating D-Bus"); + cachedState_ = state; + needsUpdate = true; + } + } + // Handle SWITCH_HOST_STATE changes (currently not mapped to D-Bus) + else if (key == "SWITCH_HOST_STATE") + { + LOG_DEBUG("[UpdateEngine] SWITCH_HOST_STATE changed (not currently mapped to D-Bus)"); + // Future: Map to host state D-Bus properties if needed + } + else + { + LOG_WARNING("[UpdateEngine] Unknown Redis key: %s", key.c_str()); + return; + } + + // Update D-Bus objects if needed + if (needsUpdate) + { + // Build new model with updated data + InventoryModel newModel = InventoryModelBuilder::build( + std::nullopt, // FRU doesn't change at runtime + cachedMetadata_, // Updated metadata + std::nullopt, // platform.json doesn't change at runtime + cachedState_ // Updated chassis state + ); + + // Update D-Bus objects + dbusExporter_->updateObjects(newModel); + + LOG_INFO("[UpdateEngine] D-Bus objects updated successfully"); + + // Notify callback + if (updateCallback_) + { + updateCallback_(); + } + } + else + { + LOG_DEBUG("[UpdateEngine] No actual change detected, skipping D-Bus update"); + } + } + catch (const std::exception& e) + { + LOG_ERROR("[UpdateEngine] Error handling Redis field change: %s", e.what()); + } +} + +} // namespace sonic::dbus_bridge + diff --git a/sonic-dbus-bridge/src/user_mgr.cpp b/sonic-dbus-bridge/src/user_mgr.cpp new file mode 100644 index 0000000..dc8be52 --- /dev/null +++ b/sonic-dbus-bridge/src/user_mgr.cpp @@ -0,0 +1,202 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "user_mgr.hpp" +#include "users.hpp" +#include "logger.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace sonic +{ +namespace user +{ + +namespace +{ +// Only the admin user is exposed via D-Bus for authentication/authorization +constexpr const char* adminUserName = "admin"; + +long currentDate() +{ + const auto date = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (date > std::numeric_limits::max()) + { + return std::numeric_limits::max(); + } + + if (date < std::numeric_limits::min()) + { + return std::numeric_limits::min(); + } + + return date; +} + +} + + + +bool UserMgr::isUserExist(const std::string& userName) +{ + if (userName.empty()) + { + LOG_ERROR("User name is empty"); + throw std::invalid_argument("User name is empty"); + } + if (usersList.find(userName) == usersList.end()) + { + return false; + } + return true; +} + + + +UserMgr::UserMgr(sdbusplus::asio::object_server& server, const char* path, + sonic::dbus_bridge::ObjectMapperService* objectMapper) : + server(server), + path(path), + objectMapper_(objectMapper) +{ + // Register ObjectManager interface at /xyz/openbmc_project/user + // This is required for BMCWeb's getManagedObjects() to work + server.add_manager(path); + + // Register xyz.openbmc_project.User.Manager interface + userMgrIface = server.add_interface(path, "xyz.openbmc_project.User.Manager"); + + // Register AllPrivileges property (read-only) + userMgrIface->register_property("AllPrivileges", privMgr); + + // Register AllGroups property (read-only) + userMgrIface->register_property("AllGroups", allGroups); + + // Register GetUserInfo method + userMgrIface->register_method( + "GetUserInfo", + [this](const std::string& userName) { + return getUserInfo(userName); + }); + + // Register DeleteUser method (delete via object path, not this interface) + // BMCWeb uses the Delete method on individual user objects + + userMgrIface->initialize(); + + initUserObjects(); +} + + + +bool UserMgr::isUserEnabled(const std::string& userName) +{ + std::array buffer{}; + struct spwd spwd; + struct spwd* resultPtr = nullptr; + int status = getspnam_r(userName.c_str(), &spwd, buffer.data(), + buffer.max_size(), &resultPtr); + if (!status && (&spwd == resultPtr)) + { + // according to chage/usermod code -1 means that account does not expire + // https://github.com/shadow-maint/shadow/blob/7a796897e52293efe9e210ab8da32b7aefe65591/src/chage.c + if (resultPtr->sp_expire < 0) + { + return true; + } + + // check account expiration date against current date + if (resultPtr->sp_expire > currentDate()) + { + return true; + } + + return false; + } + return false; // assume user is disabled for any error. +} + + + +void UserMgr::initUserObjects(void) +{ + // Only create D-Bus object for the admin user + // Authentication is done via PAM for all users, but only admin + // is exposed for Redfish AccountService and authorization + + std::string userName = adminUserName; + + // Check if admin user exists in the system + std::array buffer{}; + struct passwd pwd; + struct passwd* resultPtr = nullptr; + + int status = getpwnam_r(userName.c_str(), &pwd, buffer.data(), + buffer.max_size(), &resultPtr); + + if (status != 0 || resultPtr == nullptr) + { + LOG_ERROR("Admin user '%s' not found in system", userName.c_str()); + // Don't throw - service can still start, but GetUserInfo will fail + return; + } + + // Admin user always has priv-admin privilege + std::string userPriv = "priv-admin"; + + // Admin user is in redfish group (for additional permissions if needed) + std::vector userGroups = {"redfish"}; + + // Create D-Bus object path for admin user + sdbusplus::message::object_path tempObjPath(usersObjPath); + tempObjPath /= userName; + std::string objPath(tempObjPath); + + // Create the Users object for admin + usersList.emplace(userName, std::make_unique( + server, objPath, userGroups, + userPriv, isUserEnabled(userName), *this)); + + LOG_INFO("Created D-Bus object for admin user at %s", objPath.c_str()); +} + +UserInfoMap UserMgr::getUserInfo(const std::string& userName) +{ + UserInfoMap userInfo; + + // Check if user exists in our list (local user) + if (!isUserExist(userName)) + { + LOG_ERROR("GetUserInfo: User %s not found", userName.c_str()); + throw std::runtime_error("User not found"); + } + + const auto& user = usersList[userName]; + + userInfo.emplace("UserPrivilege", user->getUserPrivilege()); + userInfo.emplace("UserGroups", user->getUserGroups()); + userInfo.emplace("UserEnabled", user->getUserEnabled()); + userInfo.emplace("UserLockedForFailedAttempt", user->getUserLockedForFailedAttempt()); + userInfo.emplace("UserPasswordExpired", user->getUserPasswordExpired()); + userInfo.emplace("RemoteUser", false); // Always false for local users + + return userInfo; +} + +} // namespace user +} // namespace sonic diff --git a/sonic-dbus-bridge/src/users.cpp b/sonic-dbus-bridge/src/users.cpp new file mode 100644 index 0000000..5839f3d --- /dev/null +++ b/sonic-dbus-bridge/src/users.cpp @@ -0,0 +1,68 @@ +/////////////////////////////////////// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Nexthop AI +// Copyright (C) 2024 SONiC Project +// Author: Nexthop AI +// Author: SONiC Project +// License file: sonic-redfish/LICENSE +/////////////////////////////////////// + +#include "users.hpp" +#include "user_mgr.hpp" +#include "logger.hpp" + + +namespace sonic +{ +namespace user +{ + +/** @brief Constructs Users object. + * + * @param[in] server - sdbusplus asio object server + * @param[in] path - D-Bus path + * @param[in] groups - users group list + * @param[in] priv - user privilege + * @param[in] enabled - user enabled state + * @param[in] parent - user manager - parent object + */ +Users::Users(sdbusplus::asio::object_server& server, const std::string& path, + std::vector groups, const std::string& priv, + bool enabled, UserMgr& parent) : + userName(sdbusplus::message::object_path(path).filename()), + manager(parent), + server(server), + userPrivilege(priv), + userGroups(std::move(groups)), + userEnabled(enabled) +{ + // Create D-Bus interface for User.Attributes + userIface = server.add_interface(path, "xyz.openbmc_project.User.Attributes"); + + // Register UserPrivilege property (read-only) + userIface->register_property("UserPrivilege", userPrivilege); + + // Register UserGroups property (read-only) + userIface->register_property("UserGroups", userGroups); + + // Register UserEnabled property (read-only) + userIface->register_property("UserEnabled", userEnabled); + + userIface->register_property("UserLockedForFailedAttempt", false); + userIface->register_property("UserPasswordExpired", false); + + userIface->initialize(); + + LOG_INFO("User object created for: %s", userName.c_str()); +} + +Users::~Users() +{ + LOG_INFO("Removing D-Bus interfaces for user: %s", userName.c_str()); + + // Remove interface from D-Bus + server.remove_interface(userIface); +} + +} // namespace user +} // namespace sonic diff --git a/sonic-dbus-bridge/subprojects/sdbusplus.wrap b/sonic-dbus-bridge/subprojects/sdbusplus.wrap new file mode 100644 index 0000000..87fb818 --- /dev/null +++ b/sonic-dbus-bridge/subprojects/sdbusplus.wrap @@ -0,0 +1,8 @@ +[wrap-git] +url = https://github.com/openbmc/sdbusplus.git +revision = HEAD +depth = 1 + +[provide] +sdbusplus = sdbusplus_dep + diff --git a/sonic-dbus-bridge/subprojects/stdexec.wrap b/sonic-dbus-bridge/subprojects/stdexec.wrap new file mode 100644 index 0000000..ad1981b --- /dev/null +++ b/sonic-dbus-bridge/subprojects/stdexec.wrap @@ -0,0 +1,8 @@ +[wrap-git] +url = https://github.com/NVIDIA/stdexec.git +revision = main +depth = 1 + +[provide] +stdexec = stdexec_dep +