From 0dc3aa77846ae6ef76e245bd10024caff0920840 Mon Sep 17 00:00:00 2001 From: David Trattnig <david.trattnig@o94.at> Date: Thu, 6 May 2021 15:03:49 +0200 Subject: [PATCH] PostgreSQL support. #73 --- config/sample-development.engine.ini | 13 +++---- config/sample-docker.engine.ini | 13 +++---- config/sample-production.engine.ini | 13 +++---- contrib/mariadb-requirements.txt | 1 + contrib/postgresql-create-database.sql | 4 +++ contrib/postgresql-requirements.txt | 1 + docs/bare-metal-installation.md | 49 +++++++++++++------------- scripts/setup-db-mariadb.sh | 37 ------------------- scripts/setup-db.sh | 37 ------------------- src/base/config.py | 16 +++++---- src/scheduling/models.py | 31 +++++++++------- 11 files changed, 81 insertions(+), 134 deletions(-) create mode 100644 contrib/mariadb-requirements.txt create mode 100644 contrib/postgresql-create-database.sql create mode 100644 contrib/postgresql-requirements.txt delete mode 100755 scripts/setup-db-mariadb.sh delete mode 100755 scripts/setup-db.sh diff --git a/config/sample-development.engine.ini b/config/sample-development.engine.ini index 79954cc7..2217e735 100644 --- a/config/sample-development.engine.ini +++ b/config/sample-development.engine.ini @@ -66,6 +66,13 @@ api_engine_store_clock="http://localhost:8008/api/v1/clock" api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}" [scheduler] +# Database settings: Use 'postgresql' or 'mysql' +db_type="postgresql" +db_name="aura_engine" +db_user="aura_engine" +db_pass="---SECRET--PASSWORD---" +db_host="localhost" +db_charset="utf8" # Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs) # Either provide an absolute base path or a relative one starting in the `engine-core/src` directory. In case of `engine-core` running in docker use `/var/audio/source` audio_source_folder="audio/source" @@ -76,12 +83,6 @@ audio_playlist_folder="audio/playlist" engine_latency_offset=0.5 # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired fetching_frequency=30 -# The schedule is fetched on every "fetching_frequency" cycle and stored in a local database -db_user="aura" -db_name="aura_engine" -db_pass="---SECRET--PASSWORD---" -db_host="localhost" -db_charset="utf8" # The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds # and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window. scheduling_window_start=60 diff --git a/config/sample-docker.engine.ini b/config/sample-docker.engine.ini index a681b3c4..d954d091 100644 --- a/config/sample-docker.engine.ini +++ b/config/sample-docker.engine.ini @@ -66,6 +66,13 @@ api_engine_store_clock="http://127.0.0.1:8008/api/v1/clock" api_engine_store_health="http://127.0.0.1:8008/api/v1/source/health/${ENGINE_NUMBER}" [scheduler] +# Database settings: Use 'postgresql' or 'mysql' +db_type="postgresql" +db_name="aura_engine" +db_user="aura_engine" +db_pass="---SECRET--PASSWORD---" +db_host="127.0.0.1" +db_charset="utf8" # Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs) # Either provide an absolute base path or a relative one starting in the `engine-core/src` directory. In case of `engine-core` running in docker use `/var/audio/source` audio_source_folder="/var/audio/source" @@ -76,12 +83,6 @@ audio_playlist_folder="/var/audio/playlist" engine_latency_offset=0.5 # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired fetching_frequency=300 -# The schedule is fetched on every "fetching_frequency" cycle and stored in a local database -db_user="aura" -db_name="aura_engine" -db_pass="---SECRET--PASSWORD---" -db_host="localhost" -db_charset="utf8" # The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds # and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window. scheduling_window_start=60 diff --git a/config/sample-production.engine.ini b/config/sample-production.engine.ini index 052792b1..723b58c8 100644 --- a/config/sample-production.engine.ini +++ b/config/sample-production.engine.ini @@ -66,6 +66,13 @@ api_engine_store_clock="http://localhost:8008/api/v1/clock" api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUMBER}" [scheduler] +# Database settings: Use 'postgresql' or 'mysql' +db_type="postgresql" +db_name="aura_engine" +db_user="aura_engine" +db_pass="---SECRET--PASSWORD---" +db_host="localhost" +db_charset="utf8" # Base path as seen by "engine-core", not accessed by "engine"; this is required to construct the absolute audio file path (check "Audio Store" in the docs) # Either provide an absolute base path or a relative one starting in the `engine-core/src` directory. In case of `engine-core` running in docker use `/var/audio/source` audio_source_folder="audio/source" @@ -76,12 +83,6 @@ audio_playlist_folder="audio/playlist" engine_latency_offset=0.5 # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired fetching_frequency=300 -# The schedule is fetched on every "fetching_frequency" cycle and stored in a local database -db_user="aura" -db_name="aura_engine" -db_pass="---SECRET--PASSWORD---" -db_host="localhost" -db_charset="utf8" # The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds # and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window. scheduling_window_start=60 diff --git a/contrib/mariadb-requirements.txt b/contrib/mariadb-requirements.txt new file mode 100644 index 00000000..ccd50492 --- /dev/null +++ b/contrib/mariadb-requirements.txt @@ -0,0 +1 @@ +mysqlclient==1.3.12 \ No newline at end of file diff --git a/contrib/postgresql-create-database.sql b/contrib/postgresql-create-database.sql new file mode 100644 index 00000000..d0732d14 --- /dev/null +++ b/contrib/postgresql-create-database.sql @@ -0,0 +1,4 @@ +\c postgres +create database aura_engine; +create user aura_engine with encrypted password '1234'; +grant all privileges on database aura_engine to aura_engine; diff --git a/contrib/postgresql-requirements.txt b/contrib/postgresql-requirements.txt new file mode 100644 index 00000000..4ae932dc --- /dev/null +++ b/contrib/postgresql-requirements.txt @@ -0,0 +1 @@ +psycopg2-binary==2.8.6 \ No newline at end of file diff --git a/docs/bare-metal-installation.md b/docs/bare-metal-installation.md index 33a89b8e..3f4e0ec2 100644 --- a/docs/bare-metal-installation.md +++ b/docs/bare-metal-installation.md @@ -4,8 +4,8 @@ - [Install for Development](#install-for-development) - [Prerequisites](#prerequisites) -- [Setting up the database](#setting-up-the-database) - [Preparation](#preparation) + - [Setting up the database](#setting-up-the-database) - [Configuration](#configuration) - [Running Engine](#running-engine) - [Daemonized Engine](#daemonized-engine) @@ -23,30 +23,9 @@ Aura Engine runs on any modern Debian-based OS. It requires at least - [`Python 3.8+`](https://www.python.org/downloads/release/python-380/) - [`pip`](https://pip.pypa.io/en/stable/) - [`git`](https://git-scm.com/) -- ['virtualenv'](https://pypi.org/project/virtualenv/) (development only) -- DBMS server/client libraries for MariaDB or PostgreSQL (see below) +- ['virtualenv'](https://pypi.org/project/virtualenv/) +- [PostgreSQL 13+](https://www.postgresql.org/) or [MariaDB 10+](https://mariadb.org/) -# Setting up the database - -Depending on the DBMS you are planning to use you'll need to have the relevant server/client libaries to be present. - -**MariaDB** - -```shell -sudo apt install \ - python3.8-dev \ - default-libmysqlclient-dev \ - mariadb-server \ - libmariadbclient-dev -``` - -The following installation script sets up the initial databases and users. - -```bash - bash scripts/setup-db.sh -``` - -As soon as this is completed, carefully check if any error occured. In case your database has been setup automatically, note the relevant credentials for later use in your `engine.ini` configuration. **Setting up the project structure** @@ -94,6 +73,28 @@ cp config/sample.development.engine.ini config/engine.ini cp config/sample.production.engine.ini config/engine.ini ``` +### Setting up the database + +The primary database supported by AURA is PostgreSQL. + +```bash +# Additional Python packages for PostgreSQL +pip3 install -r contrib/postgresql-requirements.txt +# Create database and user (change password in script) +sudo -u postgres psql -f contrib/postgresql-create-database.sql +``` + +Alternatively you can also use MariaDB: + +```bash +# Additional Python packages for MariaDB +pip3 install -r contrib/mariadb-requirements.txt +# Create database and user (change password in script) +sudo mysql -u root -p < contrib/mariadb-database.sql +``` + +You might want to change the password for the database user created by the relevant script. + ## Configuration In your development environment edit following file to configure the engine: diff --git a/scripts/setup-db-mariadb.sh b/scripts/setup-db-mariadb.sh deleted file mode 100755 index a6d7c4d8..00000000 --- a/scripts/setup-db-mariadb.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Check if databases are already set-up -if test -f "$LOCKFILE_DB"; then - echo "Aura Engine Databases are already existing! Skipping..." -else - # Create random password - PASS_ENGINE="$(openssl rand -base64 24)" - - # Create databases and users - echo "--- SETTING UP DATABASE AND USERS ---" - echo "Please enter the MySQL/MariaDB root password!" - stty -echo - printf "Password: " - read rootpasswd - stty echo - printf "\n" - echo "---" - - echo "Creating database for Aura Engine..." - mysql -uroot -p${rootpasswd} -e "CREATE DATABASE aura_engine CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - mysql -uroot -p${rootpasswd} -e "CREATE USER 'aura'@'localhost' IDENTIFIED BY '${PASS_ENGINE}';" - mysql -uroot -p${rootpasswd} -e "GRANT ALL PRIVILEGES ON aura_engine.* TO 'aura'@'localhost';" - mysql -uroot -p${rootpasswd} -e "FLUSH PRIVILEGES;" - echo "Done." - - echo - echo - echo "Please note your database credentials for the next configuration steps:" - echo "-----------------------------------------------------------------------" - echo " Database: 'aura_engine'" - echo " User: 'aura'" - echo " Password: '${PASS_ENGINE}'" - echo "-----------------------------------------------------------------------" - echo - -fi \ No newline at end of file diff --git a/scripts/setup-db.sh b/scripts/setup-db.sh deleted file mode 100755 index 6ee28f84..00000000 --- a/scripts/setup-db.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# -# Setup Database -# - -# Set LOCK file location -LOCKFILE_DB=.engine.install-db.lock - -# Check if databases are already set-up -if test -f "$LOCKFILE_DB"; then - echo "Aura Engine Databases are already existing! Skipping..." -else - echo "Setting up database ..." - echo - echo "Which database system do you want to use? (Press '1' or '2')" - echo " [1] MariaDB" - echo " [2] Other / Manually" - echo - - while true; do - read -rsn1 input - - if [ "$input" = "1" ]; then - echo "Creating DB for MariaDB ..." - bash scripts/setup-db-mariadb.sh - break - fi - if [ "$input" = "2" ]; then - echo "Manual database setup selected." - break - fi - done - - # Create lockfile to avoid accidential re-creation of the database - touch $LOCKFILE_DB -fi diff --git a/src/base/config.py b/src/base/config.py index 1d4679ba..52056e65 100644 --- a/src/base/config.py +++ b/src/base/config.py @@ -103,18 +103,22 @@ class AuraConfig: return self.__dict__[key] - def get_database_uri(self): """ Retrieves the database connection string. """ - db_name = self.get("db_name") - db_user = self.get("db_user") + db_name = str(self.get("db_name")) + db_user = str(self.get("db_user")) db_pass = str(self.get("db_pass")) - db_host = self.get("db_host") + db_host = str(self.get("db_host")) + db_type = str(self.get("db_type")) db_charset = self.get("db_charset", "utf8") - return "mysql://" + db_user + ":" + db_pass + "@" + db_host + "/" + db_name + "?charset=" + db_charset - + if db_type == "mysql": + return "mysql://" + db_user + ":" + db_pass + "@" + db_host + "/" + db_name + "?charset=" + db_charset + elif db_type == "postgresql": + return f"postgresql+psycopg2://{db_user}:{db_pass}@{db_host}/{db_name}?client_encoding={db_charset}" + else: + return f"Error: invalid database type '{db_type}'" def load_config(self): diff --git a/src/scheduling/models.py b/src/scheduling/models.py index 1f477703..f6a15987 100644 --- a/src/scheduling/models.py +++ b/src/scheduling/models.py @@ -109,9 +109,16 @@ class AuraDatabaseModel(): try: Playlist.is_empty() except sa.exc.ProgrammingError as e: - errcode = e.orig.args[0] + is_available = True - if errcode == 1146: # Error for no such table + # PostgreSQL table not available + if e.code == "f405": + is_available = False + # MariaDB table not available + elif e.orig.args[0] == 1146: + is_available = False + + if not is_available: model = AuraDatabaseModel() model.recreate_db() else: @@ -122,7 +129,7 @@ class AuraDatabaseModel(): def recreate_db(systemexit = False): """ Deletes all tables and re-creates the database. - """ + """ Base.metadata.drop_all() Base.metadata.create_all() DB.session.commit() @@ -166,11 +173,11 @@ class Timeslot(DB.Model, AuraDatabaseModel): default_show_playlist = relationship("Playlist", primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \ Timeslot.default_show_playlist_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)", - uselist=False, back_populates="timeslot") + uselist=False, back_populates="timeslot") schedule_fallback = relationship("Playlist", primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \ Timeslot.schedule_fallback_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)", - uselist=False, back_populates="timeslot") + uselist=False, back_populates="timeslot") show_fallback = relationship("Playlist", primaryjoin="and_(Timeslot.timeslot_start==Playlist.timeslot_start, \ Timeslot.show_fallback_id==Playlist.playlist_id, Timeslot.show_name==Playlist.show_name)", @@ -203,7 +210,7 @@ class Timeslot(DB.Model, AuraDatabaseModel): topic = Column(String(256)) musicfocus = Column(String(256)) is_repetition = Column(Boolean()) - + # Transients active_entry = None @@ -233,7 +240,7 @@ class Timeslot(DB.Model, AuraDatabaseModel): """ timeslots = DB.session.query(Timeslot).\ filter(Timeslot.timeslot_start >= date_from).\ - order_by(Timeslot.timeslot_start).all() + order_by(Timeslot.timeslot_start).all() return timeslots @@ -278,7 +285,7 @@ class Timeslot(DB.Model, AuraDatabaseModel): playlist = self.playlist return { - "timeslot_id": self.timeslot_id, + "timeslot_id": self.timeslot_id, "timeslot_start": self.timeslot_start.isoformat(), "timeslot_end": self.timeslot_end.isoformat(), @@ -291,7 +298,7 @@ class Timeslot(DB.Model, AuraDatabaseModel): "comment": self.comment, "playlist_id": self.playlist_id, "schedule_default_id": self.schedule_default_id, - "show_default_id": self.show_default_id, + "show_default_id": self.show_default_id, "schedule_fallback_id": self.schedule_fallback_id, "show_fallback_id": self.show_fallback_id, "station_fallback_id": self.station_fallback_id, @@ -385,12 +392,12 @@ class Playlist(DB.Model, AuraDatabaseModel): Args: playlist_id (Integer): The ID of the playlist - + Returns: (Array<Playlist>): An array holding the playlists """ return DB.session.query(Playlist).filter(Playlist.playlist_id == playlist_id).order_by(Playlist.timeslot_start).all() - + @staticmethod def is_empty(): @@ -488,7 +495,7 @@ class PlaylistEntry(DB.Model, AuraDatabaseModel): # Transients entry_start_actual = None # Assigned when the entry is actually played channel = None # Assigned when entry is actually played - queue_state = None # Assigned when entry is about to be queued + queue_state = None # Assigned when entry is about to be queued status = None # Assigned when state changes -- GitLab