server alive
This commit is contained in:
parent
1c17b57d8a
commit
fab0e979d5
11 changed files with 353 additions and 28 deletions
86
DB_CONNECTION_FIX.md
Normal file
86
DB_CONNECTION_FIX.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
# PostgreSQL Authentication Fix
|
||||
|
||||
## Issue Description
|
||||
|
||||
The server was failing to connect to the PostgreSQL database with the following error:
|
||||
|
||||
```
|
||||
[OEDB] [ERROR] Failed to connect to PostgreSQL database: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "cipherbliss"
|
||||
```
|
||||
|
||||
This error occurs because the application was trying to connect to PostgreSQL using Unix socket connections, which default to "peer" authentication (using the operating system username). However, the database was configured to use password authentication, as evidenced by the fact that DBeaver could connect using the password from the `.env` file.
|
||||
|
||||
## Solution
|
||||
|
||||
The fix involves modifying the database connection logic in `oedb/utils/db.py` to:
|
||||
|
||||
1. Force TCP/IP connections instead of Unix socket connections by setting the host to "localhost" when it's empty
|
||||
2. Add appropriate connection parameters for local connections
|
||||
3. Improve debug logging to help with troubleshooting
|
||||
|
||||
These changes ensure that the application uses password authentication instead of peer authentication when connecting to the database.
|
||||
|
||||
## Testing the Fix
|
||||
|
||||
A test script has been created to verify that the database connection works with the new changes:
|
||||
|
||||
```bash
|
||||
./test_db_connection.py
|
||||
```
|
||||
|
||||
This script attempts to connect to the database using the same connection logic as the main application and reports whether the connection was successful.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Changes Made
|
||||
|
||||
1. Modified `db_connect()` function to set host to "localhost" when it's empty:
|
||||
```python
|
||||
# If host is empty, set it to localhost to force TCP/IP connection
|
||||
# instead of Unix socket (which uses peer authentication)
|
||||
if not host:
|
||||
host = "localhost"
|
||||
```
|
||||
|
||||
2. Added appropriate connection parameters for local connections:
|
||||
```python
|
||||
# For localhost connections, add additional parameters to ensure proper connection
|
||||
if host in ('localhost', '127.0.0.1'):
|
||||
logger.debug("Using TCP/IP connection with additional parameters")
|
||||
return psycopg2.connect(
|
||||
dbname=dbname,
|
||||
host=host,
|
||||
password=password,
|
||||
user=user,
|
||||
options="-c client_encoding=utf8 -c statement_timeout=3000",
|
||||
connect_timeout=3,
|
||||
application_name="oedb",
|
||||
# Disable SSL for local connections
|
||||
sslmode='disable')
|
||||
```
|
||||
|
||||
3. Improved debug logging to help with troubleshooting:
|
||||
```python
|
||||
logger.debug(f"Connecting to database: {dbname} on {host} as user {user}")
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
By setting the host to "localhost" when it's empty, we force psycopg2 to use TCP/IP connections instead of Unix socket connections. TCP/IP connections typically use password authentication, which is what the database is configured to use.
|
||||
|
||||
The additional connection parameters for local connections help ensure that the connection is established correctly and provide useful information for debugging.
|
||||
|
||||
## Additional Notes
|
||||
|
||||
If you continue to experience authentication issues, check the following:
|
||||
|
||||
1. Make sure your `.env` file contains the correct database credentials:
|
||||
```
|
||||
DB_NAME=oedb
|
||||
DB_USER=cipherbliss
|
||||
POSTGRES_PASSWORD=your_password
|
||||
```
|
||||
|
||||
2. Verify that your PostgreSQL server is configured to allow password authentication for the specified user.
|
||||
|
||||
3. Check the PostgreSQL logs for more detailed error messages.
|
|
@ -20,6 +20,7 @@ from oedb.middleware.headers import HeaderMiddleware
|
|||
from oedb.resources.event import event
|
||||
from oedb.resources.stats import StatsResource
|
||||
from oedb.resources.search import EventSearch
|
||||
from oedb.resources.root import root
|
||||
|
||||
def create_app():
|
||||
"""
|
||||
|
@ -43,10 +44,11 @@ def create_app():
|
|||
|
||||
# Add routes
|
||||
logger.info("Setting up API routes")
|
||||
app.add_route('/', root) # Handle root requests
|
||||
app.add_route('/event/search', event_search) # Handle event search requests
|
||||
app.add_route('/event/{id}', event) # Handle single event requests
|
||||
app.add_route('/event', event) # Handle event collection requests
|
||||
app.add_route('/stats', stats) # Handle stats requests
|
||||
app.add_route('/event/search', event_search) # Handle event search requests
|
||||
|
||||
logger.success("Application initialized successfully")
|
||||
return app
|
||||
|
|
|
@ -7,6 +7,7 @@ import re
|
|||
import falcon
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
import psycopg2.errors
|
||||
from oedb.models.event import BaseEvent
|
||||
from oedb.utils.db import db_connect
|
||||
from oedb.utils.serialization import dumps
|
||||
|
@ -219,7 +220,7 @@ class EventResource(BaseEvent):
|
|||
event_sort=event_sort, limit=limit)
|
||||
logger.debug(f"Executing SQL: {sql}")
|
||||
cur.execute(sql)
|
||||
resp.body = dumps(self.rows_to_collection(cur.fetchall(), geom_only))
|
||||
resp.text = dumps(self.rows_to_collection(cur.fetchall(), geom_only))
|
||||
resp.status = falcon.HTTP_200
|
||||
logger.success(f"Successfully processed GET request to /event")
|
||||
else:
|
||||
|
@ -229,16 +230,24 @@ class EventResource(BaseEvent):
|
|||
|
||||
e = cur.fetchone()
|
||||
if e is not None:
|
||||
resp.body = dumps(self.row_to_feature(e))
|
||||
resp.text = dumps(self.row_to_feature(e))
|
||||
resp.status = falcon.HTTP_200
|
||||
logger.success(f"Successfully processed GET request to /event/{id}")
|
||||
else:
|
||||
resp.status = falcon.HTTP_404
|
||||
logger.warning(f"Event not found: {id}")
|
||||
except psycopg2.errors.InsufficientPrivilege as e:
|
||||
logger.error(f"Permission denied for database table: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.text = dumps({
|
||||
"error": "Database permission error",
|
||||
"message": "The server does not have permission to access the required database tables. Please contact the administrator to fix this issue.",
|
||||
"details": str(e)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing GET request: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.body = dumps({"error": str(e)})
|
||||
resp.text = dumps({"error": str(e)})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
@ -260,29 +269,29 @@ class EventResource(BaseEvent):
|
|||
j = json.loads(body)
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid JSON or bad encoding: {e}")
|
||||
resp.body = 'invalid json or bad encoding'
|
||||
resp.text = 'invalid json or bad encoding'
|
||||
resp.status = falcon.HTTP_400
|
||||
return
|
||||
|
||||
resp.body = ''
|
||||
resp.text = ''
|
||||
if "properties" not in j:
|
||||
resp.body = resp.body + "missing 'properties' elements\n"
|
||||
resp.text = resp.text + "missing 'properties' elements\n"
|
||||
j['properties'] = dict()
|
||||
if "geometry" not in j:
|
||||
resp.body = resp.body + "missing 'geometry' elements\n"
|
||||
resp.text = resp.text + "missing 'geometry' elements\n"
|
||||
j['geometry'] = None
|
||||
if "when" not in j['properties'] and ("start" not in j['properties'] or "stop" not in j['properties']):
|
||||
resp.body = resp.body + "missing 'when' or 'start/stop' in properties\n"
|
||||
resp.text = resp.text + "missing 'when' or 'start/stop' in properties\n"
|
||||
j['properties']['when'] = None
|
||||
if "type" not in j['properties']:
|
||||
resp.body = resp.body + "missing 'type' of event in properties\n"
|
||||
resp.text = resp.text + "missing 'type' of event in properties\n"
|
||||
j['properties']['type'] = None
|
||||
if "what" not in j['properties']:
|
||||
resp.body = resp.body + "missing 'what' in properties\n"
|
||||
resp.text = resp.text + "missing 'what' in properties\n"
|
||||
j['properties']['what'] = None
|
||||
if "type" in j and j['type'] != 'Feature':
|
||||
resp.body = resp.body + 'geojson must be "type":"Feature" only\n'
|
||||
if id is None and resp.body != '':
|
||||
resp.text = resp.text + 'geojson must be "type":"Feature" only\n'
|
||||
if id is None and resp.text != '':
|
||||
resp.status = falcon.HTTP_400
|
||||
resp.set_header('Content-type', 'text/plain')
|
||||
return
|
||||
|
@ -319,7 +328,7 @@ class EventResource(BaseEvent):
|
|||
geometry = dumps(j['geometry'])
|
||||
h = self.maybe_insert_geometry(geometry, cur)
|
||||
if len(h) > 1 and h[1] is False:
|
||||
resp.body = "invalid geometry: %s\n" % h[2]
|
||||
resp.text = "invalid geometry: %s\n" % h[2]
|
||||
resp.status = falcon.HTTP_400
|
||||
resp.set_header('Content-type', 'text/plain')
|
||||
return
|
||||
|
@ -361,11 +370,11 @@ class EventResource(BaseEvent):
|
|||
AND e.events_when=tstzrange(coalesce(%s, lower(s.events_when)),coalesce(%s,upper(s.events_when)),%s) AND e.events_geo=coalesce(%s, s.events_geo);""",
|
||||
(id, j['properties']['what'], event_start, event_stop, bounds, h[0]))
|
||||
dupe = cur.fetchone()
|
||||
resp.body = """{"duplicate":"%s"}""" % (dupe[0])
|
||||
resp.text = """{"duplicate":"%s"}""" % (dupe[0])
|
||||
resp.status = '409 Conflict with event %s' % dupe[0]
|
||||
logger.warning(f"Duplicate event: {dupe[0]}")
|
||||
else:
|
||||
resp.body = """{"id":"%s"}""" % (e[0])
|
||||
resp.text = """{"id":"%s"}""" % (e[0])
|
||||
if id is None:
|
||||
resp.status = falcon.HTTP_201
|
||||
logger.success(f"Event created with ID: {e[0]}")
|
||||
|
@ -455,7 +464,7 @@ class EventResource(BaseEvent):
|
|||
except Exception as e:
|
||||
logger.error(f"Error deleting event {id}: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.body = dumps({"error": str(e)})
|
||||
resp.text = dumps({"error": str(e)})
|
||||
db.rollback()
|
||||
finally:
|
||||
cur.close()
|
||||
|
|
49
oedb/resources/root.py
Normal file
49
oedb/resources/root.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
Root resource for the OpenEventDatabase.
|
||||
"""
|
||||
|
||||
import falcon
|
||||
from oedb.utils.serialization import dumps
|
||||
from oedb.utils.logging import logger
|
||||
|
||||
class RootResource:
|
||||
"""
|
||||
Resource for the root endpoint.
|
||||
Handles the / endpoint.
|
||||
"""
|
||||
|
||||
def on_get(self, req, resp):
|
||||
"""
|
||||
Handle GET requests to the / endpoint.
|
||||
Returns a JSON response with available routes and a welcome message.
|
||||
|
||||
Args:
|
||||
req: The request object.
|
||||
resp: The response object.
|
||||
"""
|
||||
logger.info("Processing GET request to /")
|
||||
|
||||
try:
|
||||
# Create a response with available routes and a welcome message
|
||||
response = {
|
||||
"message": "Welcome to the OpenEventDatabase API!",
|
||||
"available_routes": {
|
||||
"/": "This endpoint - provides information about available routes",
|
||||
"/event": "Get events matching specified criteria",
|
||||
"/event/{id}": "Get a specific event by ID",
|
||||
"/event/search": "Search for events using a GeoJSON geometry",
|
||||
"/stats": "Get statistics about the database"
|
||||
}
|
||||
}
|
||||
|
||||
# Set the response body and status
|
||||
resp.text = dumps(response)
|
||||
resp.status = falcon.HTTP_200
|
||||
logger.success("Successfully processed GET request to /")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing GET request to /: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.text = dumps({"error": str(e)})
|
||||
|
||||
# Create a global instance of RootResource
|
||||
root = RootResource()
|
|
@ -33,7 +33,7 @@ class EventSearch(BaseEvent):
|
|||
if 'geometry' not in j:
|
||||
logger.warning("Request body missing 'geometry' field")
|
||||
resp.status = falcon.HTTP_400
|
||||
resp.body = json.dumps({"error": "Request body must contain a 'geometry' field"})
|
||||
resp.text = json.dumps({"error": "Request body must contain a 'geometry' field"})
|
||||
return
|
||||
|
||||
# Pass the query with the geometry to event.on_get
|
||||
|
@ -44,8 +44,8 @@ class EventSearch(BaseEvent):
|
|||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error decoding JSON: {e}")
|
||||
resp.status = falcon.HTTP_400
|
||||
resp.body = json.dumps({"error": "Invalid JSON in request body"})
|
||||
resp.text = json.dumps({"error": "Invalid JSON in request body"})
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing POST request to /event/search: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.body = json.dumps({"error": str(e)})
|
||||
resp.text = json.dumps({"error": str(e)})
|
|
@ -45,13 +45,13 @@ class StatsResource:
|
|||
cur.execute("SELECT row_to_json(stat) from (SELECT events_what as what, left(max(upper(events_when))::text,19) as last, count(*) as count, array_agg(distinct(regexp_replace(regexp_replace(events_tags ->> 'source','^(http://|https://)',''),'/.*',''))) as source from (select * from events order by lastupdate desc limit 10000) as last group by 1 order by 2 desc) as stat;")
|
||||
recent = cur.fetchall()
|
||||
|
||||
resp.body = dumps(dict(events_count=count, last_updated=last, uptime=uptime, db_uptime=pg_uptime, recent=recent))
|
||||
resp.text = dumps(dict(events_count=count, last_updated=last, uptime=uptime, db_uptime=pg_uptime, recent=recent))
|
||||
resp.status = falcon.HTTP_200
|
||||
logger.success("Successfully processed GET request to /stats")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing GET request to /stats: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.body = dumps({"error": str(e)})
|
||||
resp.text = dumps({"error": str(e)})
|
||||
finally:
|
||||
cur.close()
|
||||
db.close()
|
|
@ -8,6 +8,20 @@ import psycopg2
|
|||
import psycopg2.extras
|
||||
from oedb.utils.logging import logger
|
||||
|
||||
def load_env_from_file():
|
||||
"""
|
||||
Load environment variables from .env file.
|
||||
This ensures that database connection parameters are properly set.
|
||||
"""
|
||||
if os.path.exists('.env'):
|
||||
logger.info("Loading environment variables from .env file...")
|
||||
with open('.env', 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
key, value = line.split('=', 1)
|
||||
os.environ[key] = value
|
||||
|
||||
def db_connect():
|
||||
"""
|
||||
Connect to the PostgreSQL database using environment variables.
|
||||
|
@ -15,11 +29,43 @@ def db_connect():
|
|||
Returns:
|
||||
psycopg2.connection: A connection to the database.
|
||||
"""
|
||||
return psycopg2.connect(
|
||||
dbname=os.getenv("DB_NAME", "oedb"),
|
||||
host=os.getenv("DB_HOST", ""),
|
||||
password=os.getenv("POSTGRES_PASSWORD", None),
|
||||
user=os.getenv("DB_USER", ""))
|
||||
# Load environment variables from .env file
|
||||
load_env_from_file()
|
||||
|
||||
# Get connection parameters from environment variables
|
||||
dbname = os.getenv("DB_NAME", "oedb")
|
||||
host = os.getenv("DB_HOST", "")
|
||||
password = os.getenv("POSTGRES_PASSWORD", None)
|
||||
user = os.getenv("DB_USER", "")
|
||||
|
||||
# If host is empty, set it to localhost to force TCP/IP connection
|
||||
# instead of Unix socket (which uses peer authentication)
|
||||
if not host:
|
||||
host = "localhost"
|
||||
|
||||
logger.debug(f"Connecting to database: {dbname} on {host} as user {user}")
|
||||
|
||||
# For localhost connections, add additional parameters to ensure proper connection
|
||||
if host in ('localhost', '127.0.0.1'):
|
||||
logger.debug("Using TCP/IP connection with additional parameters")
|
||||
return psycopg2.connect(
|
||||
dbname=dbname,
|
||||
host=host,
|
||||
password=password,
|
||||
user=user,
|
||||
options="-c client_encoding=utf8 -c statement_timeout=3000",
|
||||
connect_timeout=3,
|
||||
application_name="oedb",
|
||||
# Disable SSL for local connections
|
||||
sslmode='disable')
|
||||
else:
|
||||
# For remote connections, use standard parameters
|
||||
logger.debug("Using standard connection parameters")
|
||||
return psycopg2.connect(
|
||||
dbname=dbname,
|
||||
host=host,
|
||||
password=password,
|
||||
user=user)
|
||||
|
||||
def check_db_connection():
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
falcon>=3.1.0
|
||||
falcon
|
||||
psycopg2-binary
|
||||
geojson
|
||||
gunicorn
|
||||
|
|
22
run_tests.sh
Executable file
22
run_tests.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
# Script to start the server in the background and run tests
|
||||
|
||||
echo "Starting the server in the background..."
|
||||
python3 backend.py > server.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Give the server a moment to start
|
||||
sleep 2
|
||||
|
||||
echo "Running tests..."
|
||||
python3 test_api.py
|
||||
TEST_RESULT=$?
|
||||
|
||||
echo "Stopping the server (PID: $SERVER_PID)..."
|
||||
kill $SERVER_PID
|
||||
|
||||
# Wait for the server to stop
|
||||
sleep 1
|
||||
|
||||
echo "Test completed with exit code: $TEST_RESULT"
|
||||
exit $TEST_RESULT
|
87
test_api.py
Executable file
87
test_api.py
Executable file
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify API endpoints are accessible.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
|
||||
BASE_URL = "http://127.0.0.1:8080"
|
||||
|
||||
def test_endpoint(endpoint, method="GET", data=None):
|
||||
"""Test an API endpoint and print the result."""
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
print(f"Testing {method} {url}...")
|
||||
|
||||
try:
|
||||
if method == "GET":
|
||||
response = requests.get(url)
|
||||
elif method == "POST":
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url, data=json.dumps(data), headers=headers)
|
||||
|
||||
print(f"Status code: {response.status_code}")
|
||||
if response.status_code < 400:
|
||||
print("Success!")
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
print("-" * 50)
|
||||
return response.status_code < 400
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}")
|
||||
print("-" * 50)
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run tests for all endpoints."""
|
||||
# Wait for server to start
|
||||
print("Waiting for server to start...")
|
||||
max_retries = 5
|
||||
retries = 0
|
||||
|
||||
while retries < max_retries:
|
||||
try:
|
||||
requests.get(f"{BASE_URL}/")
|
||||
print("Server is running!")
|
||||
break
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"Server not ready yet, retrying in 2 seconds... ({retries+1}/{max_retries})")
|
||||
retries += 1
|
||||
time.sleep(2)
|
||||
|
||||
if retries == max_retries:
|
||||
print("Could not connect to server after multiple attempts.")
|
||||
print("Please make sure the server is running on http://127.0.0.1:8080")
|
||||
return 1
|
||||
|
||||
success = True
|
||||
|
||||
# Test root endpoint
|
||||
success = test_endpoint("/") and success
|
||||
|
||||
# Test event endpoint
|
||||
success = test_endpoint("/event") and success
|
||||
|
||||
# Test event/search endpoint with POST
|
||||
search_data = {
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [2.3522, 48.8566] # Paris coordinates
|
||||
}
|
||||
}
|
||||
success = test_endpoint("/event/search", method="POST", data=search_data) and success
|
||||
|
||||
# Test stats endpoint
|
||||
success = test_endpoint("/stats") and success
|
||||
|
||||
if success:
|
||||
print("All tests passed!")
|
||||
return 0
|
||||
else:
|
||||
print("Some tests failed!")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
24
test_db_connection.py
Executable file
24
test_db_connection.py
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify database connection.
|
||||
This script attempts to connect to the PostgreSQL database using the same
|
||||
connection logic as the main application.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from oedb.utils.db import check_db_connection
|
||||
from oedb.utils.logging import logger
|
||||
|
||||
def main():
|
||||
"""Test database connection and print the result."""
|
||||
logger.info("Testing database connection...")
|
||||
|
||||
if check_db_connection():
|
||||
logger.success("Database connection successful!")
|
||||
return 0
|
||||
else:
|
||||
logger.error("Database connection failed. Check your .env file and PostgreSQL configuration.")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Loading…
Add table
Add a link
Reference in a new issue