2
# Copyright 2014 Canonical Ltd.
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License version 3, as
6
# published by the Free Software Foundation.
8
# This program is distributed in the hope that it will be useful, but
9
# WITHOUT ANY WARRANTY; without even the implied warranties of
10
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11
# PURPOSE. See the GNU Affero General Public License for more details.
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
"""Functions that access the database."""
24
logger = logging.getLogger(__name__)
29
def get_settings_path():
30
return os.path.abspath(
32
os.path.dirname(__file__),
40
@functools.lru_cache()
41
def get_db_settings(settings_path):
42
"""Return a dictionary containing the database settings, as provided by
45
If the settings file does not exist an IOError is returned.
46
If the settings file exists, but is not valid JSON, a ValueError is
51
with open(settings_path, 'r') as settings_file:
52
return json.load(settings_file)
55
def translate_juju_db_settings(db_settings):
56
"""Given a dictionary of juju database settings, return a dictionary of
57
psycopg2 database connection settings.
61
database=db_settings['database'],
62
user=db_settings['schema_user'],
63
password=db_settings['schema_password'],
64
host=db_settings['host'],
65
port=db_settings['port'],
69
def get_connection_for_request(request=None):
70
"""Get a database connection.
72
This is bound to every request object we recieve as 'request.database'.
74
Note: If this argument is not called with the 'request' parameter then the
75
caller *must* call '.close()' on the returned connection object.
78
juju_db_settings = get_db_settings(get_settings_path())
79
db_kwargs = translate_juju_db_settings(juju_db_settings)
80
connection = psycopg2.connect(**db_kwargs)
81
if request is not None:
84
request.add_finished_callback(cleanup)
88
@contextlib.contextmanager
89
def get_scoped_connection():
90
"""Get a database connection.
92
This context manager will return a database connection, and will
93
automatically close the connection when the context manager exits.
97
with nfss.database.get_scoped_connection() as db:
99
cursor.execute('SELECT 1+1;')
103
connection = get_connection_for_request()
111
"""A simple utility decorator that wraps a method that gets content from
112
the database. Using this will ensure that:
114
1) If the wrapped function returns without raising an exception, the
115
connection's 'commit()' method is called.
116
2) If the wrapped function raises an exception, the connection's
117
'rollback()' method will be called.
119
This is important since by default, any operation (including simple
120
SELECT queries) starts a new transaction. Without calling 'commit' or
121
'rollback' these transactions gradually use up more and more resources.
124
def worker(connection, *args, **kwargs):
126
result = fn(connection, *args, **kwargs)
130
connection.rollback()
136
def get_details_for_all_projects(connection):
137
"""Get high level details for all projects in the system.
139
The return value will be a list of lists. Each inner list will contain:
141
[project_name, test_array, max_date]
143
* project_name is obvious - it's the name of the project.
144
* test_array is a json formatted array of test names that have had data
145
entered for this project.
146
* max_date is the date of the last data point for this project in any test.
149
cursor = connection.cursor()
153
json_agg(distinct test.name),
154
max(data.date_entered)
156
INNER JOIN test ON project.id = test.project_id
157
INNER JOIN data ON test.id = data.test_id
158
GROUP BY project.name;
160
return cursor.fetchall()
164
def get_details_for_project(connection, project_name):
165
"""Get high-level details for a single project.
167
The return value will be a json string containing the test name, first and
168
last data point dates, and a count of data points, in an array, for each
172
cursor = connection.cursor()
176
test.name as test_name,
177
min(data.date_entered) as first_data,
178
max(data.date_entered) as last_data,
179
count(data.id) as data_points
181
INNER JOIN data on data.test_id = test.id
182
INNER JOIN project on test.project_id = project.id
183
WHERE project.name = %s
191
first_data=row[1].isoformat(),
192
last_data=row[2].isoformat(),
199
def get_details_for_test(connection, project_name, test_name, start_time=None,
201
"""Get data points for a particular project / test combo.
203
If start_time is specified, end time must also be specified. If either are
204
specified, they must be datetime objects. If neither are specified, then
205
the data range will be 30 days from the last data point.
207
This functions returns a tuple that contins, in order:
209
* A json string containing the test data points.
210
* A datetime object representing the point at which the returned data
212
* A datetime object representing the point at which the returned data
214
* A datetime object representing the first data point in the test data.
215
* A datetime object represending the last data point in the test data.
218
cursor = connection.cursor()
220
if all((start_time, end_time)):
224
min(data.date_entered),
225
max(data.date_entered)
227
INNER JOIN test on data.test_id = test.id
228
INNER JOIN project on test.project_id = project.id
229
WHERE project.name = %s
232
(project_name, test_name)
234
first, last = cursor.fetchone()
240
data.date_entered AS date_entered,
244
INNER JOIN test on data.test_id = test.id
245
INNER JOIN project on test.project_id = project.id
246
WHERE project.name = %s
248
AND data.date_entered >= %s
249
AND data.date_entered <= %s
252
(project_name, test_name, start_time, end_time)
255
cursor.fetchone()[0],
262
# user didn't specify dates.
266
min(data.date_entered),
267
max(data.date_entered) - interval '30 days',
268
max(data.date_entered)
270
INNER JOIN test on data.test_id = test.id
271
INNER JOIN project on test.project_id = project.id
272
WHERE project.name = %s
275
(project_name, test_name)
277
first, data_first, last = cursor.fetchone()
283
data.date_entered AS date_entered,
287
INNER JOIN test on data.test_id = test.id
288
INNER JOIN project on test.project_id = project.id
289
WHERE project.name = %s
291
AND data.date_entered >= %s
292
AND data.date_entered <= %s
295
(project_name, test_name, data_first, last)
298
cursor.fetchone()[0],
307
def insert_test_data(connection, project_name, test_name, data, client_key):
308
# TODO; also link client that inserted it...
309
project_id = _maybe_create_project(connection, project_name)
310
test_id = _maybe_create_test(connection, project_id, test_name)
311
cursor = connection.cursor()
313
"INSERT INTO data (date_entered, data, test_id, client_id) "
314
"VALUES ('now', %s, %s, (SELECT id FROM client where access_key = %s))"
316
(data, test_id, client_key)
318
return cursor.fetchone()[0]
321
def _maybe_create_project(connection, project_name):
322
"""If the project exists, return it's id. If it doesn't, create it and
326
cursor = connection.cursor()
327
cursor.execute('SELECT id FROM project WHERE name = %s;', (project_name,))
328
if cursor.rowcount > 0:
329
return cursor.fetchone()[0]
331
'INSERT INTO project (name) VALUES (%s) RETURNING id;',
334
return cursor.fetchone()[0]
337
def _maybe_create_test(connection, project_id, test_name):
338
cursor = connection.cursor()
340
'SELECT id FROM test WHERE name = %s AND project_id = %s;',
341
(test_name, project_id)
343
if cursor.rowcount > 0:
344
return cursor.fetchone()[0]
346
'INSERT INTO test (name, project_id) VALUES (%s, %s) RETURNING id;',
347
(test_name, project_id)
349
return cursor.fetchone()[0]
353
def get_auth_client_key_exists(connection, client_key):
354
cursor = connection.cursor()
356
'SELECT id FROM client WHERE access_key = %s AND valid = true;',
359
return cursor.rowcount != 0
363
def get_auth_client_key_list(connection):
364
"""Get a list of valid client keys."""
365
cursor = connection.cursor()
366
cursor.execute('SELECT access_key FROM client WHERE valid = true;')
367
return [r[0] for r in cursor.fetchall()]
371
def invalidate_client_key(connection, client_key):
372
cursor = connection.cursor()
374
'UPDATE client SET valid = false WHERE access_key = %s;',
380
def get_auth_resource_owner_key_for_client_key(connection, client_key):
381
cursor = connection.cursor()
383
'SELECT resource_owner_key FROM client WHERE access_key = %s;',
386
return cursor.fetchone()[0]
390
def auth_add_client_details(connection, name, description, poc, access_key,
391
owner_key, owner_secret):
392
cursor = connection.cursor()
395
INSERT INTO client (access_key, name, description, point_of_contact,
396
resource_owner_key, resource_owner_secret)
397
VALUES (%s, %s, %s, %s, %s, %s);
399
(access_key, name, description, poc, owner_key, owner_secret)
404
def get_auth_resource_owner_secret_for_client_key(connection, client_key):
405
cursor = connection.cursor()
407
'SELECT resource_owner_secret FROM client where access_key = %s;',
410
return cursor.fetchone()[0]
414
def get_auth_nonce_already_used(conn, client_key, timestamp, nonce, owner_key):
415
cursor = conn.cursor()
419
WHERE access_key = %s
420
AND timestamp = to_timestamp(%s)
422
AND resource_owner_key = %s;
424
(client_key, timestamp, nonce, owner_key)
426
return cursor.rowcount != 0
430
def store_nonce(connection, client_key, timestamp, nonce, owner_key):
431
cursor = connection.cursor()
434
INSERT INTO nonce (access_key, timestamp, nonce, resource_owner_key)
435
VALUES (%s, to_timestamp(%s), %s, %s);
437
(client_key, timestamp, nonce, owner_key)
443
def clean_old_nonces(connection):
444
cursor = connection.cursor()
449
CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - CAST('1 day' AS interval);