]>
| Commit | Line | Data |
|---|---|---|
| 91aead36 | 1 | #!/usr/bin/python3 |
| 37e24fbf MT |
2 | ############################################################################### |
| 3 | # # | |
| 4 | # ddns - A dynamic DNS client for IPFire # | |
| 5 | # Copyright (C) 2014 IPFire development team # | |
| 6 | # # | |
| 7 | # This program is free software: you can redistribute it and/or modify # | |
| 8 | # it under the terms of the GNU General Public License as published by # | |
| 9 | # the Free Software Foundation, either version 3 of the License, or # | |
| 10 | # (at your option) any later version. # | |
| 11 | # # | |
| 12 | # This program is distributed in the hope that it will be useful, # | |
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # | |
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # | |
| 15 | # GNU General Public License for more details. # | |
| 16 | # # | |
| 17 | # You should have received a copy of the GNU General Public License # | |
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. # | |
| 19 | # # | |
| 20 | ############################################################################### | |
| 21 | ||
| 22 | import datetime | |
| 63e16fee | 23 | import os |
| 37e24fbf MT |
24 | import sqlite3 |
| 25 | ||
| 26 | # Initialize the logger. | |
| 27 | import logging | |
| 28 | logger = logging.getLogger("ddns.database") | |
| 29 | logger.propagate = 1 | |
| 30 | ||
| 31 | class DDNSDatabase(object): | |
| 32 | def __init__(self, core, path): | |
| 33 | self.core = core | |
| 63e16fee | 34 | self.path = path |
| 37e24fbf | 35 | |
| 63e16fee MT |
36 | # We won't open the connection to the database directly |
| 37 | # so that we do not do it unnecessarily. | |
| 38 | self._db = None | |
| 37e24fbf | 39 | |
| 37e24fbf MT |
40 | def _open_database(self, path): |
| 41 | logger.debug("Opening database %s" % path) | |
| 42 | ||
| 43 | exists = os.path.exists(path) | |
| 44 | ||
| 45 | conn = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES) | |
| 46 | conn.isolation_level = None | |
| 47 | ||
| 63e16fee | 48 | if not exists and self.is_writable(): |
| 37e24fbf MT |
49 | logger.debug("Initialising database layout") |
| 50 | c = conn.cursor() | |
| 51 | c.executescript(""" | |
| 52 | CREATE TABLE updates ( | |
| 53 | hostname TEXT NOT NULL, | |
| 54 | status TEXT NOT NULL, | |
| 55 | message TEXT, | |
| 56 | timestamp timestamp NOT NULL | |
| 57 | ); | |
| 58 | ||
| 59 | CREATE TABLE settings ( | |
| 60 | k TEXT NOT NULL, | |
| 61 | v TEXT NOT NULL | |
| 62 | ); | |
| a2857a13 MT |
63 | |
| 64 | CREATE INDEX idx_updates_hostname ON updates(hostname); | |
| 37e24fbf MT |
65 | """) |
| 66 | c.execute("INSERT INTO settings(k, v) VALUES(?, ?)", ("version", "1")) | |
| 67 | ||
| 68 | return conn | |
| 69 | ||
| 63e16fee MT |
70 | def is_writable(self): |
| 71 | # Check if the database file exists and is writable. | |
| 72 | ret = os.access(self.path, os.W_OK) | |
| 73 | if ret: | |
| 74 | return True | |
| 75 | ||
| 76 | # If not, we check if we are able to write to the directory. | |
| 77 | # In that case the database file will be created in _open_database(). | |
| 78 | return os.access(os.path.dirname(self.path), os.W_OK) | |
| 79 | ||
| 37e24fbf | 80 | def _execute(self, query, *parameters): |
| 63e16fee MT |
81 | if self._db is None: |
| 82 | self._db = self._open_database(self.path) | |
| 83 | ||
| 37e24fbf MT |
84 | c = self._db.cursor() |
| 85 | try: | |
| 86 | c.execute(query, parameters) | |
| 87 | finally: | |
| 88 | c.close() | |
| 89 | ||
| 90 | def add_update(self, hostname, status, message=None): | |
| 63e16fee MT |
91 | if not self.is_writable(): |
| 92 | logger.warning("Could not log any updates because the database is not writable") | |
| 93 | return | |
| 94 | ||
| 37e24fbf MT |
95 | self._execute("INSERT INTO updates(hostname, status, message, timestamp) \ |
| 96 | VALUES(?, ?, ?, ?)", hostname, status, message, datetime.datetime.utcnow()) | |
| 97 | ||
| 98 | def log_success(self, hostname): | |
| 99 | logger.debug("Logging successful update for %s" % hostname) | |
| 100 | ||
| 101 | return self.add_update(hostname, "success") | |
| 102 | ||
| 103 | def log_failure(self, hostname, exception): | |
| 104 | if exception: | |
| 105 | message = "%s: %s" % (exception.__class__.__name__, exception.reason) | |
| 106 | else: | |
| 107 | message = None | |
| 108 | ||
| 109 | logger.debug("Logging failed update for %s: %s" % (hostname, message or "")) | |
| 110 | ||
| 111 | return self.add_update(hostname, "failure", message=message) | |
| 112 | ||
| 112d3fb8 MT |
113 | def last_update(self, hostname, status=None): |
| 114 | """ | |
| 115 | Returns the timestamp of the last update (with the given status code). | |
| 116 | """ | |
| f62fa5ba MT |
117 | if self._db is None: |
| 118 | self._db = self._open_database(self.path) | |
| 119 | ||
| 37e24fbf MT |
120 | c = self._db.cursor() |
| 121 | ||
| 122 | try: | |
| 112d3fb8 MT |
123 | if status: |
| 124 | c.execute("SELECT timestamp FROM updates WHERE hostname = ? AND status = ? \ | |
| 125 | ORDER BY timestamp DESC LIMIT 1", (hostname, status)) | |
| 126 | else: | |
| 127 | c.execute("SELECT timestamp FROM updates WHERE hostname = ? \ | |
| 128 | ORDER BY timestamp DESC LIMIT 1", (hostname,)) | |
| 129 | ||
| 130 | for row in c: | |
| 131 | return row[0] | |
| 132 | finally: | |
| 133 | c.close() | |
| 134 | ||
| 135 | def last_update_status(self, hostname): | |
| 136 | """ | |
| 137 | Returns the update status of the last update. | |
| 138 | """ | |
| f62fa5ba MT |
139 | if self._db is None: |
| 140 | self._db = self._open_database(self.path) | |
| 141 | ||
| 112d3fb8 MT |
142 | c = self._db.cursor() |
| 143 | ||
| 144 | try: | |
| 145 | c.execute("SELECT status FROM updates WHERE hostname = ? \ | |
| 146 | ORDER BY timestamp DESC LIMIT 1", (hostname,)) | |
| 147 | ||
| 148 | for row in c: | |
| 149 | return row[0] | |
| 150 | finally: | |
| 151 | c.close() | |
| 152 | ||
| 153 | def last_update_failure_message(self, hostname): | |
| 154 | """ | |
| 155 | Returns the reason string for the last failed update (if any). | |
| 156 | """ | |
| f62fa5ba MT |
157 | if self._db is None: |
| 158 | self._db = self._open_database(self.path) | |
| 159 | ||
| 112d3fb8 MT |
160 | c = self._db.cursor() |
| 161 | ||
| 162 | try: | |
| 163 | c.execute("SELECT message FROM updates WHERE hostname = ? AND status = ? \ | |
| 164 | ORDER BY timestamp DESC LIMIT 1", (hostname, "failure")) | |
| 37e24fbf MT |
165 | |
| 166 | for row in c: | |
| 167 | return row[0] | |
| 168 | finally: | |
| 169 | c.close() |