Compare commits

..

14 Commits

Author SHA1 Message Date
D. Berge
36e7b1fe21 Add database upgrade file 09 2022-02-06 23:26:57 +01:00
D. Berge
e7fa74326d Add README to database upgrades directory 2022-02-06 23:24:24 +01:00
D. Berge
83be83e4bd Check database schema compatibility.
The server will not start unless it satisfies itself that we're
running against a compatible database schema.
2022-02-06 22:52:45 +01:00
D. Berge
81ce6346b9 Add database schema information to package.json.
Used to determine if the actual schema on the database
is compatible with the version of the server we're
attempting to run.
2022-02-06 22:51:25 +01:00
D. Berge
923ff1acea Add more details to package.json 2022-02-06 22:50:44 +01:00
D. Berge
8ec479805a Add version reporting library.
This reports the current server version, from Git by
default.

Also, and of more interest, it reports whether the
current database schema is compatible with the
server code.
2022-02-06 22:48:20 +01:00
D. Berge
f10103d396 Enfore info key access restrictions on the API.
Obviously, those keys can be edited freely at the database
level. This is intended.
2022-02-06 22:40:53 +01:00
D. Berge
774bde7c00 Reserve certain keys on info tables 2022-02-06 22:39:11 +01:00
D. Berge
b4569c14df Update database README.
Document how to create a Dougal database from scratch
and how to update PostgreSQL.
2022-02-06 22:28:21 +01:00
D. Berge
54eea62e4a Fix require path 2022-02-06 14:24:25 +01:00
D. Berge
69c4f2dd9e Merge branch '161-transfer-files-to-asaqc' into 'devel'
Resolve "Transfer files to ASAQC"

Closes #161

See merge request wgp/dougal/software!16
2021-10-09 09:23:54 +00:00
D. Berge
acc829b978 Switch to production URL in ASAQC configuration 2021-10-06 04:16:17 +02:00
D. Berge
ff4913c0a5 Instrument getLineName to monitor probable cause of #165 2021-10-06 02:12:05 +02:00
D. Berge
51452c978a Add ASAQC task to runner 2021-10-04 21:26:13 +02:00
18 changed files with 360 additions and 20 deletions

View File

@@ -114,6 +114,9 @@ run $BINDIR/human_exports_qc.py
print_log "Export sequence data"
run $BINDIR/human_exports_seis.py
print_log "Process ASAQC queue"
run $DOUGAL_ROOT/lib/www/server/queues/asaqc/index.js
rm "$LOCKFILE"
print_info "End run"

View File

@@ -35,7 +35,7 @@ imports:
queues:
asaqc:
request:
url: "https://localhost:3077/vt/v1/api/upload-file-encoded"
url: "https://api.gateway.equinor.com/vt/v1/api/upload-file-encoded"
args:
method: POST
headers:

View File

@@ -19,3 +19,79 @@ Created with:
```bash
SCHEMA_NAME=survey_X EPSG_CODE=XXXXX $DOUGAL_ROOT/sbin/dump_schema.sh
```
## To create a new Dougal database
Ensure that the following packages are installed:
* `postgresql*-postgis-utils`
* `postgresql*-postgis`
* `postgresql*-contrib` # For B-trees
```bash
psql -U postgres <./database-template.sql
```
---
# Upgrading PostgreSQL
The following is based on https://en.opensuse.org/SDB:PostgreSQL#Upgrading_major_PostgreSQL_version
```bash
# The following bash code should be checked and executed
# line for line whenever you do an upgrade. The example
# shows the upgrade process from an original installation
# of version 12 up to version 14.
# install the new server as well as the required postgresql-contrib packages:
zypper in postgresql14-server postgresql14-contrib postgresql12-contrib
# If not yet done, copy the configuration create a new PostgreSQL configuration directory...
mkdir /etc/postgresql
# and copy the original file to this global directory
cd /srv/pgsql/data
for i in pg_hba.conf pg_ident.conf postgresql.conf postgresql.auto.conf ; do cp -a $i /etc/postgresql/$i ; done
# Now create a new data-directory and initialize it for usage with the new server
install -d -m 0700 -o postgres -g postgres /srv/pgsql/data14
cd /srv/pgsql/data14
sudo -u postgres /usr/lib/postgresql14/bin/initdb .
# replace the newly generated files by a symlink to the global files.
# After doing so, you may check the difference of the created backup files and
# the files from the former installation
for i in pg_hba.conf pg_ident.conf postgresql.conf postgresql.auto.conf ; do old $i ; ln -s /etc/postgresql/$i .; done
# Copy over special thesaurus files if some exists.
#cp -a /usr/share/postgresql12/tsearch_data/my_thesaurus_german.ths /usr/share/postgresql14/tsearch_data/
# Now it's time to disable the service...
systemctl stop postgresql.service
# And to start the migration. Please ensure, the directories fit to your upgrade path
sudo -u postgres /usr/lib/postgresql14/bin/pg_upgrade --link \
--old-bindir="/usr/lib/postgresql12/bin" \
--new-bindir="/usr/lib/postgresql14/bin" \
--old-datadir="/srv/pgsql/data/" \
--new-datadir="/srv/pgsql/data14/"
# After successfully migrating the data...
cd ..
# if not already symlinked move the old data to a versioned directory matching
# your old installation...
mv data data12
# and set a symlink to the new data directory
ln -sf data14/ data
# Now start the new service
systemctl start postgresql.service
# If everything has been sucessful, you should uninstall old packages...
#zypper rm -u postgresql12 postgresql13
# and remove old data directories
#rm -rf /srv/pgsql/data_OLD_POSTGRES_VERSION_NUMBER
# For good measure:
sudo -u postgres /usr/lib/postgresql14/bin/vacuumdb --all --analyze-in-stages
```

34
etc/db/upgrades/README.md Normal file
View File

@@ -0,0 +1,34 @@
# Database schema upgrades
When the database schema needs to be upgraded in order to provide new functionality, fix errors, etc., an upgrade script should be added to this directory.
The script can be SQL (preferred) or anything else (Bash, Python, …) in the event of complex upgrades.
The script itself should:
* document what the intended changes are;
* contain instructions on how to run it;
* make the user aware of any non-obvious side effects; and
* say if it is safe to run the script multiple times on the
* same schema / database.
## Naming
Script files should be named `upgrade-<index>-<commit-id-old>-<commit-id-new>-v<schema-version>.sql`, where:
* `<index>` is a correlative two-digit index. When reaching 99, existing files will be renamed to a three digit index (001-099) and new files will use three digits.
* `<commit-id-old>` is the ID of the Git commit that last introduced a schema change.
* `<commit-id-new>` is the ID of the first Git commit expecting the updated schema.
* `<schema-version>` is the version of the schema.
Note: the `<schema-version>` value should be updated with every change and it should be the same as reported by:
```sql
select value->>'db_schema' as db_schema from public.info where key = 'version';
```
If necessary, the wanted schema version must also be updated in `package.json`.
## Running
Schema upgrades are always run manually.

View File

@@ -0,0 +1,24 @@
-- Upgrade the database from commit 74b3de5c to commit 83be83e4.
--
-- NOTE: This upgrade only affects the `public` schema.
--
-- This inserts a database schema version into the database.
-- Note that we are not otherwise changing the schema, so older
-- server code will continue to run against this version.
--
-- ATTENTION!
--
-- This value should be incremented every time that the database
-- schema changes (either `public` or any of the survey schemas)
-- and is used by the server at start-up to detect if it is
-- running against a compatible schema version.
--
-- To apply, run as the dougal user:
--
-- psql < $THIS_FILE
--
-- NOTE: It can be applied multiple times without ill effect.
INSERT INTO public.info VALUES ('version', '{"db_schema": "0.1.0"}')
ON CONFLICT (key) DO UPDATE
SET value = public.info.value || '{"db_schema": "0.1.0"}' WHERE public.info.key = 'version';

View File

@@ -4,7 +4,7 @@ const { info } = require('../../../lib/db');
module.exports = async function (req, res, next) {
try {
await info.delete(req.params.project, req.params.path);
await info.delete(req.params.project, req.params.path, undefined, req.user.role);
res.status(204).send();
next();
} catch (err) {

View File

@@ -4,7 +4,7 @@ const { info } = require('../../../lib/db');
module.exports = async function (req, res, next) {
try {
res.status(200).json(await info.get(req.params.project, req.params.path, req.query));
res.status(200).json(await info.get(req.params.project, req.params.path, req.query, req.user.role));
} catch (err) {
if (err instanceof TypeError) {
res.status(404).json(null);

View File

@@ -6,7 +6,7 @@ module.exports = async function (req, res, next) {
try {
const payload = req.body;
await info.post(req.params.project, req.params.path, payload);
await info.post(req.params.project, req.params.path, payload, undefined, req.user.role);
res.status(201).send();
next();
} catch (err) {

View File

@@ -6,7 +6,7 @@ module.exports = async function (req, res, next) {
try {
const payload = req.body;
await info.put(req.params.project, req.params.path, payload);
await info.put(req.params.project, req.params.path, payload, undefined, req.user.role);
res.status(201).send();
next();
} catch (err) {

View File

@@ -1,18 +1,28 @@
#!/usr/bin/node
const api = require('./api');
const ws = require('./ws');
async function main () {
// Check that we're running against the correct database version
const version = require('./lib/version');
console.log("Running version", await version.describe());
version.compatible()
.then( () => {
const api = require('./api');
const ws = require('./ws');
const { fork } = require('child_process');
const { fork } = require('child_process');
// const em = require('./events');
const server = api.start(process.env.HTTP_PORT || 3000, process.env.HTTP_PATH);
ws.start(server);
const server = api.start(process.env.HTTP_PORT || 3000, process.env.HTTP_PATH);
ws.start(server);
const eventManagerPath = [__dirname, "events"].join("/");
const eventManager = fork(eventManagerPath, /*{ stdio: 'ignore' }*/);
const eventManagerPath = [__dirname, "events"].join("/");
const eventManager = fork(eventManagerPath, /*{ stdio: 'ignore' }*/);
process.on('exit', () => eventManager.kill());
})
.catch( ({current, wanted}) => {
console.error(`Fatal error: incompatible database schema version ${current} (wanted: ${wanted})`);
process.exit(1);
});
}
process.on('exit', () => eventManager.kill());
// em.start();
main();

View File

@@ -0,0 +1,83 @@
/** Check permission to read or write certain keys.
*
* The global and survey `info` tables can be used to
* store and retrieve arbitrary data, but it is also
* used by the software, with some keys being reserved
* for specific purposes.
*
* This module lists those keys which are in some way
* reserved and reports on who should be allowed what
* type of access to them.
*/
/** Reserved keys.
*
* The structure of this dictionary is
* object.verb.subject = Boolean.
*
* The special value `_` is a wildcard
* denoting the default condition for
* a verb or a subject.
*/
const dictionary = {
version: {
// Database or schema version string.
// Everyone can read, nobody can alter.
get: { _: true },
_ : { _: false }
},
config: {
// Configuration (site-wide or survey)
// Nobody except admin can access
_: { _: false, admin: true }
},
qc: {
// QC results (survey)
// Everyone can read, nobody can write
get: { _: true },
_ : { _: false }
},
equipment: {
// Equipment info (site)
// Everyone can read, user + admin can alter
get: { _: true },
_ : { _: false, user: true, admin: true }
},
contact: {
// Contact details (basically an example entry)
// Everyone can read, admin can alter
get: { _: true },
_ : { _: false, admin: true },
}
}
/** Check if access is allowed to an info entry.
*
* @a key {String} is the object of the action.
* @a verb {String} is the action.
* @a role {String} is the subject of the action.
*
* @returns {Boolean} `true` is the action is allowed,
* `false` if it is not.
*
* By default, all actions are allowed on a key that's
* not listed in the dictionary. For a key that is listed,
* the result for a default action or subject is denoted
* by `_`, others are entered explicitly.
*
*/
function checkPermission (key, verb, role) {
const entry = dictionary[key]
if (entry) {
const action = entry[verb] ?? entry._
if (action) {
return action[role] ?? action._ ?? false;
}
return false;
}
return true;
}
module.exports = checkPermission;

View File

@@ -1,9 +1,15 @@
const { setSurvey, transaction } = require('../connection');
const checkPermission = require('./check-permission');
async function del (projectId, path, opts = {}) {
async function del (projectId, path, opts = {}, role) {
const client = await setSurvey(projectId);
const [key, ...jsonpath] = (path||"").split("/").filter(i => i.length);
if (!checkPermission(key, "delete", role)) {
throw {status: 403, message: "Forbidden"};
return;
}
try {
const text = jsonpath.length
? `

View File

@@ -1,9 +1,15 @@
const { setSurvey } = require('../connection');
const checkPermission = require('./check-permission');
async function get (projectId, path, opts = {}) {
async function get (projectId, path, opts = {}, role) {
const client = await setSurvey(projectId);
const [key, ...subkey] = path.split("/").filter(i => i.trim().length);
if (!checkPermission(key, "get", role)) {
throw {status: 403, message: "Forbidden"};
return;
}
const text = `
SELECT value
FROM info

View File

@@ -1,9 +1,15 @@
const { setSurvey, transaction } = require('../connection');
const checkPermission = require('./check-permission');
async function post (projectId, path, payload, opts = {}) {
async function post (projectId, path, payload, opts = {}, role) {
const client = await setSurvey(projectId);
const [key, ...jsonpath] = (path||"").split("/").filter(i => i.length);
if (!checkPermission(key, "post", role)) {
throw {status: 403, message: "Forbidden"};
return;
}
try {
const text = jsonpath.length
? `

View File

@@ -1,9 +1,15 @@
const { setSurvey, transaction } = require('../connection');
const checkPermission = require('./check-permission');
async function put (projectId, path, payload, opts = {}) {
async function put (projectId, path, payload, opts = {}, role) {
const client = await setSurvey(projectId);
const [key, ...jsonpath] = (path||"").split("/").filter(i => i.length);
if (!checkPermission(key, "put", role)) {
throw {status: 403, message: "Forbidden"};
return;
}
try {
const text = jsonpath.length
? `

View File

@@ -1,3 +1,4 @@
const alert = require("../../../alerts");
const configuration = require('../../configuration');
async function getDistance (client, payload) {
@@ -88,6 +89,13 @@ async function getPlanned (client) {
async function getLineName (client, projectId, payload) {
// FIXME TODO Get line name script from configuration
// Ref.: https://gitlab.com/wgp/dougal/software/-/issues/129
// This is to monitor #165
// https://gitlab.com/wgp/dougal/software/-/issues/incident/165
if (!payload?.line) {
alert({function: "getLineName", client, projectId, payload});
}
const planned = await getPlanned(client);
const previous = await getSequencesForLine(client, payload.line);

View File

@@ -0,0 +1,68 @@
const semver = require("semver");
const { exec } = require("child_process");
const pkg = require("../package.json");
/** Report whether the database schema version is
* compatible with the version required by this server.
*
* The current schema version is retrieved from the
* public.info table.
*
* The wanted version is retrieved from package.json
* (config.db_schema).
*
* @returns true if the versions are compatible,
* false otherwise.
*/
function compatible () {
const { info } = require('./db');
return new Promise ( async (resolve, reject) => {
const current = await info.get(null, "version/db_schema");
const wanted = pkg.config.db_schema;
if (semver.satisfies(current, wanted)) {
resolve({current, wanted});
} else {
reject({current, wanted});
}
});
}
/** Return software name.
*
*/
function name () {
const pkg = require("../package.json");
return pkg.name ?? pkg.description ?? "Unknown";
}
/** Return software version, from Git if possible.
*
*/
async function describe () {
return new Promise( (resolve, reject) => {
const cmd = `git describe || echo git+$(git describe --always);`;
exec(cmd, {cwd: __dirname}, (error, stdout, stderr) => {
if (error) {
reject(error);
}
if (stdout) {
resolve(stdout.trim());
} else {
// Most likely not installed from Git, use the
// version number in package.json.
resolve(pkg.version ?? "Unknown")
}
})
});
}
function version () {
return pkg.version;
}
version.compatible = compatible;
version.name = name;
version.describe = describe;
module.exports = version;

View File

@@ -9,6 +9,16 @@
},
"author": "Aaltronav s.r.o.",
"license": "UNLICENSED",
"private": true,
"config": {
"db_schema": "^0.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"os": [
"linux"
],
"dependencies": {
"cookie-parser": "^1.4.5",
"express": "^4.17.1",