ServerlessBase Blog
  • How to Create a Server Inventory System

    A 150-160 character meta description containing 'server inventory system'

    How to Create a Server Inventory System

    You've been managing servers for months. Maybe you're running a handful of VMs, some containers, and a few bare metal machines. Every time you need to check something—CPU usage, disk space, or who has access to a particular instance—you open a terminal, SSH into the machine, and run a bunch of commands. It works, but it's slow and error-prone. You lose track of which server has which application, which one has the latest security patches, and which one is actually running.

    A server inventory system solves this problem by creating a centralized, searchable record of all your infrastructure assets. It gives you a single source of truth for server details, configurations, and status. In this guide, you'll build a practical server inventory system from scratch using a simple database and a web interface. You'll learn how to track server metadata, document configurations, and monitor status—all in a way that scales with your infrastructure.

    What Makes a Good Server Inventory

    A server inventory system needs to answer three fundamental questions: What servers do you have? What are they doing? What are their configurations? The answers to these questions change constantly as you deploy, decommission, and modify infrastructure. Your inventory system must reflect those changes in real time.

    Think of a server inventory like a library catalog. Each server is a book with metadata—title, author, location, and current status. You can search for books by title, find where a specific book is located, and check if it's currently checked out. Similarly, you should be able to search your server inventory by hostname, IP address, or application name, see where a server is deployed, and check its current operational status.

    Core Components of an Inventory System

    A functional server inventory consists of three main components: data storage, data entry, and data visualization. The storage layer holds all server information in a structured format. The entry layer allows you to add, update, and remove server records. The visualization layer presents the data in a way that's easy to understand and navigate.

    The storage layer can be a simple database like SQLite, PostgreSQL, or MySQL. For small teams, SQLite works perfectly because it's lightweight and requires no separate server process. For larger deployments, PostgreSQL provides better performance and concurrent access. The entry layer can be a web form, a command-line tool, or an API that accepts JSON payloads. The visualization layer is typically a web dashboard that displays server lists, details, and status indicators.

    Designing Your Inventory Schema

    Your database schema determines what information you can track about each server. At minimum, you need fields for hostname, IP address, operating system, and status. As your needs grow, you'll want to add more fields for application details, environment variables, and custom metadata.

    The following table compares different approaches to server inventory systems:

    FactorSpreadsheetSimple DatabaseAdvanced CMDB
    Ease of SetupVery EasyEasyModerate
    ScalabilityPoorGoodExcellent
    Real-time UpdatesManualAutomaticAutomatic
    Search & FilteringBasicAdvancedAdvanced
    CostFreeLowHigh
    Best ForSmall TeamsGrowing TeamsEnterprise

    Building the Database Schema

    Let's design a practical schema for a server inventory system. We'll use PostgreSQL because it's widely available and provides excellent query capabilities. The schema consists of two main tables: servers and server_metadata.

    The servers table stores core server information:

    CREATE TABLE servers (
      id SERIAL PRIMARY KEY,
      hostname VARCHAR(255) NOT NULL UNIQUE,
      ip_address VARCHAR(45) NOT NULL,
      os_type VARCHAR(50) NOT NULL,
      os_version VARCHAR(100),
      cpu_cores INTEGER,
      memory_gb INTEGER,
      disk_gb INTEGER,
      status VARCHAR(50) DEFAULT 'unknown',
      last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      notes TEXT
    );

    The server_metadata table stores additional, flexible metadata:

    CREATE TABLE server_metadata (
      id SERIAL PRIMARY KEY,
      server_id INTEGER REFERENCES servers(id) ON DELETE CASCADE,
      key VARCHAR(255) NOT NULL,
      value TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

    This schema gives you a solid foundation. You can add more tables for applications, users, or custom fields as needed.

    Creating the Web Interface

    Now let's build a simple web interface to manage your server inventory. We'll use Flask for the backend and Jinja2 for templating. First, install the required packages:

    pip install flask flask-sqlalchemy

    Create a basic Flask application:

    from flask import Flask, render_template, request, redirect, url_for
    from flask_sqlalchemy import SQLAlchemy
     
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///servers.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db = SQLAlchemy(app)
     
    class Server(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        hostname = db.Column(db.String(255), unique=True, nullable=False)
        ip_address = db.Column(db.String(45), nullable=False)
        os_type = db.Column(db.String(50), nullable=False)
        os_version = db.Column(db.String(100))
        cpu_cores = db.Column(db.Integer)
        memory_gb = db.Column(db.Integer)
        disk_gb = db.Column(db.Integer)
        status = db.Column(db.String(50), default='unknown')
        last_updated = db.Column(db.DateTime, default=db.func.current_timestamp())
        notes = db.Column(db.Text)
     
        def __repr__(self):
            return f'<Server {self.hostname}>'
     
    @app.route('/')
    def index():
        servers = Server.query.all()
        return render_template('index.html', servers=servers)
     
    @app.route('/add', methods=['GET', 'POST'])
    def add_server():
        if request.method == 'POST':
            hostname = request.form['hostname']
            ip_address = request.form['ip_address']
            os_type = request.form['os_type']
            os_version = request.form.get('os_version')
            cpu_cores = request.form.get('cpu_cores')
            memory_gb = request.form.get('memory_gb')
            disk_gb = request.form.get('disk_gb')
            status = request.form.get('status', 'unknown')
            notes = request.form.get('notes')
     
            server = Server(
                hostname=hostname,
                ip_address=ip_address,
                os_type=os_type,
                os_version=os_version,
                cpu_cores=cpu_cores,
                memory_gb=memory_gb,
                disk_gb=disk_gb,
                status=status,
                notes=notes
            )
            db.session.add(server)
            db.session.commit()
            return redirect(url_for('index'))
     
        return render_template('add_server.html')
     
    if __name__ == '__main__':
        with app.app_context():
            db.create_all()
        app.run(debug=True)

    This code creates a basic Flask application with routes for viewing servers and adding new ones. The database is automatically created when you run the application.

    Creating the HTML Templates

    Create a templates directory and add the following files:

    templates/index.html:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Server Inventory</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            table { width: 100%; border-collapse: collapse; margin-top: 20px; }
            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
            th { background-color: #f2f2f2; }
            tr:hover { background-color: #f5f5f5; }
            .status { padding: 4px 8px; border-radius: 4px; }
            .status.running { background-color: #d4edda; color: #155724; }
            .status.stopped { background-color: #f8d7da; color: #721c24; }
            .status.unknown { background-color: #e2e3e5; color: #383d41; }
        </style>
    </head>
    <body>
        <h1>Server Inventory</h1>
        <a href="{{ url_for('add_server') }}">Add New Server</a>
        <table>
            <thead>
                <tr>
                    <th>Hostname</th>
                    <th>IP Address</th>
                    <th>OS</th>
                    <th>CPU</th>
                    <th>Memory</th>
                    <th>Disk</th>
                    <th>Status</th>
                    <th>Last Updated</th>
                </tr>
            </thead>
            <tbody>
                {% for server in servers %}
                <tr>
                    <td>{{ server.hostname }}</td>
                    <td>{{ server.ip_address }}</td>
                    <td>{{ server.os_type }} {{ server.os_version or '' }}</td>
                    <td>{{ server.cpu_cores or 'N/A' }}</td>
                    <td>{{ server.memory_gb or 'N/A' }} GB</td>
                    <td>{{ server.disk_gb or 'N/A' }} GB</td>
                    <td><span class="status {{ server.status }}">{{ server.status }}</span></td>
                    <td>{{ server.last_updated.strftime('%Y-%m-%d %H:%M') }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </body>
    </html>

    templates/add_server.html:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Add Server</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            form { max-width: 600px; margin-top: 20px; }
            .form-group { margin-bottom: 15px; }
            label { display: block; margin-bottom: 5px; }
            input, select, textarea { width: 100%; padding: 8px; }
            button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }
            button:hover { background-color: #45a049; }
        </style>
    </head>
    <body>
        <h1>Add New Server</h1>
        <form method="POST">
            <div class="form-group">
                <label for="hostname">Hostname</label>
                <input type="text" id="hostname" name="hostname" required>
            </div>
            <div class="form-group">
                <label for="ip_address">IP Address</label>
                <input type="text" id="ip_address" name="ip_address" required>
            </div>
            <div class="form-group">
                <label for="os_type">Operating System</label>
                <select id="os_type" name="os_type" required>
                    <option value="Ubuntu">Ubuntu</option>
                    <option value="Debian">Debian</option>
                    <option value="CentOS">CentOS</option>
                    <option value="AlmaLinux">AlmaLinux</option>
                    <option value="Rocky Linux">Rocky Linux</option>
                    <option value="Fedora">Fedora</option>
                    <option value="Arch Linux">Arch Linux</option>
                    <option value="Windows Server">Windows Server</option>
                </select>
            </div>
            <div class="form-group">
                <label for="os_version">OS Version</label>
                <input type="text" id="os_version" name="os_version">
            </div>
            <div class="form-group">
                <label for="cpu_cores">CPU Cores</label>
                <input type="number" id="cpu_cores" name="cpu_cores">
            </div>
            <div class="form-group">
                <label for="memory_gb">Memory (GB)</label>
                <input type="number" id="memory_gb" name="memory_gb">
            </div>
            <div class="form-group">
                <label for="disk_gb">Disk Space (GB)</label>
                <input type="number" id="disk_gb" name="disk_gb">
            </div>
            <div class="form-group">
                <label for="status">Status</label>
                <select id="status" name="status">
                    <option value="running">Running</option>
                    <option value="stopped">Stopped</option>
                    <option value="maintenance">Maintenance</option>
                    <option value="unknown">Unknown</option>
                </select>
            </div>
            <div class="form-group">
                <label for="notes">Notes</label>
                <textarea id="notes" name="notes" rows="4"></textarea>
            </div>
            <button type="submit">Add Server</button>
        </form>
        <a href="{{ url_for('index') }}">Back to Inventory</a>
    </body>
    </html>

    Automating Server Discovery

    Manually adding servers to your inventory is tedious and error-prone. Let's create a script that automatically discovers servers on your network and adds them to the inventory.

    First, create a script called discover_servers.py:

    import subprocess
    import socket
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    import ipaddress
     
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///servers.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db = SQLAlchemy(app)
     
    class Server(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        hostname = db.Column(db.String(255), unique=True, nullable=False)
        ip_address = db.Column(db.String(45), nullable=False)
        os_type = db.Column(db.String(50), nullable=False)
        os_version = db.Column(db.String(100))
        cpu_cores = db.Column(db.Integer)
        memory_gb = db.Column(db.Integer)
        disk_gb = db.Column(db.Integer)
        status = db.Column(db.String(50), default='unknown')
        last_updated = db.Column(db.DateTime, default=db.func.current_timestamp())
        notes = db.Column(db.Text)
     
    def get_hostname(ip):
        try:
            hostname = socket.gethostbyaddr(ip)[0]
            return hostname
        except socket.herror:
            return None
     
    def get_os_info(ip):
        try:
            result = subprocess.run(
                ['ssh', '-o', 'StrictHostKeyChecking=no', '-o', 'ConnectTimeout=5', f'{ip}', 'cat /etc/os-release'],
                capture_output=True,
                text=True,
                timeout=10
            )
            if result.returncode == 0:
                lines = result.stdout.split('\n')
                for line in lines:
                    if line.startswith('ID='):
                        os_type = line.split('=')[1].strip('"')
                    elif line.startswith('VERSION_ID='):
                        os_version = line.split('=')[1].strip('"')
                return os_type, os_version
        except Exception as e:
            print(f"Error getting OS info for {ip}: {e}")
        return None, None
     
    def discover_servers(network):
        discovered = []
        try:
            network = ipaddress.ip_network(network)
            for host in network.hosts():
                ip = str(host)
                hostname = get_hostname(ip)
                os_type, os_version = get_os_info(ip)
                if hostname or os_type:
                    discovered.append({
                        'ip': ip,
                        'hostname': hostname or ip,
                        'os_type': os_type or 'unknown',
                        'os_version': os_version or ''
                    })
        except ValueError:
            print(f"Invalid network: {network}")
        return discovered
     
    if __name__ == '__main__':
        with app.app_context():
            db.create_all()
            network = input("Enter network range (e.g., 192.168.1.0/24): ")
            servers = discover_servers(network)
            for server in servers:
                existing = Server.query.filter_by(hostname=server['hostname']).first()
                if not existing:
                    new_server = Server(
                        hostname=server['hostname'],
                        ip_address=server['ip'],
                        os_type=server['os_type'],
                        os_version=server['os_version']
                    )
                    db.session.add(new_server)
                    print(f"Discovered: {server['hostname']} ({server['ip']})")
            db.session.commit()
            print(f"Added {len(servers)} servers to inventory")

    This script uses SSH to connect to servers on your network and gather information about their operating system. It's a good starting point, but you'll want to add error handling and authentication for production use.

    Integrating with ServerlessBase

    Platforms like ServerlessBase can help automate parts of your server inventory management. When you deploy applications through a platform, it can automatically register servers in your inventory with relevant metadata. This integration reduces manual work and ensures your inventory stays synchronized with your deployments.

    For example, when you deploy an application to a server, ServerlessBase can automatically update the server's status to "running" and record the application details. This creates a tight feedback loop between your deployment pipeline and your inventory system.

    Best Practices for Server Inventory

    Maintaining an accurate server inventory requires discipline and automation. Here are some best practices to follow:

    1. Update regularly: Schedule regular checks to update server status and metadata. This can be done through cron jobs or scheduled tasks.

    2. Use consistent naming conventions: Establish clear naming conventions for hostnames and IP addresses. This makes searching and filtering easier.

    3. Document configurations: Record important configuration details like firewall rules, security settings, and custom scripts.

    4. Include relationships: Track which applications run on which servers and which databases they connect to.

    5. Set up alerts: Configure alerts for servers that go offline or have unusual resource usage.

    6. Regular audits: Periodically review your inventory to remove obsolete entries and correct errors.

    Common Pitfalls to Avoid

    Building a server inventory system has several common pitfalls. One is making it too complex. Start simple and add features as needed. Another is neglecting updates. An inventory that doesn't reflect reality is worse than no inventory at all. Finally, don't forget about security. Your inventory contains sensitive information about your infrastructure, so protect it appropriately.

    Conclusion

    A server inventory system is one of the most valuable tools for infrastructure management. It gives you visibility into your entire infrastructure, helps you troubleshoot issues faster, and ensures you never lose track of a server. The system you built in this guide provides a solid foundation that you can extend as your needs grow.

    The most important step is to start using your inventory system consistently. Even a basic inventory will save you time and reduce errors compared to managing servers manually. Once you're comfortable with the basics, you can add more features like automated discovery, integration with deployment tools, and advanced reporting.

    The next step is to deploy your inventory system to a server and populate it with your existing infrastructure. Take the time to add all your servers manually first, then gradually automate the process. Before you know it, you'll have a comprehensive, up-to-date inventory that makes managing your infrastructure much easier.

    Leave comment