Skip to content

Commit

Permalink
Automatic addition of database before connecting to it (#4316)
Browse files Browse the repository at this point in the history
* added a function that creates a database before addproduct() function connects to it

* renamed function to follow convention of naming

* retrigger checks

* removed unnecessary test case

* test commit

* changed try catch block

* working implementation

* commit to check if test cases will work

* fixed sqlite error

* fixed tests failing

* changed behaviour of add_test_package_product to work with new fucntionality of add_product

* possible fix of psql_psycopq2

* removed changed to creation of test package product

* probable fix

* final working implementation

* removed automatic prevention of previous jobs, as it was already implemented by other pr

* comments applied

* probable fix multiple server database prohibition problem

* fix of failing test
  • Loading branch information
feyruzb authored Nov 29, 2024
1 parent a3ede93 commit 7aa78ab
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 22 deletions.
67 changes: 67 additions & 0 deletions web/server/codechecker_server/api/product_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

from sqlalchemy.sql.expression import and_

from sqlalchemy import create_engine, exc
from sqlalchemy.engine.url import URL

import codechecker_api_shared
from codechecker_api.ProductManagement_v6 import ttypes

Expand Down Expand Up @@ -318,6 +321,57 @@ def getProductConfiguration(self, product_id):

return prod

@timeit
def __add_product_support(self, product):
"""
Creates a database for the given product,
to assist addProduct() function that connects to
an already existing database.
"""

product_info = product.connection
if product_info.engine == 'sqlite':
LOG.info("Using SQLite engine, skipping database creation")
return True

db_host = product_info.host
db_engine = product_info.engine
db_port = int(product_info.port)
db_user = convert.from_b64(product_info.username_b64)
db_pass = convert.from_b64(product_info.password_b64)
db_name = product_info.database

engine_url = URL(
drivername=db_engine,
username=db_user,
password=db_pass,
host=db_host,
port=db_port,
database='postgres'
)
engine = create_engine(engine_url)
try:
with engine.connect() as conn:
conn.execute("commit")
LOG.info("Creating database '%s'", db_name)
conn.execute(f"CREATE DATABASE {db_name}")
conn.close()
except exc.ProgrammingError as e:
LOG.error("ProgrammingError occurred: %s", str(e))
if "already exists" in str(e):
LOG.error("Database '%s' already exists", db_name)
return False
else:
LOG.error("Error occurred while creating database: %s", str(e))
return False
except exc.SQLAlchemyError as e:
LOG.error("SQLAlchemyError occurred: %s", str(e))
return False
finally:
engine.dispose()

return True

@timeit
def addProduct(self, product):
"""
Expand Down Expand Up @@ -352,6 +406,19 @@ def addProduct(self, product):
codechecker_api_shared.ttypes.ErrorCode.GENERAL,
msg)

# Check if the database is already in use by another product.
db_in_use = self.__server.is_database_used(product)
if db_in_use:
LOG.error("Database '%s' is already in use by another product!",
product.connection.database)
raise codechecker_api_shared.ttypes.RequestFailed(
codechecker_api_shared.ttypes.ErrorCode.DATABASE,
"Database is already in use by another product!")

# Add database before letting product connect to it
if self.__add_product_support(product):
LOG.info("Database support added successfully.")

# Some values come encoded as Base64, decode these.
displayed_name = convert.from_b64(product.displayedName_b64) \
if product.displayedName_b64 else product.endpoint
Expand Down
32 changes: 32 additions & 0 deletions web/server/codechecker_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import multiprocess
from sqlalchemy.orm import sessionmaker
from sqlalchemy.engine.url import make_url
from sqlalchemy.sql.expression import func
from thrift.protocol import TJSONProtocol
from thrift.transport import TTransport
Expand Down Expand Up @@ -753,6 +754,9 @@ def __init__(self,
permissions.initialise_defaults('SYSTEM', {
'config_db_session': cfg_sess
})

self.cfg_sess_private = cfg_sess

products = cfg_sess.query(ORMProduct).all()
for product in products:
self.add_product(product)
Expand Down Expand Up @@ -896,6 +900,34 @@ def add_product(self, orm_product, init_db=False):

self.__products[prod.endpoint] = prod

def is_database_used(self, conn):
"""
Returns bool whether the given database is already connected to by
the server.
"""

# get the database name from the database connection args
conn = make_url(conn.connection)
is_sqlite = conn.engine == 'sqlite'

# create a tuple of database that is going to be added for comparison
to_add = (f"{conn.engine}+pysqlite" if is_sqlite
else f"{conn.engine}+psycopg2",
conn.database, conn.host, conn.port)

# dynamic_list contains the currently connected databases to servers
dynamic_list = [(make_url(a.connection).drivername,
make_url(a.connection).database,
make_url(a.connection).host,
make_url(a.connection).port)
for a in self.cfg_sess_private.query(ORMProduct).all()]

self.cfg_sess_private.commit()
self.cfg_sess_private.close()

# True if found, False otherwise
return to_add in dynamic_list

@property
def num_products(self):
"""
Expand Down
10 changes: 3 additions & 7 deletions web/server/vue-cli/src/views/NewFeatures.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
<!-- eslint-disable max-len -->
<v-container fluid>
<v-timeline align-top>


<v-timeline-item fill-dot icon="mdi-star">
<new-release-item>
<template v-slot:title>
Expand Down Expand Up @@ -42,20 +40,18 @@
<template v-slot:title>
New Static HTML Report Pages
</template>
The static HTML generation is rewritten so it can handle much larger result set.
The static HTML generation is rewritten so it can handle much larger result set.
</new-feature-item>

<new-feature-item>
<template v-slot:title>
New report filter to list closed and outstanding reports
</template>
A new filter has been added to list outstanding and closed reports. An outstanding report is a report with detection status new, reopened, unresolved with review status unreviewed or confirmed.
A new filter has been added to list outstanding and closed reports. An outstanding report is a report with detection status new, reopened, unresolved with review status unreviewed or confirmed.
</new-feature-item>

</new-release-item>
</v-timeline-item>


<v-timeline-item fill-dot icon="mdi-star" color="green lighten-1">
<new-release-item color="green lighten-1">
<template v-slot:title>
Expand Down Expand Up @@ -2443,4 +2439,4 @@ export default {
NewReleaseItem
}
};
</script>
</script>
15 changes: 9 additions & 6 deletions web/tests/functional/products/test_config_db_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import unittest

from codechecker_api_shared.ttypes import Permission
from codechecker_api_shared.ttypes import RequestFailed

from codechecker_api.ProductManagement_v6.ttypes import ProductConfiguration
from codechecker_api.ProductManagement_v6.ttypes import DatabaseConnection
Expand Down Expand Up @@ -154,8 +155,8 @@ def create_test_product(product_name, product_endpoint):
description_b64=name,
connection=DatabaseConnection(
engine='sqlite',
host='',
port=0,
host=None,
port=None,
username_b64='',
password_b64='',
database=os.path.join(self.test_workspace_secondary,
Expand All @@ -169,8 +170,10 @@ def create_test_product(product_name, product_endpoint):

product_cfg = create_test_product('producttest_second 2',
'producttest_second_2')
self.assertTrue(self._pr_client_2.addProduct(product_cfg),
"Cannot create product on secondary server.")

# expect request to fail, cannot connect 2 products to 1 database
with self.assertRaises(RequestFailed):
self._pr_client_2.addProduct(product_cfg)

# Product name full string match.
products = self._pr_client_2.getProducts('producttest_second', None)
Expand All @@ -182,10 +185,10 @@ def create_test_product(product_name, product_endpoint):

# Product name substring match.
products = self._pr_client_2.getProducts('producttest_second*', None)
self.assertEqual(len(products), 2)
self.assertEqual(len(products), 1)

products = self._pr_client_2.getProducts(None, 'producttest_second*')
self.assertEqual(len(products), 2)
self.assertEqual(len(products), 1)

# Use the same CodeChecker config that was used on the main server,
# but store into the secondary one.
Expand Down
10 changes: 5 additions & 5 deletions web/tests/functional/products/test_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"""


from copy import deepcopy
# from copy import deepcopy
import os
import unittest

Expand Down Expand Up @@ -93,10 +93,10 @@ def test_add_invalid_product(self):

# Test setting up product with valid endpoint but no database
# connection.
with self.assertRaises(RequestFailed):
cfg = deepcopy(product_cfg)
cfg.endpoint = "valid"
self._root_client.addProduct(cfg)
# with self.assertRaises(RequestFailed):
# cfg = deepcopy(product_cfg)
# cfg.endpoint = "valid"
# self._root_client.addProduct(cfg)

# Test some invalid strings based on pattern.
dbc = DatabaseConnection(
Expand Down
6 changes: 4 additions & 2 deletions web/tests/functional/store/test_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ def setup_class(self):

server_access['viewer_product'] = 'store_limited_product'
codechecker.add_test_package_product(server_access, TEST_WORKSPACE,
report_limit=2)
report_limit=2,
database_name='store_limited')

server_access['viewer_product'] = 'store_test'
codechecker.add_test_package_product(server_access, TEST_WORKSPACE)
codechecker.add_test_package_product(server_access, TEST_WORKSPACE,
database_name='store_test')

# Extend the checker configuration with the server access.
codechecker_cfg.update(server_access)
Expand Down
5 changes: 3 additions & 2 deletions web/tests/libtest/codechecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,8 @@ def start_server_proc(event, server_cmd, checking_env):

def add_test_package_product(server_data, test_folder, check_env=None,
protocol='http', report_limit=None,
user_permissions=None):
user_permissions=None,
database_name="data.sqlite"):
"""
Add a product for a test suite to the server provided by server_data.
Server must be running before called.
Expand Down Expand Up @@ -781,7 +782,7 @@ def add_test_package_product(server_data, test_folder, check_env=None,
else:
# SQLite databases are put under the workspace of the appropriate test.
add_command += ['--sqlite',
os.path.join(test_folder, 'data.sqlite')]
os.path.join(test_folder, database_name)]

print(' '.join(add_command))

Expand Down

0 comments on commit 7aa78ab

Please sign in to comment.