Commit 562d3b51 authored by Taavi Meinberg's avatar Taavi Meinberg
Browse files

Pull request #5: Develop

Merge in XTIIM/csapi from develop to release

* commit 'e6302b2b':
  Updating requirements
  Updating readme with status API info
  Updating nginx conf
  Adding status API
  Work in progress
  README.md edited online with Bitbucket
  README.md edited online with Bitbucket
parents da842909 e6302b2b
......@@ -18,9 +18,11 @@ Installation was tested with Ubuntu 18.04.
Provided systemd and nginx configurations assume than program files are installed under `/opt/csapi`. Program is running under `xroad` user to be able to access X-Road configuration files and database without any additional configurations.
Create `/opt/csapi` directory:
Create `/opt/csapi` and `/opt/csapi/socket` directories:
```bash
sudo mkdir -p /opt/csapi
sudo mkdir -p /opt/csapi/socket
sudo chown xroad /opt/csapi/socket/
```
And copy files `csapi.py`, `server.py`, and `requirements.txt` into `/opt/csapi` directory.
......@@ -103,6 +105,12 @@ curl --cert client.crt --key client.key --cacert csapi.crt -i -d '{"member_class
Note that you can allow multiple clients (or nodes) by creating certificate bundle. That can be done by concatenating multiple client certificates into single `client.crt` file.
### API Status
API Status is available on `/status` endpoint. You can test that with curl:
```bash
curl -k https://central-server.domain.local:5443/status
```
## Testing
Note that `server.py` is a configuration file for logging and Flask and therefore not covered by tests.
......
......@@ -383,6 +383,27 @@ def incorrect_client(client_dn):
'msg': 'Client certificate is not allowed: {}'.format(client_dn)})
def test_db():
"""Add new X-Road subsystem to Central Server"""
conf = get_db_conf()
if not conf['username'] or not conf['password'] or not conf['database']:
LOGGER.error('DB_CONF_ERROR: Cannot access database configuration')
return {
'http_status': 500, 'code': 'DB_CONF_ERROR',
'msg': 'Cannot access database configuration'}
with get_db_connection(conf) as conn:
with conn.cursor() as cur:
cur.execute("""select 1 from system_parameters where key='instanceIdentifier'""")
rec = cur.fetchone()
if rec:
return {
'http_status': 200, 'code': 'OK',
'msg': 'API is ready'}
return {'http_status': 500, 'code': 'DB_ERROR', 'msg': 'Unexpected DB state'}
class MemberApi(Resource):
"""Member API class for Flask"""
def __init__(self, config):
......@@ -459,3 +480,24 @@ class SubsystemApi(Resource):
'msg': 'Unclassified database error'}
return make_response(response)
class StatusApi(Resource):
"""Status API class for Flask"""
def __init__(self, config):
self.config = config
@staticmethod
def get():
"""GET method"""
LOGGER.info('Incoming status request')
try:
response = test_db()
except psycopg2.Error as err:
LOGGER.error('DB_ERROR: Unclassified database error: %s', err)
response = {
'http_status': 500, 'code': 'DB_ERROR',
'msg': 'Unclassified database error'}
return make_response(response)
......@@ -14,6 +14,11 @@ server {
# who fail authentication
ssl_verify_client optional;
location /status {
# No auth required for status
proxy_pass http://unix:/opt/csapi/socket/csapi.sock;
}
location / {
# Require authentication!!!
if ($ssl_client_verify != SUCCESS) {
......
psycopg2-binary==2.8.2
Flask==1.0.3
Flask-RESTful==0.3.7
gunicorn==19.9.0
psycopg2-binary==2.8.5
Flask==1.1.2
Flask-RESTful==0.3.8
gunicorn==20.0.4
coverage
pylint
......@@ -3,7 +3,7 @@
import logging
from flask import Flask
from flask_restful import Api
from csapi import MemberApi, SubsystemApi, load_config
from csapi import MemberApi, SubsystemApi, StatusApi, load_config
handler = logging.FileHandler('/var/log/xroad/csapi.log')
handler.setFormatter(logging.Formatter('%(asctime)s - %(process)d - %(levelname)s: %(message)s'))
......@@ -24,5 +24,6 @@ app = Flask(__name__)
api = Api(app)
api.add_resource(MemberApi, '/member', resource_class_kwargs={'config': config})
api.add_resource(SubsystemApi, '/subsystem', resource_class_kwargs={'config': config})
api.add_resource(StatusApi, '/status', resource_class_kwargs={'config': config})
logger.info('Starting Central Server API')
......@@ -17,6 +17,8 @@ class MainTestCase(unittest.TestCase):
'config': {'allow_all': True}})
self.api.add_resource(csapi.SubsystemApi, '/subsystem', resource_class_kwargs={
'config': {'allow_all': True}})
self.api.add_resource(csapi.StatusApi, '/status', resource_class_kwargs={
'config': {'allow_all': True}})
@patch('builtins.open', return_value=io.StringIO('''adapter=postgresql
encoding=utf8
......@@ -858,6 +860,125 @@ reconnect=true
'member_class': 'MEMBER_CLASS', 'member_code': 'MEMBER_CODE',
'subsystem_code': 'SUBSYSTEM_CODE'})
@patch('csapi.get_db_connection')
@patch('csapi.get_db_conf', return_value={
'database': 'centerui_production',
'password': 'centerui_pass',
'username': 'centerui_user'})
def test_test_db_ok(self, mock_get_db_conf, mock_get_db_connection):
mock_get_db_connection.execute = MagicMock()
mock_get_db_connection.fetchone = MagicMock(return_value=1)
self.assertEqual(
{'code': 'OK', 'http_status': 200, 'msg': 'API is ready'},
csapi.test_db())
mock_get_db_conf.assert_called_with()
mock_get_db_connection.assert_called_with({
'database': 'centerui_production', 'password': 'centerui_pass',
'username': 'centerui_user'})
@patch('csapi.get_db_connection')
@patch('csapi.get_db_conf', return_value={
'database': 'centerui_production',
'password': 'centerui_pass',
'username': 'centerui_user'})
def test_test_db_not_ok(self, mock_get_db_conf, mock_get_db_connection):
mock_cur = mock_get_db_connection.return_value.__enter__.return_value.cursor.return_value
mock_cur.__enter__.return_value.fetchone.return_value = None
self.assertEqual(
{'code': 'DB_ERROR', 'http_status': 500, 'msg': 'Unexpected DB state'},
csapi.test_db())
mock_get_db_conf.assert_called_with()
mock_get_db_connection.assert_called_with({
'database': 'centerui_production', 'password': 'centerui_pass',
'username': 'centerui_user'})
@patch('csapi.get_db_connection')
@patch('csapi.get_db_conf', return_value={
'database': '',
'password': 'centerui_pass',
'username': 'centerui_user'})
def test_test_db_no_database(self, mock_get_db_conf, mock_get_db_connection):
with self.assertLogs(csapi.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'DB_CONF_ERROR', 'http_status': 500,
'msg': 'Cannot access database configuration'},
csapi.test_db())
self.assertEqual(
['ERROR:csapi:DB_CONF_ERROR: Cannot access database configuration'], cm.output)
mock_get_db_conf.assert_called_with()
mock_get_db_connection.assert_not_called()
@patch('csapi.get_db_connection')
@patch('csapi.get_db_conf', return_value={
'database': 'centerui_production',
'password': '',
'username': 'centerui_user'})
def test_test_db_no_password(self, mock_get_db_conf, mock_get_db_connection):
with self.assertLogs(csapi.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'DB_CONF_ERROR', 'http_status': 500,
'msg': 'Cannot access database configuration'},
csapi.test_db())
self.assertEqual(
['ERROR:csapi:DB_CONF_ERROR: Cannot access database configuration'], cm.output)
mock_get_db_conf.assert_called_with()
mock_get_db_connection.assert_not_called()
@patch('csapi.get_db_connection')
@patch('csapi.get_db_conf', return_value={
'database': 'centerui_production',
'password': 'centerui_pass',
'username': ''})
def test_test_db_no_username(self, mock_get_db_conf, mock_get_db_connection):
with self.assertLogs(csapi.LOGGER, level='INFO') as cm:
self.assertEqual(
{
'code': 'DB_CONF_ERROR', 'http_status': 500,
'msg': 'Cannot access database configuration'},
csapi.test_db())
self.assertEqual(
['ERROR:csapi:DB_CONF_ERROR: Cannot access database configuration'], cm.output)
mock_get_db_conf.assert_called_with()
mock_get_db_connection.assert_not_called()
@patch('csapi.test_db', side_effect=psycopg2.Error('DB_ERROR_MSG'))
def test_status_db_error_handled(self, mock_test_db):
with self.app.app_context():
with self.assertLogs(csapi.LOGGER, level='INFO') as cm:
response = self.client.get('/status')
self.assertEqual(500, response.status_code)
self.assertEqual(
jsonify({
'code': 'DB_ERROR',
'msg': 'Unclassified database error'}).json,
response.json
)
self.assertEqual([
'INFO:csapi:Incoming status request',
'ERROR:csapi:DB_ERROR: Unclassified database error: DB_ERROR_MSG',
"INFO:csapi:Response: {'http_status': 500, 'code': 'DB_ERROR', 'msg': "
"'Unclassified database error'}"], cm.output)
mock_test_db.assert_called_with()
@patch('csapi.test_db', return_value={
'http_status': 200, 'code': 'OK', 'msg': 'All Correct'})
def test_status_ok(self, mock_test_db):
with self.app.app_context():
with self.assertLogs(csapi.LOGGER, level='INFO') as cm:
response = self.client.get('/status')
self.assertEqual(200, response.status_code)
self.assertEqual(
jsonify({'code': 'OK', 'msg': 'All Correct'}).json,
response.json
)
self.assertEqual([
'INFO:csapi:Incoming status request',
"INFO:csapi:Response: {'http_status': 200, 'code': 'OK', 'msg': 'All "
"Correct'}"], cm.output)
mock_test_db.assert_called_with()
class NoConfTestCase(unittest.TestCase):
def setUp(self):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment