fix: FQDN resolution and display_name blank bug; fix: Arch/Alpine/RPM packages
Bug fixes: - get_fqdn() now prioritizes 'hostname -f' (returns full FQDN) over /etc/hostname (returns short hostname) - Added get_hostname() for short hostname extraction - Added hostname field to EnrollmentRequest for manager display_name population - Updated SPEC.md and API_DOCUMENTATION.md Package fixes: - Arch: Added linux-patch-api.install with post_install/upgrade/remove hooks, user creation, directory creation, config handling - Alpine: Added linux-patch-api.apk-install with pre/post install/deinstall hooks, user creation, directory creation, config handling, missing config.yaml.example - RPM: Dynamic version from Cargo.toml, %ghost %config(noreplace) for live configs, tarball exclusions, /var/log in %files
This commit is contained in:
@ -909,6 +909,7 @@ Enrollment endpoints enable new hosts to register with the Patch Manager and rec
|
|||||||
| `fqdn` | string | Yes | Fully qualified domain name of the host |
|
| `fqdn` | string | Yes | Fully qualified domain name of the host |
|
||||||
| `ip_address` | string | Yes | Primary non-loopback IPv4 address |
|
| `ip_address` | string | Yes | Primary non-loopback IPv4 address |
|
||||||
| `os_details` | object | Yes | OS metadata (free-form JSON object) |
|
| `os_details` | object | Yes | OS metadata (free-form JSON object) |
|
||||||
|
| `hostname` | string | No | Short hostname (without domain). Used by the manager to populate `display_name` on approval. If omitted, the manager falls back to the FQDN. |
|
||||||
|
|
||||||
**`os_details` common fields:**
|
**`os_details` common fields:**
|
||||||
|
|
||||||
@ -933,7 +934,8 @@ curl -X POST https://manager.example.com/api/v1/enroll \
|
|||||||
"version_id": "12",
|
"version_id": "12",
|
||||||
"kernel": "6.1.0-kali9-amd64",
|
"kernel": "6.1.0-kali9-amd64",
|
||||||
"id_like": "debian"
|
"id_like": "debian"
|
||||||
}
|
},
|
||||||
|
"hostname": "host-01"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1916,7 +1916,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
|
|||||||
2
SPEC.md
2
SPEC.md
@ -169,7 +169,7 @@ The enrollment flow runs before mTLS server startup. On success, the daemon proc
|
|||||||
### Phase 1: Registration Request
|
### Phase 1: Registration Request
|
||||||
- **Identity Extraction:**
|
- **Identity Extraction:**
|
||||||
- `/etc/machine-id` (fallback: `/var/lib/dbus/machine-id`)
|
- `/etc/machine-id` (fallback: `/var/lib/dbus/machine-id`)
|
||||||
- FQDN from `/etc/hostname` → `hostname -f` → `hostname` → `localhost`
|
- FQDN from `hostname -f` (validated contains `.`) → `hostname` + `hostname -d` → `/etc/hostname` → `hostname` → `localhost`
|
||||||
- Non-loopback IPv4 addresses via network interface enumeration
|
- Non-loopback IPv4 addresses via network interface enumeration
|
||||||
- OS details from `/etc/os-release` (distro, version, id_like, codename) + kernel version (`uname -r`)
|
- OS details from `/etc/os-release` (distro, version, id_like, codename) + kernel version (`uname -r`)
|
||||||
- **Submission:** Unauthenticated `POST /api/v1/enroll` to manager with identity payload
|
- **Submission:** Unauthenticated `POST /api/v1/enroll` to manager with identity payload
|
||||||
|
|||||||
@ -46,25 +46,41 @@ fi
|
|||||||
|
|
||||||
# Create package directory in /home/builduser (accessible by builduser)
|
# Create package directory in /home/builduser (accessible by builduser)
|
||||||
PKGDIR=/home/builduser/apk-package
|
PKGDIR=/home/builduser/apk-package
|
||||||
|
rm -rf "$PKGDIR"
|
||||||
mkdir -p "$PKGDIR"/usr/bin
|
mkdir -p "$PKGDIR"/usr/bin
|
||||||
mkdir -p "$PKGDIR"/etc/linux_patch_api
|
mkdir -p "$PKGDIR"/etc/linux_patch_api/certs
|
||||||
mkdir -p "$PKGDIR"/etc/init.d
|
mkdir -p "$PKGDIR"/etc/init.d
|
||||||
|
mkdir -p "$PKGDIR"/var/lib/linux_patch_api
|
||||||
|
mkdir -p "$PKGDIR"/var/log/linux_patch_api
|
||||||
|
|
||||||
# Copy files
|
# Copy binary
|
||||||
|
chmod 755 target/x86_64-unknown-linux-musl/release/linux-patch-api
|
||||||
cp target/x86_64-unknown-linux-musl/release/linux-patch-api "$PKGDIR"/usr/bin/
|
cp target/x86_64-unknown-linux-musl/release/linux-patch-api "$PKGDIR"/usr/bin/
|
||||||
chmod 755 "$PKGDIR"/usr/bin/linux-patch-api
|
|
||||||
|
# Copy OpenRC init script
|
||||||
cp configs/linux-patch-api-openrc "$PKGDIR"/etc/init.d/linux-patch-api
|
cp configs/linux-patch-api-openrc "$PKGDIR"/etc/init.d/linux-patch-api
|
||||||
chmod 755 "$PKGDIR"/etc/init.d/linux-patch-api
|
chmod 755 "$PKGDIR"/etc/init.d/linux-patch-api
|
||||||
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml
|
|
||||||
|
# Copy example configs (as .example files - install script creates live configs)
|
||||||
|
cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml.example
|
||||||
|
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml.example
|
||||||
|
|
||||||
|
# Copy install script for APKBUILD
|
||||||
|
mkdir -p /home/builduser/repo
|
||||||
|
cp configs/linux-patch-api.apk-install /home/builduser/repo/linux-patch-api.apk-install
|
||||||
|
|
||||||
# Use /home/builduser as workspace for APKBUILD
|
# Use /home/builduser as workspace for APKBUILD
|
||||||
WORKSPACE_DIR=/home/builduser
|
WORKSPACE_DIR=/home/builduser
|
||||||
|
|
||||||
|
# Get version from Cargo.toml
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
|
||||||
# Create APKBUILD
|
# Create APKBUILD
|
||||||
|
# Note: install= must use literal package name, not $pkgname (unquoted heredoc expands variables)
|
||||||
echo "Creating APKBUILD..."
|
echo "Creating APKBUILD..."
|
||||||
cat > APKBUILD << EOF
|
cat > APKBUILD << EOF
|
||||||
pkgname=linux-patch-api
|
pkgname=linux-patch-api
|
||||||
pkgver=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
pkgver=${VERSION}
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Secure remote package management API for Linux systems"
|
pkgdesc="Secure remote package management API for Linux systems"
|
||||||
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
||||||
@ -72,12 +88,17 @@ arch="x86_64"
|
|||||||
license="MIT"
|
license="MIT"
|
||||||
makedepends=""
|
makedepends=""
|
||||||
depends="openrc"
|
depends="openrc"
|
||||||
|
install="linux-patch-api.apk-install"
|
||||||
|
subpackages=""
|
||||||
source=""
|
source=""
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
install -d "\$pkgdir"/usr/bin
|
install -d "\$pkgdir"/usr/bin
|
||||||
install -d "\$pkgdir"/etc/linux_patch_api
|
install -d "\$pkgdir"/etc/linux_patch_api/certs
|
||||||
install -d "\$pkgdir"/etc/init.d
|
install -d "\$pkgdir"/etc/init.d
|
||||||
|
install -d "\$pkgdir"/var/lib/linux_patch_api
|
||||||
|
install -d "\$pkgdir"/var/log/linux_patch_api
|
||||||
|
|
||||||
cp -r ${WORKSPACE_DIR}/apk-package/usr/bin/* "\$pkgdir"/usr/bin/
|
cp -r ${WORKSPACE_DIR}/apk-package/usr/bin/* "\$pkgdir"/usr/bin/
|
||||||
cp -r ${WORKSPACE_DIR}/apk-package/etc/linux_patch_api/* "\$pkgdir"/etc/linux_patch_api/
|
cp -r ${WORKSPACE_DIR}/apk-package/etc/linux_patch_api/* "\$pkgdir"/etc/linux_patch_api/
|
||||||
cp -r ${WORKSPACE_DIR}/apk-package/etc/init.d/* "\$pkgdir"/etc/init.d/
|
cp -r ${WORKSPACE_DIR}/apk-package/etc/init.d/* "\$pkgdir"/etc/init.d/
|
||||||
|
|||||||
@ -24,35 +24,61 @@ fi
|
|||||||
|
|
||||||
# Create package directory
|
# Create package directory
|
||||||
PKGDIR=$(pwd)/arch-package
|
PKGDIR=$(pwd)/arch-package
|
||||||
|
rm -rf "$PKGDIR"
|
||||||
mkdir -p "$PKGDIR"/usr/bin
|
mkdir -p "$PKGDIR"/usr/bin
|
||||||
mkdir -p "$PKGDIR"/etc/linux_patch_api
|
mkdir -p "$PKGDIR"/etc/linux_patch_api/certs
|
||||||
mkdir -p "$PKGDIR"/usr/lib/systemd/system
|
mkdir -p "$PKGDIR"/usr/lib/systemd/system
|
||||||
|
mkdir -p "$PKGDIR"/var/lib/linux_patch_api
|
||||||
|
mkdir -p "$PKGDIR"/var/log/linux_patch_api
|
||||||
|
|
||||||
# Copy files
|
# Copy binary
|
||||||
|
chmod 755 target/release/linux-patch-api
|
||||||
cp target/release/linux-patch-api "$PKGDIR"/usr/bin/
|
cp target/release/linux-patch-api "$PKGDIR"/usr/bin/
|
||||||
chmod 755 "$PKGDIR"/usr/bin/linux-patch-api
|
|
||||||
|
# Copy systemd service
|
||||||
cp configs/linux-patch-api.service "$PKGDIR"/usr/lib/systemd/system/
|
cp configs/linux-patch-api.service "$PKGDIR"/usr/lib/systemd/system/
|
||||||
cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml
|
|
||||||
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml
|
# Copy example configs (as .example files - install script creates live configs)
|
||||||
|
cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml.example
|
||||||
|
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml.example
|
||||||
|
|
||||||
|
# Copy install script
|
||||||
|
cp configs/linux-patch-api.install PKGBUILD.install
|
||||||
|
|
||||||
# Create PKGBUILD with quoted heredoc to prevent $pkgdir expansion
|
# Create PKGBUILD with quoted heredoc to prevent $pkgdir expansion
|
||||||
# $pkgdir must be literal for makepkg to expand at runtime
|
# $pkgdir must be literal for makepkg to expand at runtime
|
||||||
echo "Creating PKGBUILD..."
|
echo "Creating PKGBUILD..."
|
||||||
cat > PKGBUILD << 'EOF'
|
cat > PKGBUILD << 'EOF'
|
||||||
pkgname=linux-patch-api
|
pkgname=linux-patch-api
|
||||||
pkgver=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
pkgver=VERSION_PLACEHOLDER
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Secure remote package management API for Linux systems"
|
pkgdesc="Secure remote package management API for Linux systems"
|
||||||
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('systemd')
|
depends=('systemd')
|
||||||
|
install=linux-patch-api.install
|
||||||
|
backup=(
|
||||||
|
'etc/linux_patch_api/config.yaml'
|
||||||
|
'etc/linux_patch_api/whitelist.yaml'
|
||||||
|
)
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cp -r /home/builduser/repo/arch-package/* "$pkgdir"/
|
cp -r /home/builduser/repo/arch-package/* "$pkgdir"/
|
||||||
|
|
||||||
|
# Ensure directories exist with proper structure
|
||||||
|
mkdir -p "$pkgdir"/etc/linux_patch_api/certs
|
||||||
|
mkdir -p "$pkgdir"/var/lib/linux_patch_api
|
||||||
|
mkdir -p "$pkgdir"/var/log/linux_patch_api
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Replace version placeholder with actual version from Cargo.toml
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
sed -i "s/VERSION_PLACEHOLDER/$VERSION/" PKGBUILD
|
||||||
|
|
||||||
|
echo "PKGBUILD version: $VERSION"
|
||||||
|
|
||||||
# Create .SRCINFO
|
# Create .SRCINFO
|
||||||
echo "Creating .SRCINFO..."
|
echo "Creating .SRCINFO..."
|
||||||
|
|
||||||
|
|||||||
19
build-rpm.sh
19
build-rpm.sh
@ -21,27 +21,38 @@ if ! command -v rpmbuild &> /dev/null; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Get version from Cargo.toml
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
echo "Building version: $VERSION"
|
||||||
|
|
||||||
# Setup RPM build directory structure
|
# Setup RPM build directory structure
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
# Create source tarball (required by %autosetup in spec file)
|
# Create source tarball (required by %autosetup in spec file)
|
||||||
echo "Creating source tarball..."
|
echo "Creating source tarball..."
|
||||||
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}"
|
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}"
|
||||||
# Copy files excluding unwanted directories using find
|
|
||||||
|
# Copy files excluding unnecessary directories
|
||||||
cp -r . "$TMPDIR/linux-patch-api-${VERSION}/"
|
cp -r . "$TMPDIR/linux-patch-api-${VERSION}/"
|
||||||
|
|
||||||
|
# Remove unnecessary directories from tarball
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/target"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/target"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.git"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.git"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/releases"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/releases"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.github"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.github"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/debian"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/debian"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/arch-package"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.abuild"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/apk-package"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.a0proj"
|
||||||
|
|
||||||
tar -czf ~/rpmbuild/SOURCES/linux-patch-api-${VERSION}.tar.gz -C "$TMPDIR" "linux-patch-api-${VERSION}"
|
tar -czf ~/rpmbuild/SOURCES/linux-patch-api-${VERSION}.tar.gz -C "$TMPDIR" "linux-patch-api-${VERSION}"
|
||||||
rm -rf "$TMPDIR"
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
# Copy spec file
|
# Prepare spec file with dynamic version
|
||||||
echo "Preparing spec file..."
|
echo "Preparing spec file..."
|
||||||
cp linux-patch-api.spec ~/rpmbuild/SPECS/
|
sed "s/VERSION_PLACEHOLDER/$VERSION/" linux-patch-api.spec > ~/rpmbuild/SPECS/linux-patch-api.spec
|
||||||
|
|
||||||
# Build RPM
|
# Build RPM
|
||||||
echo "Building RPM package..."
|
echo "Building RPM package..."
|
||||||
|
|||||||
91
configs/linux-patch-api.apk-install
Normal file
91
configs/linux-patch-api.apk-install
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Alpine Linux install hooks for linux-patch-api
|
||||||
|
# Reference: debian/{preinst,postinst,prerm,postrm}
|
||||||
|
# Alpine APKBUILD install script format: pre-install, post-install, pre-deinstall, post-deinstall
|
||||||
|
|
||||||
|
# Pre-install: Create user/group and directories before files are laid down
|
||||||
|
pre_install() {
|
||||||
|
# Create system group
|
||||||
|
if ! getent group linux-patch-api >/dev/null; then
|
||||||
|
addgroup --system linux-patch-api
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create system user
|
||||||
|
if ! getent passwd linux-patch-api >/dev/null; then
|
||||||
|
adduser --system --ingroup linux-patch-api --home /var/lib/linux_patch_api --no-create-home --shell /sbin/nologin --gecos "Linux Patch API Service" --disabled-password linux-patch-api
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
|
mkdir -p /var/lib/linux_patch_api
|
||||||
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set proper ownership
|
||||||
|
chown -R linux-patch-api:linux-patch-api /var/lib/linux_patch_api
|
||||||
|
chown -R linux-patch-api:linux-patch-api /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set secure permissions
|
||||||
|
chmod 750 /etc/linux_patch_api
|
||||||
|
chmod 750 /etc/linux_patch_api/certs
|
||||||
|
chmod 755 /var/lib/linux_patch_api
|
||||||
|
chmod 755 /var/log/linux_patch_api
|
||||||
|
|
||||||
|
echo "Pre-installation setup completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Post-install: Copy example configs, enable service
|
||||||
|
post_install() {
|
||||||
|
# Copy example configs if they don't exist
|
||||||
|
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||||
|
if [ -f "/etc/linux_patch_api/config.yaml.example" ]; then
|
||||||
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
|
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/config.yaml
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
|
if [ -f "/etc/linux_patch_api/whitelist.yaml.example" ]; then
|
||||||
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/whitelist.yaml
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable the service (but don't start automatically - admin should configure first)
|
||||||
|
rc-update add linux-patch-api default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "linux-patch-api installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
||||||
|
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
||||||
|
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
||||||
|
echo " 4. Start the service: rc-service linux-patch-api start"
|
||||||
|
echo " 5. Check status: rc-service linux-patch-api status"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pre-deinstall: Stop and disable service before files are removed
|
||||||
|
pre_deinstall() {
|
||||||
|
# Stop the service if running
|
||||||
|
if rc-service linux-patch-api status >/dev/null 2>&1; then
|
||||||
|
rc-service linux-patch-api stop
|
||||||
|
echo "Service stopped"
|
||||||
|
else
|
||||||
|
echo "Service was not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable the service
|
||||||
|
rc-update del linux-patch-api default 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Post-deinstall: Clean up on removal
|
||||||
|
post_deinstall() {
|
||||||
|
# Remove directories only if empty (preserve user data on reinstall)
|
||||||
|
rmdir /var/lib/linux_patch_api 2>/dev/null || true
|
||||||
|
rmdir /var/log/linux_patch_api 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "linux-patch-api removed"
|
||||||
|
}
|
||||||
98
configs/linux-patch-api.install
Normal file
98
configs/linux-patch-api.install
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Arch Linux install hooks for linux-patch-api
|
||||||
|
# Reference: debian/{preinst,postinst,prerm,postrm}
|
||||||
|
|
||||||
|
post_install() {
|
||||||
|
# Create system group
|
||||||
|
if ! getent group linux-patch-api &>/dev/null; then
|
||||||
|
groupadd --system linux-patch-api
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create system user
|
||||||
|
if ! getent passwd linux-patch-api &>/dev/null; then
|
||||||
|
useradd --system \
|
||||||
|
--gid linux-patch-api \
|
||||||
|
--home-dir /var/lib/linux_patch_api \
|
||||||
|
--no-create-home \
|
||||||
|
--shell /usr/bin/nologin \
|
||||||
|
--comment "Linux Patch API Service" \
|
||||||
|
linux-patch-api
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
|
mkdir -p /var/lib/linux_patch_api
|
||||||
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set proper ownership
|
||||||
|
chown -R linux-patch-api:linux-patch-api /var/lib/linux_patch_api
|
||||||
|
chown -R linux-patch-api:linux-patch-api /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set secure permissions
|
||||||
|
chmod 750 /etc/linux_patch_api
|
||||||
|
chmod 750 /etc/linux_patch_api/certs
|
||||||
|
chmod 755 /var/lib/linux_patch_api
|
||||||
|
chmod 755 /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Copy example configs if they don't exist
|
||||||
|
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||||
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
|
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/config.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/whitelist.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd daemon
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service (but don't start automatically - admin should configure first)
|
||||||
|
systemctl enable linux-patch-api.service
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "linux-patch-api installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
||||||
|
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
||||||
|
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
||||||
|
echo " 4. Start the service: systemctl start linux-patch-api"
|
||||||
|
echo " 5. Check status: systemctl status linux-patch-api"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
post_upgrade() {
|
||||||
|
# Reload systemd daemon on upgrade
|
||||||
|
systemctl daemon-reload
|
||||||
|
}
|
||||||
|
|
||||||
|
pre_remove() {
|
||||||
|
# Stop the service before removal
|
||||||
|
if systemctl is-active --quiet linux-patch-api.service; then
|
||||||
|
systemctl stop linux-patch-api.service
|
||||||
|
echo "Service stopped successfully"
|
||||||
|
else
|
||||||
|
echo "Service was not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable the service
|
||||||
|
if systemctl is-enabled --quiet linux-patch-api.service 2>/dev/null; then
|
||||||
|
systemctl disable linux-patch-api.service
|
||||||
|
echo "Service disabled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
post_remove() {
|
||||||
|
# Reload systemd to remove service file
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove directories only if empty (preserve user data on upgrade/reinstall)
|
||||||
|
# Arch doesn't have purge vs remove distinction like Debian
|
||||||
|
rmdir --ignore-fail-on-non-empty /var/lib/linux_patch_api 2>/dev/null || true
|
||||||
|
rmdir --ignore-fail-on-non-empty /var/log/linux_patch_api 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "linux-patch-api removed"
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
|
|
||||||
Name: linux-patch-api
|
Name: linux-patch-api
|
||||||
Version: 1.0.0
|
Version: VERSION_PLACEHOLDER
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Secure remote package management API for Linux systems
|
Summary: Secure remote package management API for Linux systems
|
||||||
License: MIT
|
License: MIT
|
||||||
@ -162,6 +162,8 @@ fi
|
|||||||
/lib/systemd/system/linux-patch-api.service
|
/lib/systemd/system/linux-patch-api.service
|
||||||
%config(noreplace) /etc/linux_patch_api/config.yaml.example
|
%config(noreplace) /etc/linux_patch_api/config.yaml.example
|
||||||
%config(noreplace) /etc/linux_patch_api/whitelist.yaml.example
|
%config(noreplace) /etc/linux_patch_api/whitelist.yaml.example
|
||||||
|
%ghost %config(noreplace) /etc/linux_patch_api/config.yaml
|
||||||
|
%ghost %config(noreplace) /etc/linux_patch_api/whitelist.yaml
|
||||||
%dir /etc/linux_patch_api
|
%dir /etc/linux_patch_api
|
||||||
%dir /etc/linux_patch_api/certs
|
%dir /etc/linux_patch_api/certs
|
||||||
%dir /var/lib/linux_patch_api
|
%dir /var/lib/linux_patch_api
|
||||||
@ -169,7 +171,7 @@ fi
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
%changelog
|
%changelog
|
||||||
* Thu Apr 09 2026 Echo <echo@moon-dragon.us> - 1.0.0-1
|
* Thu Apr 09 2026 Echo <echo@moon-dragon.us> - 1.1.7-1
|
||||||
- Initial production release
|
- Initial production release
|
||||||
- Secure mTLS-authenticated REST API for remote package management
|
- Secure mTLS-authenticated REST API for remote package management
|
||||||
- 15 API endpoints for package install/remove, patch application, system management
|
- 15 API endpoints for package install/remove, patch application, system management
|
||||||
|
|||||||
@ -18,6 +18,10 @@ pub struct EnrollmentRequest {
|
|||||||
pub fqdn: String,
|
pub fqdn: String,
|
||||||
pub ip_address: String,
|
pub ip_address: String,
|
||||||
pub os_details: serde_json::Value,
|
pub os_details: serde_json::Value,
|
||||||
|
/// Short hostname (from /etc/hostname or hostname command).
|
||||||
|
/// Used by the manager to populate `display_name` on approval.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub hostname: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response from `POST /api/v1/enroll` (HTTP 202).
|
/// Response from `POST /api/v1/enroll` (HTTP 202).
|
||||||
@ -220,12 +224,18 @@ impl EnrollmentClient {
|
|||||||
let os_details = identity::get_os_details()
|
let os_details = identity::get_os_details()
|
||||||
.context("Failed to collect OS details — /etc/os-release may be missing")?;
|
.context("Failed to collect OS details — /etc/os-release may be missing")?;
|
||||||
|
|
||||||
// 2. Build EnrollmentRequest struct
|
// 2. Collect short hostname for display_name on manager
|
||||||
|
let hostname = identity::get_hostname()
|
||||||
|
.map_err(|e| tracing::warn!(error = %e, "Failed to determine hostname — display_name will use FQDN fallback"))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// 3. Build EnrollmentRequest struct
|
||||||
let request = EnrollmentRequest {
|
let request = EnrollmentRequest {
|
||||||
machine_id,
|
machine_id,
|
||||||
fqdn,
|
fqdn,
|
||||||
ip_address,
|
ip_address,
|
||||||
os_details,
|
os_details,
|
||||||
|
hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@ -502,6 +512,7 @@ mod tests {
|
|||||||
fqdn: "node.example.com".into(),
|
fqdn: "node.example.com".into(),
|
||||||
ip_address: "192.168.1.10".into(),
|
ip_address: "192.168.1.10".into(),
|
||||||
os_details: serde_json::json!({"distro": "Debian", "version": "12"}),
|
os_details: serde_json::json!({"distro": "Debian", "version": "12"}),
|
||||||
|
hostname: Some("node".into()),
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&request).expect("Failed to serialize EnrollmentRequest");
|
let json = serde_json::to_string(&request).expect("Failed to serialize EnrollmentRequest");
|
||||||
assert!(json.contains("machine_id"));
|
assert!(json.contains("machine_id"));
|
||||||
|
|||||||
@ -31,36 +31,109 @@ pub fn get_machine_id() -> Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the fully-qualified domain name.
|
/// Resolve the fully-qualified domain name.
|
||||||
/// Strategy: `gethostname` via std → fallback to `hostname` CLI → "localhost".
|
///
|
||||||
|
/// Strategy (in priority order):
|
||||||
|
/// 1. `hostname -f` → if result contains `.`, it's a real FQDN
|
||||||
|
/// 2. `hostname` + `hostname -d` → combine short hostname + domain
|
||||||
|
/// 3. `/etc/hostname` → short hostname fallback
|
||||||
|
/// 4. `hostname` command → last resort
|
||||||
|
/// 5. `"localhost"` → final fallback
|
||||||
pub fn get_fqdn() -> Result<String> {
|
pub fn get_fqdn() -> Result<String> {
|
||||||
// Try reading from hostname file first (common on systemd systems)
|
// 1. Try `hostname -f` — returns FQDN on properly configured systems
|
||||||
|
if let Ok(output) = Command::new("hostname").arg("-f").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if !name.is_empty() && name.contains('.') && name != "(none)" {
|
||||||
|
tracing::debug!(fqdn = %name, "Resolved FQDN via hostname -f");
|
||||||
|
return Ok(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try combining short hostname + domain from `hostname -d`
|
||||||
|
if let Ok(short_output) = Command::new("hostname").output() {
|
||||||
|
if short_output.status.success() {
|
||||||
|
let short = String::from_utf8_lossy(&short_output.stdout).trim().to_string();
|
||||||
|
if !short.is_empty() && short != "(none)" {
|
||||||
|
if let Ok(domain_output) = Command::new("hostname").arg("-d").output() {
|
||||||
|
if domain_output.status.success() {
|
||||||
|
let domain = String::from_utf8_lossy(&domain_output.stdout).trim().to_string();
|
||||||
|
if !domain.is_empty() {
|
||||||
|
let fqdn = format!("{}.{}", short, domain);
|
||||||
|
tracing::debug!(fqdn = %fqdn, "Resolved FQDN via hostname + hostname -d");
|
||||||
|
return Ok(fqdn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Domain not available — fall through to try other methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try reading from /etc/hostname (common on systemd systems, usually short hostname)
|
||||||
if let Ok(name) = fs::read_to_string("/etc/hostname") {
|
if let Ok(name) = fs::read_to_string("/etc/hostname") {
|
||||||
let trimmed = name.trim().to_string();
|
let trimmed = name.trim().to_string();
|
||||||
if !trimmed.is_empty() && trimmed != "(none)" {
|
if !trimmed.is_empty() && trimmed != "(none)" {
|
||||||
|
tracing::debug!(hostname = %trimmed, "Resolved hostname via /etc/hostname (may be short)");
|
||||||
return Ok(trimmed);
|
return Ok(trimmed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to hostname command
|
// 4. Fallback to plain hostname command
|
||||||
if let Ok(output) = Command::new("hostname").arg("-f").output() {
|
|
||||||
if output.status.success() {
|
|
||||||
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
if !name.is_empty() {
|
|
||||||
return Ok(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to plain hostname
|
|
||||||
if let Ok(output) = Command::new("hostname").output() {
|
if let Ok(output) = Command::new("hostname").output() {
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
|
tracing::debug!(hostname = %name, "Resolved hostname via hostname command");
|
||||||
return Ok(name);
|
return Ok(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Final fallback
|
||||||
|
tracing::warn!("Could not determine hostname — falling back to localhost");
|
||||||
|
Ok("localhost".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the short hostname (without domain).
|
||||||
|
///
|
||||||
|
/// Strategy: `/etc/hostname` → `hostname` command → split FQDN on `.` → `"localhost"`.
|
||||||
|
pub fn get_hostname() -> Result<String> {
|
||||||
|
// Try reading from /etc/hostname (usually contains the short hostname)
|
||||||
|
if let Ok(name) = fs::read_to_string("/etc/hostname") {
|
||||||
|
let trimmed = name.trim().to_string();
|
||||||
|
if !trimmed.is_empty() && trimmed != "(none)" {
|
||||||
|
// If it contains a dot, take just the first component
|
||||||
|
let short = trimmed.split('.').next().unwrap_or(&trimmed).to_string();
|
||||||
|
tracing::debug!(hostname = %short, "Resolved short hostname via /etc/hostname");
|
||||||
|
return Ok(short);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try hostname command
|
||||||
|
if let Ok(output) = Command::new("hostname").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
// If it contains a dot, take just the first component
|
||||||
|
let short = name.split('.').next().unwrap_or(&name).to_string();
|
||||||
|
tracing::debug!(hostname = %short, "Resolved short hostname via hostname command");
|
||||||
|
return Ok(short);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try splitting FQDN from get_fqdn()
|
||||||
|
if let Ok(fqdn) = get_fqdn() {
|
||||||
|
if fqdn != "localhost" && fqdn.contains('.') {
|
||||||
|
let short = fqdn.split('.').next().unwrap_or(&fqdn).to_string();
|
||||||
|
tracing::debug!(hostname = %short, "Resolved short hostname by splitting FQDN");
|
||||||
|
return Ok(short);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback
|
||||||
|
tracing::warn!("Could not determine short hostname — falling back to localhost");
|
||||||
Ok("localhost".into())
|
Ok("localhost".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,6 +439,56 @@ mod tests {
|
|||||||
assert!(!fqdn.is_empty(), "FQDN should not be empty");
|
assert!(!fqdn.is_empty(), "FQDN should not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fqdn_prefers_full_domain() {
|
||||||
|
// If hostname -f returns a value with a dot, get_fqdn should return it
|
||||||
|
// (not the short hostname from /etc/hostname)
|
||||||
|
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||||||
|
// On properly configured systems, FQDN should contain at least one dot
|
||||||
|
// If it doesn't, it's likely a short hostname from /etc/hostname
|
||||||
|
if fqdn.contains('.') {
|
||||||
|
// FQDN contains domain — good
|
||||||
|
assert!(
|
||||||
|
fqdn.split('.').count() >= 2,
|
||||||
|
"FQDN should have at least host.domain format, got: {}",
|
||||||
|
fqdn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If no dot, it's a short hostname — acceptable fallback but not ideal
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_is_not_empty() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
assert!(!hostname.is_empty(), "Hostname should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_is_short_form() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
// Short hostname should NOT contain dots
|
||||||
|
assert!(
|
||||||
|
!hostname.contains('.'),
|
||||||
|
"Short hostname should not contain dots, got: {}",
|
||||||
|
hostname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostname_is_prefix_of_fqdn() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||||||
|
// If FQDN contains a dot, hostname should be the first component
|
||||||
|
if fqdn.contains('.') {
|
||||||
|
let fqdn_prefix = fqdn.split('.').next().unwrap_or(&fqdn);
|
||||||
|
assert_eq!(
|
||||||
|
hostname, fqdn_prefix,
|
||||||
|
"Short hostname '{}' should match FQDN prefix '{}'",
|
||||||
|
hostname, fqdn_prefix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn os_details_contains_kernel() {
|
fn os_details_contains_kernel() {
|
||||||
let details = get_os_details().expect("Failed to get OS details");
|
let details = get_os_details().expect("Failed to get OS details");
|
||||||
|
|||||||
@ -16,8 +16,8 @@ pub use client::{
|
|||||||
};
|
};
|
||||||
/// Re-export identity extraction functions.
|
/// Re-export identity extraction functions.
|
||||||
pub use identity::{
|
pub use identity::{
|
||||||
get_fqdn, get_ip_addresses, get_ip_for_interface, get_machine_id, get_os_details,
|
get_fqdn, get_hostname, get_ip_addresses, get_ip_for_interface, get_machine_id,
|
||||||
get_primary_ip, get_route_source_ip, is_container_bridge, is_link_local,
|
get_os_details, get_primary_ip, get_route_source_ip, is_container_bridge, is_link_local,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Run the full enrollment flow against the manager at the given URL.
|
/// Run the full enrollment flow against the manager at the given URL.
|
||||||
|
|||||||
@ -473,6 +473,18 @@ async fn test_registration_payload_structure() {
|
|||||||
os_obj.contains_key("distro") || os_obj.contains_key("kernel"),
|
os_obj.contains_key("distro") || os_obj.contains_key("kernel"),
|
||||||
"os_details should contain distro or kernel information"
|
"os_details should contain distro or kernel information"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Verify hostname field (optional, may be present or absent)
|
||||||
|
// When present, it should be a non-empty string without dots (short hostname)
|
||||||
|
if let Some(hostname) = payload.get("hostname").and_then(|v| v.as_str()) {
|
||||||
|
assert!(!hostname.is_empty(), "hostname should not be empty when present");
|
||||||
|
assert!(
|
||||||
|
!hostname.contains('.'),
|
||||||
|
"hostname should be short form (no dots), got: {}",
|
||||||
|
hostname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// hostname field is optional — its absence is valid (skip_serializing_if = None)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
//! Verifies machine-id, FQDN, IP address collection, and OS detail parsing.
|
//! Verifies machine-id, FQDN, IP address collection, and OS detail parsing.
|
||||||
|
|
||||||
use linux_patch_api::enroll::identity::{
|
use linux_patch_api::enroll::identity::{
|
||||||
get_fqdn, get_ip_addresses, get_machine_id, get_os_details, get_primary_ip,
|
get_fqdn, get_hostname, get_ip_addresses, get_machine_id, get_os_details, get_primary_ip,
|
||||||
get_route_source_ip, is_container_bridge, is_link_local,
|
get_route_source_ip, is_container_bridge, is_link_local,
|
||||||
};
|
};
|
||||||
use linux_patch_api::enroll::EnrollmentRequest;
|
use linux_patch_api::enroll::EnrollmentRequest;
|
||||||
@ -138,6 +138,97 @@ fn test_fqdn_reasonable_length() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Hostname Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hostname_returns_non_empty() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
assert!(!hostname.is_empty(), "Hostname should not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hostname_is_short_form() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
// Short hostname should NOT contain dots (that would be an FQDN)
|
||||||
|
assert!(
|
||||||
|
!hostname.contains('.'),
|
||||||
|
"Short hostname should not contain dots, got: {}",
|
||||||
|
hostname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hostname_is_consistent() {
|
||||||
|
let h1 = get_hostname().expect("Failed to get hostname (call 1)");
|
||||||
|
let h2 = get_hostname().expect("Failed to get hostname (call 2)");
|
||||||
|
assert_eq!(h1, h2, "Hostname should be consistent across calls");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hostname_is_subset_of_fqdn() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||||||
|
// If FQDN contains a dot, the short hostname should be the first component
|
||||||
|
if fqdn.contains('.') {
|
||||||
|
let fqdn_prefix = fqdn.split('.').next().unwrap_or(&fqdn);
|
||||||
|
assert_eq!(
|
||||||
|
hostname, fqdn_prefix,
|
||||||
|
"Short hostname '{}' should match FQDN prefix '{}'",
|
||||||
|
hostname, fqdn_prefix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hostname_valid_characters() {
|
||||||
|
let hostname = get_hostname().expect("Failed to get hostname");
|
||||||
|
for c in hostname.chars() {
|
||||||
|
assert!(
|
||||||
|
c.is_alphanumeric() || c == '-',
|
||||||
|
"Short hostname contains invalid character: {:?}",
|
||||||
|
c
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_enrollment_hostname_field_serializes() {
|
||||||
|
// Verify that hostname field serializes correctly when Some and when None
|
||||||
|
let request_with_hostname = EnrollmentRequest {
|
||||||
|
machine_id: "test-id".to_string(),
|
||||||
|
fqdn: "host.example.com".to_string(),
|
||||||
|
ip_address: "10.0.0.1".to_string(),
|
||||||
|
os_details: serde_json::json!({"name": "Test"}),
|
||||||
|
hostname: Some("host".to_string()),
|
||||||
|
};
|
||||||
|
let json_with = serde_json::to_string(&request_with_hostname)
|
||||||
|
.expect("Should serialize with hostname");
|
||||||
|
assert!(
|
||||||
|
json_with.contains("\"hostname\""),
|
||||||
|
"hostname field should be present in JSON when Some"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
json_with.contains("\"host\""),
|
||||||
|
"hostname value should be 'host' in JSON"
|
||||||
|
);
|
||||||
|
|
||||||
|
let request_without_hostname = EnrollmentRequest {
|
||||||
|
machine_id: "test-id".to_string(),
|
||||||
|
fqdn: "host.example.com".to_string(),
|
||||||
|
ip_address: "10.0.0.1".to_string(),
|
||||||
|
os_details: serde_json::json!({"name": "Test"}),
|
||||||
|
hostname: None,
|
||||||
|
};
|
||||||
|
let json_without = serde_json::to_string(&request_without_hostname)
|
||||||
|
.expect("Should serialize without hostname");
|
||||||
|
assert!(
|
||||||
|
!json_without.contains("\"hostname\""),
|
||||||
|
"hostname field should be omitted from JSON when None (skip_serializing_if)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// IP Address Tests
|
// IP Address Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -371,11 +462,14 @@ fn test_enrollment_payload_construction() {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| "127.0.0.1".to_string());
|
.unwrap_or_else(|| "127.0.0.1".to_string());
|
||||||
|
|
||||||
|
let hostname = get_hostname().ok();
|
||||||
|
|
||||||
let request = EnrollmentRequest {
|
let request = EnrollmentRequest {
|
||||||
machine_id,
|
machine_id,
|
||||||
fqdn,
|
fqdn,
|
||||||
ip_address: primary_ip,
|
ip_address: primary_ip,
|
||||||
os_details,
|
os_details,
|
||||||
|
hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify payload serializes to valid JSON
|
// Verify payload serializes to valid JSON
|
||||||
@ -412,11 +506,14 @@ fn test_enrollment_payload_matches_manager_schema() {
|
|||||||
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||||||
let os_details = get_os_details().expect("Failed to get OS details");
|
let os_details = get_os_details().expect("Failed to get OS details");
|
||||||
|
|
||||||
|
let hostname = get_hostname().ok();
|
||||||
|
|
||||||
let request = EnrollmentRequest {
|
let request = EnrollmentRequest {
|
||||||
machine_id: machine_id.clone(),
|
machine_id: machine_id.clone(),
|
||||||
fqdn: fqdn.clone(),
|
fqdn: fqdn.clone(),
|
||||||
ip_address: ip_addrs.first().cloned().unwrap_or_default(),
|
ip_address: ip_addrs.first().cloned().unwrap_or_default(),
|
||||||
os_details: os_details.clone(),
|
os_details: os_details.clone(),
|
||||||
|
hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate against expected manager API schema
|
// Validate against expected manager API schema
|
||||||
@ -449,11 +546,14 @@ fn test_enrollment_payload_roundtrip() {
|
|||||||
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||||||
let os_details = get_os_details().expect("Failed to get OS details");
|
let os_details = get_os_details().expect("Failed to get OS details");
|
||||||
|
|
||||||
|
let hostname = get_hostname().ok();
|
||||||
|
|
||||||
let request = EnrollmentRequest {
|
let request = EnrollmentRequest {
|
||||||
machine_id,
|
machine_id,
|
||||||
fqdn,
|
fqdn,
|
||||||
ip_address: ip_addrs.first().cloned().unwrap_or_default(),
|
ip_address: ip_addrs.first().cloned().unwrap_or_default(),
|
||||||
os_details,
|
os_details,
|
||||||
|
hostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialize to JSON then deserialize back
|
// Serialize to JSON then deserialize back
|
||||||
@ -464,6 +564,7 @@ fn test_enrollment_payload_roundtrip() {
|
|||||||
assert_eq!(request.machine_id, deserialized.machine_id);
|
assert_eq!(request.machine_id, deserialized.machine_id);
|
||||||
assert_eq!(request.fqdn, deserialized.fqdn);
|
assert_eq!(request.fqdn, deserialized.fqdn);
|
||||||
assert_eq!(request.ip_address, deserialized.ip_address);
|
assert_eq!(request.ip_address, deserialized.ip_address);
|
||||||
|
assert_eq!(request.hostname, deserialized.hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -507,6 +608,10 @@ fn test_identity_functions_do_not_panic() {
|
|||||||
let _ = get_fqdn();
|
let _ = get_fqdn();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let _ = std::panic::catch_unwind(|| {
|
||||||
|
let _ = get_hostname();
|
||||||
|
});
|
||||||
|
|
||||||
let _ = std::panic::catch_unwind(|| {
|
let _ = std::panic::catch_unwind(|| {
|
||||||
let _ = get_ip_addresses();
|
let _ = get_ip_addresses();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user