Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
ENV/
env/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Environment
.env
.env.local

# Testing
.pytest_cache/
.coverage
htmlcov/

# Docker
postgres_data/

28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# STAGE 1: Base
# We load dependencies here so they are cached for both test and prod
FROM python:3.11-slim AS base

WORKDIR /app

# Prevent Python from writing pyc files and buffering stdout
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# STAGE 2: Test
# This stage prepares the test environment (tests run via docker compose, not during build)
FROM base AS test
COPY . .
# You can install dev-dependencies here if you have a separate requirements-dev.txt
# RUN pip install pytest-cov
CMD ["pytest", "--verbose", "tests/"]

# STAGE 3: Production
# This stage is optimized for runtime
FROM base AS production
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
91 changes: 68 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,76 @@
# Instructions
# Server Inventory Manager

You are developing an inventory management software solution for a cloud services company that provisions servers in multiple data centers. You must build a CRUD app for tracking the state of all the servers.
A CRUD application to manage server inventory.

Deliverables:
- PR to https://github.com/Mathpix/hiring-challenge-devops-python that includes:
- API code
- CLI code
- pytest test suite
- Working Docker Compose stack
## Specs

Short API.md on how to run everything, also a short API and CLI spec
### API
API documentation available at [http://&lt;hostname&gt;:8000/docs](http://<hostname>:8000/docs)
- **POST /servers**: Create
- **GET /servers**: List all
- **GET /servers/{id}**: Details
- **PUT /servers/{id}**: Update
- **DELETE /servers/{id}**: Remove

Required endpoints:
- POST /servers → create a server
- GET /servers → list all servers
- GET /servers/{id} → get one server
- PUT /servers/{id} → update server
- DELETE /servers/{id} → delete server
### CLI
- `list`: Show table of servers
- `create [hostname] [ip] [state] [datacenter](optional) [cpu](optional) [ram_gb](optional)`: Add server
- `update [id] [hostname] [ip] [state] [datacenter](optional) [cpu](optional) [ram_gb](optional)`: Edit server
- `delete [id]`: Remove server

Requirements:
- Use FastAPI or Flask
- Store data in PostgreSQL
- Use raw SQL
## How to Run

Validate that:
- hostname is unique
- IP address looks like an IP
### Option 1: Run Tests Then Deploy API (Interactive)

Run tests on the `inventory_test` database, and if successful, automatically build and start the API:

```bash
./run-tests-and-deploy.sh
```

This script will:
1. Start the database service
2. Run tests against `inventory_test` database
3. Remove the test container on success
4. Build and start the API service

### Option 2: Run Tests Only

```bash
docker-compose up -d db --wait
docker-compose run --rm test
```

### Option 3: Start Full Stack Directly (Without test container cleanup)

```bash
docker-compose up --build
```


## Using the CLI

First, install the dependencies:

```bash
python -m venv .venv
source .venv/bin/activate

pip install -r requirements.txt
```

Then, you can use the CLI to manage servers:

```bash
python -m app.cli list
python -m app.cli create web-01 10.0.0.5 active
```

## Future Improvements

- Add extra columns
- Add IP address unique check
- Add ability to update one field independently
- Deploy separate binary for CLI

State is one of: active, offline, retired

Empty file added app/__init__.py
Empty file.
81 changes: 81 additions & 0 deletions app/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import typer
import requests
import json
from rich.console import Console
from rich.table import Table

app = typer.Typer()
console = Console()
API_URL = "http://localhost:8000/servers"

@app.command()
def list():
"""List all servers."""
try:
response = requests.get(API_URL)
response.raise_for_status()
servers = response.json()

table = Table(title="Server Inventory")
table.add_column("ID", style="cyan")
table.add_column("Hostname", style="magenta")
table.add_column("IP Address", style="green")
table.add_column("State", style="yellow")
table.add_column("Datacenter", style="blue")
table.add_column("CPU Cores", style="purple")
table.add_column("RAM GB", style="red")

for s in servers:
table.add_row(
str(s['id']), s['hostname'], s['ip_address'],
s['state'], s['datacenter'] or '',
str(s['cpu_cores']) if s['cpu_cores'] else '',
str(s['ram_gb']) if s['ram_gb'] else ''
)

console.print(table)
except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}")

@app.command()
def create(hostname: str, ip: str, state: str, datacenter: str, cpu_cores: int, ram_gb: int):
"""Create a new server. State must be: active, offline, retired"""
payload = {"hostname": hostname, "ip_address": ip, "state": state, "datacenter": datacenter,
"cpu_cores": cpu_cores, "ram_gb": ram_gb}
response = requests.post(API_URL, json=payload)
if response.status_code == 201:
console.print(f"[bold green]Created:[/bold green] {response.json()}")
else:
console.print(f"[bold red]Failed:[/bold red] {response.text}")

@app.command()
def get(id: int):
"""Get server details by ID."""
response = requests.get(f"{API_URL}/{id}")
if response.status_code == 200:
console.print(response.json())
else:
console.print(f"[bold red]Error:[/bold red] {response.text}")

@app.command()
def update(id: int, hostname: str, ip: str, state: str, datacenter: str, cpu_cores: int, ram_gb: int):
"""Update a server."""
payload = {"hostname": hostname, "ip_address": ip, "state": state, "datacenter": datacenter,
"cpu_cores": cpu_cores, "ram_gb": ram_gb}
response = requests.put(f"{API_URL}/{id}", json=payload)
if response.status_code == 200:
console.print(f"[bold green]Updated:[/bold green] {response.json()}")
else:
console.print(f"[bold red]Failed:[/bold red] {response.text}")

@app.command()
def delete(id: int):
"""Delete a server."""
response = requests.delete(f"{API_URL}/{id}")
if response.status_code == 204:
console.print(f"[bold green]Server {id} deleted.[/bold green]")
else:
console.print(f"[bold red]Failed:[/bold red] {response.text}")

if __name__ == "__main__":
app()
Loading