1# Copyright 2014-2016 OpenMarket Ltd
2# Copyright 2020-2021 The Matrix.org Foundation C.I.C.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15import argparse
16import logging
17import os
18
19from synapse.config._base import Config, ConfigError
20
21logger = logging.getLogger(__name__)
22
23NON_SQLITE_DATABASE_PATH_WARNING = """\
24Ignoring 'database_path' setting: not using a sqlite3 database.
25--------------------------------------------------------------------------------
26"""
27
28DEFAULT_CONFIG = """\
29## Database ##
30
31# The 'database' setting defines the database that synapse uses to store all of
32# its data.
33#
34# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or
35# 'psycopg2' (for PostgreSQL).
36#
37# 'txn_limit' gives the maximum number of transactions to run per connection
38# before reconnecting. Defaults to 0, which means no limit.
39#
40# 'args' gives options which are passed through to the database engine,
41# except for options starting 'cp_', which are used to configure the Twisted
42# connection pool. For a reference to valid arguments, see:
43#   * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
44#   * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
45#   * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__
46#
47#
48# Example SQLite configuration:
49#
50#database:
51#  name: sqlite3
52#  args:
53#    database: /path/to/homeserver.db
54#
55#
56# Example Postgres configuration:
57#
58#database:
59#  name: psycopg2
60#  txn_limit: 10000
61#  args:
62#    user: synapse_user
63#    password: secretpassword
64#    database: synapse
65#    host: localhost
66#    port: 5432
67#    cp_min: 5
68#    cp_max: 10
69#
70# For more information on using Synapse with Postgres,
71# see https://matrix-org.github.io/synapse/latest/postgres.html.
72#
73database:
74  name: sqlite3
75  args:
76    database: %(database_path)s
77"""
78
79
80class DatabaseConnectionConfig:
81    """Contains the connection config for a particular database.
82
83    Args:
84        name: A label for the database, used for logging.
85        db_config: The config for a particular database, as per `database`
86            section of main config. Has three fields: `name` for database
87            module name, `args` for the args to give to the database
88            connector, and optional `data_stores` that is a list of stores to
89            provision on this database (defaulting to all).
90    """
91
92    def __init__(self, name: str, db_config: dict):
93        db_engine = db_config.get("name", "sqlite3")
94
95        if db_engine not in ("sqlite3", "psycopg2"):
96            raise ConfigError("Unsupported database type %r" % (db_engine,))
97
98        if db_engine == "sqlite3":
99            db_config.setdefault("args", {}).update(
100                {"cp_min": 1, "cp_max": 1, "check_same_thread": False}
101            )
102
103        data_stores = db_config.get("data_stores")
104        if data_stores is None:
105            data_stores = ["main", "state"]
106
107        self.name = name
108        self.config = db_config
109
110        # The `data_stores` config is actually talking about `databases` (we
111        # changed the name).
112        self.databases = data_stores
113
114
115class DatabaseConfig(Config):
116    section = "database"
117
118    def __init__(self, *args, **kwargs):
119        super().__init__(*args, **kwargs)
120
121        self.databases = []
122
123    def read_config(self, config, **kwargs) -> None:
124        # We *experimentally* support specifying multiple databases via the
125        # `databases` key. This is a map from a label to database config in the
126        # same format as the `database` config option, plus an extra
127        # `data_stores` key to specify which data store goes where. For example:
128        #
129        #   databases:
130        #       master:
131        #           name: psycopg2
132        #           data_stores: ["main"]
133        #           args: {}
134        #       state:
135        #           name: psycopg2
136        #           data_stores: ["state"]
137        #           args: {}
138
139        multi_database_config = config.get("databases")
140        database_config = config.get("database")
141        database_path = config.get("database_path")
142
143        if multi_database_config and database_config:
144            raise ConfigError("Can't specify both 'database' and 'databases' in config")
145
146        if multi_database_config:
147            if database_path:
148                raise ConfigError("Can't specify 'database_path' with 'databases'")
149
150            self.databases = [
151                DatabaseConnectionConfig(name, db_conf)
152                for name, db_conf in multi_database_config.items()
153            ]
154
155        if database_config:
156            self.databases = [DatabaseConnectionConfig("master", database_config)]
157
158        if database_path:
159            if self.databases and self.databases[0].name != "sqlite3":
160                logger.warning(NON_SQLITE_DATABASE_PATH_WARNING)
161                return
162
163            database_config = {"name": "sqlite3", "args": {}}
164            self.databases = [DatabaseConnectionConfig("master", database_config)]
165            self.set_databasepath(database_path)
166
167    def generate_config_section(self, data_dir_path, **kwargs) -> str:
168        return DEFAULT_CONFIG % {
169            "database_path": os.path.join(data_dir_path, "homeserver.db")
170        }
171
172    def read_arguments(self, args: argparse.Namespace) -> None:
173        """
174        Cases for the cli input:
175          - If no databases are configured and no database_path is set, raise.
176          - No databases and only database_path available ==> sqlite3 db.
177          - If there are multiple databases and a database_path raise an error.
178          - If the database set in the config file is sqlite then
179            overwrite with the command line argument.
180        """
181
182        if args.database_path is None:
183            if not self.databases:
184                raise ConfigError("No database config provided")
185            return
186
187        if len(self.databases) == 0:
188            database_config = {"name": "sqlite3", "args": {}}
189            self.databases = [DatabaseConnectionConfig("master", database_config)]
190            self.set_databasepath(args.database_path)
191            return
192
193        if self.get_single_database().name == "sqlite3":
194            self.set_databasepath(args.database_path)
195        else:
196            logger.warning(NON_SQLITE_DATABASE_PATH_WARNING)
197
198    def set_databasepath(self, database_path: str) -> None:
199
200        if database_path != ":memory:":
201            database_path = self.abspath(database_path)
202
203        self.databases[0].config["args"]["database"] = database_path
204
205    @staticmethod
206    def add_arguments(parser: argparse.ArgumentParser) -> None:
207        db_group = parser.add_argument_group("database")
208        db_group.add_argument(
209            "-d",
210            "--database-path",
211            metavar="SQLITE_DATABASE_PATH",
212            help="The path to a sqlite database to use.",
213        )
214
215    def get_single_database(self) -> DatabaseConnectionConfig:
216        """Returns the database if there is only one, useful for e.g. tests"""
217        if not self.databases:
218            raise Exception("More than one database exists")
219
220        return self.databases[0]
221