diff options
| author | Richard Purdie <richard.purdie@linuxfoundation.org> | 2025-11-07 13:31:53 +0000 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2025-11-07 13:31:53 +0000 |
| commit | 8c22ff0d8b70d9b12f0487ef696a7e915b9e3173 (patch) | |
| tree | efdc32587159d0050a69009bdf2330a531727d95 /bitbake/lib/prserv | |
| parent | d412d2747595c1cc4a5e3ca975e3adc31b2f7891 (diff) | |
| download | poky-8c22ff0d8b70d9b12f0487ef696a7e915b9e3173.tar.gz | |
The poky repository master branch is no longer being updated.
You can either:
a) switch to individual clones of bitbake, openembedded-core, meta-yocto and yocto-docs
b) use the new bitbake-setup
You can find information about either approach in our documentation:
https://docs.yoctoproject.org/
Note that "poky" the distro setting is still available in meta-yocto as
before and we continue to use and maintain that.
Long live Poky!
Some further information on the background of this change can be found
in: https://lists.openembedded.org/g/openembedded-architecture/message/2179
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/prserv')
| -rw-r--r-- | bitbake/lib/prserv/__init__.py | 95 | ||||
| -rw-r--r-- | bitbake/lib/prserv/client.py | 72 | ||||
| -rw-r--r-- | bitbake/lib/prserv/db.py | 301 | ||||
| -rw-r--r-- | bitbake/lib/prserv/serv.py | 482 | ||||
| -rw-r--r-- | bitbake/lib/prserv/tests.py | 388 |
5 files changed, 0 insertions, 1338 deletions
diff --git a/bitbake/lib/prserv/__init__.py b/bitbake/lib/prserv/__init__.py deleted file mode 100644 index ffc5a40a28..0000000000 --- a/bitbake/lib/prserv/__init__.py +++ /dev/null | |||
| @@ -1,95 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright BitBake Contributors | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | |||
| 7 | |||
| 8 | __version__ = "2.0.0" | ||
| 9 | |||
| 10 | import logging | ||
| 11 | logger = logging.getLogger("BitBake.PRserv") | ||
| 12 | |||
| 13 | from bb.asyncrpc.client import parse_address, ADDR_TYPE_UNIX, ADDR_TYPE_WS | ||
| 14 | |||
| 15 | def create_server(addr, dbpath, upstream=None, read_only=False): | ||
| 16 | from . import serv | ||
| 17 | |||
| 18 | s = serv.PRServer(dbpath, upstream=upstream, read_only=read_only) | ||
| 19 | host, port = addr.split(":") | ||
| 20 | s.start_tcp_server(host, int(port)) | ||
| 21 | |||
| 22 | return s | ||
| 23 | |||
| 24 | def increase_revision(ver): | ||
| 25 | """Take a revision string such as "1" or "1.2.3" or even a number and increase its last number | ||
| 26 | This fails if the last number is not an integer""" | ||
| 27 | |||
| 28 | fields=str(ver).split('.') | ||
| 29 | last = fields[-1] | ||
| 30 | |||
| 31 | try: | ||
| 32 | val = int(last) | ||
| 33 | except Exception as e: | ||
| 34 | logger.critical("Unable to increase revision value %s: %s" % (ver, e)) | ||
| 35 | raise e | ||
| 36 | |||
| 37 | return ".".join(fields[0:-1] + [ str(val + 1) ]) | ||
| 38 | |||
| 39 | def _revision_greater_or_equal(rev1, rev2): | ||
| 40 | """Compares x.y.z revision numbers, using integer comparison | ||
| 41 | Returns True if rev1 is greater or equal to rev2""" | ||
| 42 | |||
| 43 | fields1 = rev1.split(".") | ||
| 44 | fields2 = rev2.split(".") | ||
| 45 | l1 = len(fields1) | ||
| 46 | l2 = len(fields2) | ||
| 47 | |||
| 48 | for i in range(l1): | ||
| 49 | val1 = int(fields1[i]) | ||
| 50 | if i < l2: | ||
| 51 | val2 = int(fields2[i]) | ||
| 52 | if val2 < val1: | ||
| 53 | return True | ||
| 54 | elif val2 > val1: | ||
| 55 | return False | ||
| 56 | else: | ||
| 57 | return True | ||
| 58 | return True | ||
| 59 | |||
| 60 | def revision_smaller(rev1, rev2): | ||
| 61 | """Compares x.y.z revision numbers, using integer comparison | ||
| 62 | Returns True if rev1 is strictly smaller than rev2""" | ||
| 63 | return not(_revision_greater_or_equal(rev1, rev2)) | ||
| 64 | |||
| 65 | def revision_greater(rev1, rev2): | ||
| 66 | """Compares x.y.z revision numbers, using integer comparison | ||
| 67 | Returns True if rev1 is strictly greater than rev2""" | ||
| 68 | return _revision_greater_or_equal(rev1, rev2) and (rev1 != rev2) | ||
| 69 | |||
| 70 | def create_client(addr): | ||
| 71 | from . import client | ||
| 72 | |||
| 73 | c = client.PRClient() | ||
| 74 | |||
| 75 | try: | ||
| 76 | (typ, a) = parse_address(addr) | ||
| 77 | c.connect_tcp(*a) | ||
| 78 | return c | ||
| 79 | except Exception as e: | ||
| 80 | c.close() | ||
| 81 | raise e | ||
| 82 | |||
| 83 | async def create_async_client(addr): | ||
| 84 | from . import client | ||
| 85 | |||
| 86 | c = client.PRAsyncClient() | ||
| 87 | |||
| 88 | try: | ||
| 89 | (typ, a) = parse_address(addr) | ||
| 90 | await c.connect_tcp(*a) | ||
| 91 | return c | ||
| 92 | |||
| 93 | except Exception as e: | ||
| 94 | await c.close() | ||
| 95 | raise e | ||
diff --git a/bitbake/lib/prserv/client.py b/bitbake/lib/prserv/client.py deleted file mode 100644 index 9f5794c433..0000000000 --- a/bitbake/lib/prserv/client.py +++ /dev/null | |||
| @@ -1,72 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright BitBake Contributors | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | |||
| 7 | import logging | ||
| 8 | import bb.asyncrpc | ||
| 9 | from . import create_async_client | ||
| 10 | |||
| 11 | logger = logging.getLogger("BitBake.PRserv") | ||
| 12 | |||
| 13 | class PRAsyncClient(bb.asyncrpc.AsyncClient): | ||
| 14 | def __init__(self): | ||
| 15 | super().__init__("PRSERVICE", "1.0", logger) | ||
| 16 | |||
| 17 | async def getPR(self, version, pkgarch, checksum, history=False): | ||
| 18 | response = await self.invoke( | ||
| 19 | {"get-pr": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "history": history}} | ||
| 20 | ) | ||
| 21 | if response: | ||
| 22 | return response["value"] | ||
| 23 | |||
| 24 | async def test_pr(self, version, pkgarch, checksum, history=False): | ||
| 25 | response = await self.invoke( | ||
| 26 | {"test-pr": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "history": history}} | ||
| 27 | ) | ||
| 28 | if response: | ||
| 29 | return response["value"] | ||
| 30 | |||
| 31 | async def test_package(self, version, pkgarch): | ||
| 32 | response = await self.invoke( | ||
| 33 | {"test-package": {"version": version, "pkgarch": pkgarch}} | ||
| 34 | ) | ||
| 35 | if response: | ||
| 36 | return response["value"] | ||
| 37 | |||
| 38 | async def max_package_pr(self, version, pkgarch): | ||
| 39 | response = await self.invoke( | ||
| 40 | {"max-package-pr": {"version": version, "pkgarch": pkgarch}} | ||
| 41 | ) | ||
| 42 | if response: | ||
| 43 | return response["value"] | ||
| 44 | |||
| 45 | async def importone(self, version, pkgarch, checksum, value): | ||
| 46 | response = await self.invoke( | ||
| 47 | {"import-one": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "value": value}} | ||
| 48 | ) | ||
| 49 | if response: | ||
| 50 | return response["value"] | ||
| 51 | |||
| 52 | async def export(self, version, pkgarch, checksum, colinfo, history=False): | ||
| 53 | response = await self.invoke( | ||
| 54 | {"export": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "colinfo": colinfo, "history": history}} | ||
| 55 | ) | ||
| 56 | if response: | ||
| 57 | return (response["metainfo"], response["datainfo"]) | ||
| 58 | |||
| 59 | async def is_readonly(self): | ||
| 60 | response = await self.invoke( | ||
| 61 | {"is-readonly": {}} | ||
| 62 | ) | ||
| 63 | if response: | ||
| 64 | return response["readonly"] | ||
| 65 | |||
| 66 | class PRClient(bb.asyncrpc.Client): | ||
| 67 | def __init__(self): | ||
| 68 | super().__init__() | ||
| 69 | self._add_methods("getPR", "test_pr", "test_package", "max_package_pr", "importone", "export", "is_readonly") | ||
| 70 | |||
| 71 | def _get_async_client(self): | ||
| 72 | return PRAsyncClient() | ||
diff --git a/bitbake/lib/prserv/db.py b/bitbake/lib/prserv/db.py deleted file mode 100644 index 2da493ddf5..0000000000 --- a/bitbake/lib/prserv/db.py +++ /dev/null | |||
| @@ -1,301 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright BitBake Contributors | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | |||
| 7 | import logging | ||
| 8 | import os.path | ||
| 9 | import errno | ||
| 10 | import prserv | ||
| 11 | import sqlite3 | ||
| 12 | |||
| 13 | from contextlib import closing | ||
| 14 | from . import increase_revision, revision_greater, revision_smaller | ||
| 15 | |||
| 16 | logger = logging.getLogger("BitBake.PRserv") | ||
| 17 | |||
| 18 | # | ||
| 19 | # "No History" mode - for a given query tuple (version, pkgarch, checksum), | ||
| 20 | # the returned value will be the largest among all the values of the same | ||
| 21 | # (version, pkgarch). This means the PR value returned can NOT be decremented. | ||
| 22 | # | ||
| 23 | # "History" mode - Return a new higher value for previously unseen query | ||
| 24 | # tuple (version, pkgarch, checksum), otherwise return historical value. | ||
| 25 | # Value can decrement if returning to a previous build. | ||
| 26 | |||
| 27 | class PRTable(object): | ||
| 28 | def __init__(self, conn, table, read_only): | ||
| 29 | self.conn = conn | ||
| 30 | self.read_only = read_only | ||
| 31 | self.table = table | ||
| 32 | |||
| 33 | # Creating the table even if the server is read-only. | ||
| 34 | # This avoids a race condition if a shared database | ||
| 35 | # is accessed by a read-only server first. | ||
| 36 | |||
| 37 | with closing(self.conn.cursor()) as cursor: | ||
| 38 | cursor.execute("CREATE TABLE IF NOT EXISTS %s \ | ||
| 39 | (version TEXT NOT NULL, \ | ||
| 40 | pkgarch TEXT NOT NULL, \ | ||
| 41 | checksum TEXT NOT NULL, \ | ||
| 42 | value TEXT, \ | ||
| 43 | PRIMARY KEY (version, pkgarch, checksum, value));" % self.table) | ||
| 44 | self.conn.commit() | ||
| 45 | |||
| 46 | def _extremum_value(self, rows, is_max): | ||
| 47 | value = None | ||
| 48 | |||
| 49 | for row in rows: | ||
| 50 | current_value = row[0] | ||
| 51 | if value is None: | ||
| 52 | value = current_value | ||
| 53 | else: | ||
| 54 | if is_max: | ||
| 55 | is_new_extremum = revision_greater(current_value, value) | ||
| 56 | else: | ||
| 57 | is_new_extremum = revision_smaller(current_value, value) | ||
| 58 | if is_new_extremum: | ||
| 59 | value = current_value | ||
| 60 | return value | ||
| 61 | |||
| 62 | def _max_value(self, rows): | ||
| 63 | return self._extremum_value(rows, True) | ||
| 64 | |||
| 65 | def _min_value(self, rows): | ||
| 66 | return self._extremum_value(rows, False) | ||
| 67 | |||
| 68 | def test_package(self, version, pkgarch): | ||
| 69 | """Returns whether the specified package version is found in the database for the specified architecture""" | ||
| 70 | |||
| 71 | # Just returns the value if found or None otherwise | ||
| 72 | with closing(self.conn.cursor()) as cursor: | ||
| 73 | data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=?;" % self.table, | ||
| 74 | (version, pkgarch)) | ||
| 75 | row=data.fetchone() | ||
| 76 | if row is not None: | ||
| 77 | return True | ||
| 78 | else: | ||
| 79 | return False | ||
| 80 | |||
| 81 | def test_checksum_value(self, version, pkgarch, checksum, value): | ||
| 82 | """Returns whether the specified value is found in the database for the specified package, architecture and checksum""" | ||
| 83 | |||
| 84 | with closing(self.conn.cursor()) as cursor: | ||
| 85 | data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and checksum=? and value=?;" % self.table, | ||
| 86 | (version, pkgarch, checksum, value)) | ||
| 87 | row=data.fetchone() | ||
| 88 | if row is not None: | ||
| 89 | return True | ||
| 90 | else: | ||
| 91 | return False | ||
| 92 | |||
| 93 | def test_value(self, version, pkgarch, value): | ||
| 94 | """Returns whether the specified value is found in the database for the specified package and architecture""" | ||
| 95 | |||
| 96 | # Just returns the value if found or None otherwise | ||
| 97 | with closing(self.conn.cursor()) as cursor: | ||
| 98 | data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and value=?;" % self.table, | ||
| 99 | (version, pkgarch, value)) | ||
| 100 | row=data.fetchone() | ||
| 101 | if row is not None: | ||
| 102 | return True | ||
| 103 | else: | ||
| 104 | return False | ||
| 105 | |||
| 106 | |||
| 107 | def find_package_max_value(self, version, pkgarch): | ||
| 108 | """Returns the greatest value for (version, pkgarch), or None if not found. Doesn't create a new value""" | ||
| 109 | |||
| 110 | with closing(self.conn.cursor()) as cursor: | ||
| 111 | data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=?;" % (self.table), | ||
| 112 | (version, pkgarch)) | ||
| 113 | rows = data.fetchall() | ||
| 114 | value = self._max_value(rows) | ||
| 115 | return value | ||
| 116 | |||
| 117 | def find_value(self, version, pkgarch, checksum, history=False): | ||
| 118 | """Returns the value for the specified checksum if found or None otherwise.""" | ||
| 119 | |||
| 120 | if history: | ||
| 121 | return self.find_min_value(version, pkgarch, checksum) | ||
| 122 | else: | ||
| 123 | return self.find_max_value(version, pkgarch, checksum) | ||
| 124 | |||
| 125 | |||
| 126 | def _find_extremum_value(self, version, pkgarch, checksum, is_max): | ||
| 127 | """Returns the maximum (if is_max is True) or minimum (if is_max is False) value | ||
| 128 | for (version, pkgarch, checksum), or None if not found. Doesn't create a new value""" | ||
| 129 | |||
| 130 | with closing(self.conn.cursor()) as cursor: | ||
| 131 | data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND checksum=?;" % (self.table), | ||
| 132 | (version, pkgarch, checksum)) | ||
| 133 | rows = data.fetchall() | ||
| 134 | return self._extremum_value(rows, is_max) | ||
| 135 | |||
| 136 | def find_max_value(self, version, pkgarch, checksum): | ||
| 137 | return self._find_extremum_value(version, pkgarch, checksum, True) | ||
| 138 | |||
| 139 | def find_min_value(self, version, pkgarch, checksum): | ||
| 140 | return self._find_extremum_value(version, pkgarch, checksum, False) | ||
| 141 | |||
| 142 | def find_new_subvalue(self, version, pkgarch, base): | ||
| 143 | """Take and increase the greatest "<base>.y" value for (version, pkgarch), or return "<base>.0" if not found. | ||
| 144 | This doesn't store a new value.""" | ||
| 145 | |||
| 146 | with closing(self.conn.cursor()) as cursor: | ||
| 147 | data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND value LIKE '%s.%%';" % (self.table, base), | ||
| 148 | (version, pkgarch)) | ||
| 149 | rows = data.fetchall() | ||
| 150 | value = self._max_value(rows) | ||
| 151 | |||
| 152 | if value is not None: | ||
| 153 | return increase_revision(value) | ||
| 154 | else: | ||
| 155 | return base + ".0" | ||
| 156 | |||
| 157 | def store_value(self, version, pkgarch, checksum, value): | ||
| 158 | """Store value in the database""" | ||
| 159 | |||
| 160 | if not self.read_only and not self.test_checksum_value(version, pkgarch, checksum, value): | ||
| 161 | with closing(self.conn.cursor()) as cursor: | ||
| 162 | cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table), | ||
| 163 | (version, pkgarch, checksum, value)) | ||
| 164 | self.conn.commit() | ||
| 165 | |||
| 166 | def _get_value(self, version, pkgarch, checksum, history): | ||
| 167 | |||
| 168 | max_value = self.find_package_max_value(version, pkgarch) | ||
| 169 | |||
| 170 | if max_value is None: | ||
| 171 | # version, pkgarch completely unknown. Return initial value. | ||
| 172 | return "0" | ||
| 173 | |||
| 174 | value = self.find_value(version, pkgarch, checksum, history) | ||
| 175 | |||
| 176 | if value is None: | ||
| 177 | # version, pkgarch found but not checksum. Create a new value from the maximum one | ||
| 178 | return increase_revision(max_value) | ||
| 179 | |||
| 180 | if history: | ||
| 181 | return value | ||
| 182 | |||
| 183 | # "no history" mode - If the value is not the maximum value for the package, need to increase it. | ||
| 184 | if max_value > value: | ||
| 185 | return increase_revision(max_value) | ||
| 186 | else: | ||
| 187 | return value | ||
| 188 | |||
| 189 | def get_value(self, version, pkgarch, checksum, history): | ||
| 190 | value = self._get_value(version, pkgarch, checksum, history) | ||
| 191 | if not self.read_only: | ||
| 192 | self.store_value(version, pkgarch, checksum, value) | ||
| 193 | return value | ||
| 194 | |||
| 195 | def importone(self, version, pkgarch, checksum, value): | ||
| 196 | self.store_value(version, pkgarch, checksum, value) | ||
| 197 | return value | ||
| 198 | |||
| 199 | def export(self, version, pkgarch, checksum, colinfo, history=False): | ||
| 200 | metainfo = {} | ||
| 201 | with closing(self.conn.cursor()) as cursor: | ||
| 202 | #column info | ||
| 203 | if colinfo: | ||
| 204 | metainfo["tbl_name"] = self.table | ||
| 205 | metainfo["core_ver"] = prserv.__version__ | ||
| 206 | metainfo["col_info"] = [] | ||
| 207 | data = cursor.execute("PRAGMA table_info(%s);" % self.table) | ||
| 208 | for row in data: | ||
| 209 | col = {} | ||
| 210 | col["name"] = row["name"] | ||
| 211 | col["type"] = row["type"] | ||
| 212 | col["notnull"] = row["notnull"] | ||
| 213 | col["dflt_value"] = row["dflt_value"] | ||
| 214 | col["pk"] = row["pk"] | ||
| 215 | metainfo["col_info"].append(col) | ||
| 216 | |||
| 217 | #data info | ||
| 218 | datainfo = [] | ||
| 219 | |||
| 220 | if history: | ||
| 221 | sqlstmt = "SELECT * FROM %s as T1 WHERE 1=1 " % self.table | ||
| 222 | else: | ||
| 223 | sqlstmt = "SELECT T1.version, T1.pkgarch, T1.checksum, T1.value FROM %s as T1, \ | ||
| 224 | (SELECT version, pkgarch, max(value) as maxvalue FROM %s GROUP BY version, pkgarch) as T2 \ | ||
| 225 | WHERE T1.version=T2.version AND T1.pkgarch=T2.pkgarch AND T1.value=T2.maxvalue " % (self.table, self.table) | ||
| 226 | sqlarg = [] | ||
| 227 | where = "" | ||
| 228 | if version: | ||
| 229 | where += "AND T1.version=? " | ||
| 230 | sqlarg.append(str(version)) | ||
| 231 | if pkgarch: | ||
| 232 | where += "AND T1.pkgarch=? " | ||
| 233 | sqlarg.append(str(pkgarch)) | ||
| 234 | if checksum: | ||
| 235 | where += "AND T1.checksum=? " | ||
| 236 | sqlarg.append(str(checksum)) | ||
| 237 | |||
| 238 | sqlstmt += where + ";" | ||
| 239 | |||
| 240 | if len(sqlarg): | ||
| 241 | data = cursor.execute(sqlstmt, tuple(sqlarg)) | ||
| 242 | else: | ||
| 243 | data = cursor.execute(sqlstmt) | ||
| 244 | for row in data: | ||
| 245 | if row["version"]: | ||
| 246 | col = {} | ||
| 247 | col["version"] = row["version"] | ||
| 248 | col["pkgarch"] = row["pkgarch"] | ||
| 249 | col["checksum"] = row["checksum"] | ||
| 250 | col["value"] = row["value"] | ||
| 251 | datainfo.append(col) | ||
| 252 | return (metainfo, datainfo) | ||
| 253 | |||
| 254 | def dump_db(self, fd): | ||
| 255 | writeCount = 0 | ||
| 256 | for line in self.conn.iterdump(): | ||
| 257 | writeCount = writeCount + len(line) + 1 | ||
| 258 | fd.write(line) | ||
| 259 | fd.write("\n") | ||
| 260 | return writeCount | ||
| 261 | |||
| 262 | class PRData(object): | ||
| 263 | """Object representing the PR database""" | ||
| 264 | def __init__(self, filename, read_only=False): | ||
| 265 | self.filename=os.path.abspath(filename) | ||
| 266 | self.read_only = read_only | ||
| 267 | #build directory hierarchy | ||
| 268 | try: | ||
| 269 | os.makedirs(os.path.dirname(self.filename)) | ||
| 270 | except OSError as e: | ||
| 271 | if e.errno != errno.EEXIST: | ||
| 272 | raise e | ||
| 273 | uri = "file:%s%s" % (self.filename, "?mode=ro" if self.read_only else "") | ||
| 274 | logger.debug("Opening PRServ database '%s'" % (uri)) | ||
| 275 | self.connection=sqlite3.connect(uri, uri=True) | ||
| 276 | self.connection.row_factory=sqlite3.Row | ||
| 277 | self.connection.execute("PRAGMA synchronous = OFF;") | ||
| 278 | self.connection.execute("PRAGMA journal_mode = WAL;") | ||
| 279 | self.connection.commit() | ||
| 280 | self._tables={} | ||
| 281 | |||
| 282 | def disconnect(self): | ||
| 283 | self.connection.commit() | ||
| 284 | self.connection.close() | ||
| 285 | |||
| 286 | def __getitem__(self, tblname): | ||
| 287 | if not isinstance(tblname, str): | ||
| 288 | raise TypeError("tblname argument must be a string, not '%s'" % | ||
| 289 | type(tblname)) | ||
| 290 | if tblname in self._tables: | ||
| 291 | return self._tables[tblname] | ||
| 292 | else: | ||
| 293 | tableobj = self._tables[tblname] = PRTable(self.connection, tblname, self.read_only) | ||
| 294 | return tableobj | ||
| 295 | |||
| 296 | def __delitem__(self, tblname): | ||
| 297 | if tblname in self._tables: | ||
| 298 | del self._tables[tblname] | ||
| 299 | logger.info("drop table %s" % (tblname)) | ||
| 300 | self.connection.execute("DROP TABLE IF EXISTS %s;" % tblname) | ||
| 301 | self.connection.commit() | ||
diff --git a/bitbake/lib/prserv/serv.py b/bitbake/lib/prserv/serv.py deleted file mode 100644 index e175886308..0000000000 --- a/bitbake/lib/prserv/serv.py +++ /dev/null | |||
| @@ -1,482 +0,0 @@ | |||
| 1 | # | ||
| 2 | # Copyright BitBake Contributors | ||
| 3 | # | ||
| 4 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 5 | # | ||
| 6 | |||
| 7 | import os,sys,logging | ||
| 8 | import signal, time | ||
| 9 | import socket | ||
| 10 | import io | ||
| 11 | import sqlite3 | ||
| 12 | import prserv | ||
| 13 | import prserv.db | ||
| 14 | import errno | ||
| 15 | from . import create_async_client, revision_smaller, increase_revision | ||
| 16 | import bb.asyncrpc | ||
| 17 | |||
| 18 | logger = logging.getLogger("BitBake.PRserv") | ||
| 19 | |||
| 20 | PIDPREFIX = "/tmp/PRServer_%s_%s.pid" | ||
| 21 | singleton = None | ||
| 22 | |||
| 23 | class PRServerClient(bb.asyncrpc.AsyncServerConnection): | ||
| 24 | def __init__(self, socket, server): | ||
| 25 | super().__init__(socket, "PRSERVICE", server.logger) | ||
| 26 | self.server = server | ||
| 27 | |||
| 28 | self.handlers.update({ | ||
| 29 | "get-pr": self.handle_get_pr, | ||
| 30 | "test-pr": self.handle_test_pr, | ||
| 31 | "test-package": self.handle_test_package, | ||
| 32 | "max-package-pr": self.handle_max_package_pr, | ||
| 33 | "import-one": self.handle_import_one, | ||
| 34 | "export": self.handle_export, | ||
| 35 | "is-readonly": self.handle_is_readonly, | ||
| 36 | }) | ||
| 37 | |||
| 38 | def validate_proto_version(self): | ||
| 39 | return (self.proto_version == (1, 0)) | ||
| 40 | |||
| 41 | async def dispatch_message(self, msg): | ||
| 42 | try: | ||
| 43 | return await super().dispatch_message(msg) | ||
| 44 | except: | ||
| 45 | raise | ||
| 46 | |||
| 47 | async def handle_test_pr(self, request): | ||
| 48 | '''Finds the PR value corresponding to the request. If not found, returns None and doesn't insert a new value''' | ||
| 49 | version = request["version"] | ||
| 50 | pkgarch = request["pkgarch"] | ||
| 51 | checksum = request["checksum"] | ||
| 52 | history = request["history"] | ||
| 53 | |||
| 54 | value = self.server.table.find_value(version, pkgarch, checksum, history) | ||
| 55 | return {"value": value} | ||
| 56 | |||
| 57 | async def handle_test_package(self, request): | ||
| 58 | '''Tells whether there are entries for (version, pkgarch) in the db. Returns True or False''' | ||
| 59 | version = request["version"] | ||
| 60 | pkgarch = request["pkgarch"] | ||
| 61 | |||
| 62 | value = self.server.table.test_package(version, pkgarch) | ||
| 63 | return {"value": value} | ||
| 64 | |||
| 65 | async def handle_max_package_pr(self, request): | ||
| 66 | '''Finds the greatest PR value for (version, pkgarch) in the db. Returns None if no entry was found''' | ||
| 67 | version = request["version"] | ||
| 68 | pkgarch = request["pkgarch"] | ||
| 69 | |||
| 70 | value = self.server.table.find_package_max_value(version, pkgarch) | ||
| 71 | return {"value": value} | ||
| 72 | |||
| 73 | async def handle_get_pr(self, request): | ||
| 74 | version = request["version"] | ||
| 75 | pkgarch = request["pkgarch"] | ||
| 76 | checksum = request["checksum"] | ||
| 77 | history = request["history"] | ||
| 78 | |||
| 79 | if self.upstream_client is None: | ||
| 80 | value = self.server.table.get_value(version, pkgarch, checksum, history) | ||
| 81 | return {"value": value} | ||
| 82 | |||
| 83 | # We have an upstream server. | ||
| 84 | # Check whether the local server already knows the requested configuration. | ||
| 85 | # If the configuration is a new one, the generated value we will add will | ||
| 86 | # depend on what's on the upstream server. That's why we're calling find_value() | ||
| 87 | # instead of get_value() directly. | ||
| 88 | |||
| 89 | value = self.server.table.find_value(version, pkgarch, checksum, history) | ||
| 90 | upstream_max = await self.upstream_client.max_package_pr(version, pkgarch) | ||
| 91 | |||
| 92 | if value is not None: | ||
| 93 | |||
| 94 | # The configuration is already known locally. | ||
| 95 | |||
| 96 | if history: | ||
| 97 | value = self.server.table.get_value(version, pkgarch, checksum, history) | ||
| 98 | else: | ||
| 99 | existing_value = value | ||
| 100 | # In "no history", we need to make sure the value doesn't decrease | ||
| 101 | # and is at least greater than the maximum upstream value | ||
| 102 | # and the maximum local value | ||
| 103 | |||
| 104 | local_max = self.server.table.find_package_max_value(version, pkgarch) | ||
| 105 | if revision_smaller(value, local_max): | ||
| 106 | value = increase_revision(local_max) | ||
| 107 | |||
| 108 | if revision_smaller(value, upstream_max): | ||
| 109 | # Ask upstream whether it knows the checksum | ||
| 110 | upstream_value = await self.upstream_client.test_pr(version, pkgarch, checksum) | ||
| 111 | if upstream_value is None: | ||
| 112 | # Upstream doesn't have our checksum, let create a new one | ||
| 113 | value = upstream_max + ".0" | ||
| 114 | else: | ||
| 115 | # Fine to take the same value as upstream | ||
| 116 | value = upstream_max | ||
| 117 | |||
| 118 | if not value == existing_value and not self.server.read_only: | ||
| 119 | self.server.table.store_value(version, pkgarch, checksum, value) | ||
| 120 | |||
| 121 | return {"value": value} | ||
| 122 | |||
| 123 | # The configuration is a new one for the local server | ||
| 124 | # Let's ask the upstream server whether it knows it | ||
| 125 | |||
| 126 | known_upstream = await self.upstream_client.test_package(version, pkgarch) | ||
| 127 | |||
| 128 | if not known_upstream: | ||
| 129 | |||
| 130 | # The package is not known upstream, must be a local-only package | ||
| 131 | # Let's compute the PR number using the local-only method | ||
| 132 | |||
| 133 | value = self.server.table.get_value(version, pkgarch, checksum, history) | ||
| 134 | return {"value": value} | ||
| 135 | |||
| 136 | # The package is known upstream, let's ask the upstream server | ||
| 137 | # whether it knows our new output hash | ||
| 138 | |||
| 139 | value = await self.upstream_client.test_pr(version, pkgarch, checksum) | ||
| 140 | |||
| 141 | if value is not None: | ||
| 142 | |||
| 143 | # Upstream knows this output hash, let's store it and use it too. | ||
| 144 | |||
| 145 | if not self.server.read_only: | ||
| 146 | self.server.table.store_value(version, pkgarch, checksum, value) | ||
| 147 | # If the local server is read only, won't be able to store the new | ||
| 148 | # value in the database and will have to keep asking the upstream server | ||
| 149 | return {"value": value} | ||
| 150 | |||
| 151 | # The output hash doesn't exist upstream, get the most recent number from upstream (x) | ||
| 152 | # Then, we want to have a new PR value for the local server: x.y | ||
| 153 | |||
| 154 | upstream_max = await self.upstream_client.max_package_pr(version, pkgarch) | ||
| 155 | # Here we know that the package is known upstream, so upstream_max can't be None | ||
| 156 | subvalue = self.server.table.find_new_subvalue(version, pkgarch, upstream_max) | ||
| 157 | |||
| 158 | if not self.server.read_only: | ||
| 159 | self.server.table.store_value(version, pkgarch, checksum, subvalue) | ||
| 160 | |||
| 161 | return {"value": subvalue} | ||
| 162 | |||
| 163 | async def process_requests(self): | ||
| 164 | if self.server.upstream is not None: | ||
| 165 | self.upstream_client = await create_async_client(self.server.upstream) | ||
| 166 | else: | ||
| 167 | self.upstream_client = None | ||
| 168 | |||
| 169 | try: | ||
| 170 | await super().process_requests() | ||
| 171 | finally: | ||
| 172 | if self.upstream_client is not None: | ||
| 173 | await self.upstream_client.close() | ||
| 174 | |||
| 175 | async def handle_import_one(self, request): | ||
| 176 | response = None | ||
| 177 | if not self.server.read_only: | ||
| 178 | version = request["version"] | ||
| 179 | pkgarch = request["pkgarch"] | ||
| 180 | checksum = request["checksum"] | ||
| 181 | value = request["value"] | ||
| 182 | |||
| 183 | value = self.server.table.importone(version, pkgarch, checksum, value) | ||
| 184 | if value is not None: | ||
| 185 | response = {"value": value} | ||
| 186 | |||
| 187 | return response | ||
| 188 | |||
| 189 | async def handle_export(self, request): | ||
| 190 | version = request["version"] | ||
| 191 | pkgarch = request["pkgarch"] | ||
| 192 | checksum = request["checksum"] | ||
| 193 | colinfo = request["colinfo"] | ||
| 194 | history = request["history"] | ||
| 195 | |||
| 196 | try: | ||
| 197 | (metainfo, datainfo) = self.server.table.export(version, pkgarch, checksum, colinfo, history) | ||
| 198 | except sqlite3.Error as exc: | ||
| 199 | self.logger.error(str(exc)) | ||
| 200 | metainfo = datainfo = None | ||
| 201 | |||
| 202 | return {"metainfo": metainfo, "datainfo": datainfo} | ||
| 203 | |||
| 204 | async def handle_is_readonly(self, request): | ||
| 205 | return {"readonly": self.server.read_only} | ||
| 206 | |||
| 207 | class PRServer(bb.asyncrpc.AsyncServer): | ||
| 208 | def __init__(self, dbfile, read_only=False, upstream=None): | ||
| 209 | super().__init__(logger) | ||
| 210 | self.dbfile = dbfile | ||
| 211 | self.table = None | ||
| 212 | self.read_only = read_only | ||
| 213 | self.upstream = upstream | ||
| 214 | |||
| 215 | def accept_client(self, socket): | ||
| 216 | return PRServerClient(socket, self) | ||
| 217 | |||
| 218 | def start(self): | ||
| 219 | tasks = super().start() | ||
| 220 | self.db = prserv.db.PRData(self.dbfile, read_only=self.read_only) | ||
| 221 | self.table = self.db["PRMAIN"] | ||
| 222 | |||
| 223 | self.logger.info("Started PRServer with DBfile: %s, Address: %s, PID: %s" % | ||
| 224 | (self.dbfile, self.address, str(os.getpid()))) | ||
| 225 | |||
| 226 | if self.upstream is not None: | ||
| 227 | self.logger.info("And upstream PRServer: %s " % (self.upstream)) | ||
| 228 | |||
| 229 | return tasks | ||
| 230 | |||
| 231 | async def stop(self): | ||
| 232 | self.db.disconnect() | ||
| 233 | await super().stop() | ||
| 234 | |||
| 235 | class PRServSingleton(object): | ||
| 236 | def __init__(self, dbfile, logfile, host, port, upstream): | ||
| 237 | self.dbfile = dbfile | ||
| 238 | self.logfile = logfile | ||
| 239 | self.host = host | ||
| 240 | self.port = port | ||
| 241 | self.upstream = upstream | ||
| 242 | |||
| 243 | def start(self): | ||
| 244 | self.prserv = PRServer(self.dbfile, upstream=self.upstream) | ||
| 245 | self.prserv.start_tcp_server(socket.gethostbyname(self.host), self.port) | ||
| 246 | self.process = self.prserv.serve_as_process(log_level=logging.WARNING) | ||
| 247 | |||
| 248 | if not self.prserv.address: | ||
| 249 | raise PRServiceConfigError | ||
| 250 | if not self.port: | ||
| 251 | self.port = int(self.prserv.address.rsplit(":", 1)[1]) | ||
| 252 | |||
| 253 | def run_as_daemon(func, pidfile, logfile): | ||
| 254 | """ | ||
| 255 | See Advanced Programming in the UNIX, Sec 13.3 | ||
| 256 | """ | ||
| 257 | try: | ||
| 258 | pid = os.fork() | ||
| 259 | if pid > 0: | ||
| 260 | os.waitpid(pid, 0) | ||
| 261 | #parent return instead of exit to give control | ||
| 262 | return pid | ||
| 263 | except OSError as e: | ||
| 264 | raise Exception("%s [%d]" % (e.strerror, e.errno)) | ||
| 265 | |||
| 266 | os.setsid() | ||
| 267 | """ | ||
| 268 | fork again to make sure the daemon is not session leader, | ||
| 269 | which prevents it from acquiring controlling terminal | ||
| 270 | """ | ||
| 271 | try: | ||
| 272 | pid = os.fork() | ||
| 273 | if pid > 0: #parent | ||
| 274 | os._exit(0) | ||
| 275 | except OSError as e: | ||
| 276 | raise Exception("%s [%d]" % (e.strerror, e.errno)) | ||
| 277 | |||
| 278 | os.chdir("/") | ||
| 279 | |||
| 280 | sys.stdout.flush() | ||
| 281 | sys.stderr.flush() | ||
| 282 | |||
| 283 | # We could be called from a python thread with io.StringIO as | ||
| 284 | # stdout/stderr or it could be 'real' unix fd forking where we need | ||
| 285 | # to physically close the fds to prevent the program launching us from | ||
| 286 | # potentially hanging on a pipe. Handle both cases. | ||
| 287 | si = open("/dev/null", "r") | ||
| 288 | try: | ||
| 289 | os.dup2(si.fileno(), sys.stdin.fileno()) | ||
| 290 | except (AttributeError, io.UnsupportedOperation): | ||
| 291 | sys.stdin = si | ||
| 292 | so = open(logfile, "a+") | ||
| 293 | try: | ||
| 294 | os.dup2(so.fileno(), sys.stdout.fileno()) | ||
| 295 | except (AttributeError, io.UnsupportedOperation): | ||
| 296 | sys.stdout = so | ||
| 297 | try: | ||
| 298 | os.dup2(so.fileno(), sys.stderr.fileno()) | ||
| 299 | except (AttributeError, io.UnsupportedOperation): | ||
| 300 | sys.stderr = so | ||
| 301 | |||
| 302 | # Clear out all log handlers prior to the fork() to avoid calling | ||
| 303 | # event handlers not part of the PRserver | ||
| 304 | for logger_iter in logging.Logger.manager.loggerDict.keys(): | ||
| 305 | logging.getLogger(logger_iter).handlers = [] | ||
| 306 | |||
| 307 | # Ensure logging makes it to the logfile | ||
| 308 | streamhandler = logging.StreamHandler() | ||
| 309 | streamhandler.setLevel(logging.DEBUG) | ||
| 310 | formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s") | ||
| 311 | streamhandler.setFormatter(formatter) | ||
| 312 | logger.addHandler(streamhandler) | ||
| 313 | |||
| 314 | # write pidfile | ||
| 315 | pid = str(os.getpid()) | ||
| 316 | with open(pidfile, "w") as pf: | ||
| 317 | pf.write("%s\n" % pid) | ||
| 318 | |||
| 319 | func() | ||
| 320 | os.remove(pidfile) | ||
| 321 | os._exit(0) | ||
| 322 | |||
| 323 | def start_daemon(dbfile, host, port, logfile, read_only=False, upstream=None): | ||
| 324 | ip = socket.gethostbyname(host) | ||
| 325 | pidfile = PIDPREFIX % (ip, port) | ||
| 326 | try: | ||
| 327 | with open(pidfile) as pf: | ||
| 328 | pid = int(pf.readline().strip()) | ||
| 329 | except IOError: | ||
| 330 | pid = None | ||
| 331 | |||
| 332 | if pid: | ||
| 333 | sys.stderr.write("pidfile %s already exist. Daemon already running?\n" | ||
| 334 | % pidfile) | ||
| 335 | return 1 | ||
| 336 | |||
| 337 | dbfile = os.path.abspath(dbfile) | ||
| 338 | def daemon_main(): | ||
| 339 | server = PRServer(dbfile, read_only=read_only, upstream=upstream) | ||
| 340 | server.start_tcp_server(ip, port) | ||
| 341 | server.serve_forever() | ||
| 342 | |||
| 343 | run_as_daemon(daemon_main, pidfile, os.path.abspath(logfile)) | ||
| 344 | return 0 | ||
| 345 | |||
| 346 | def stop_daemon(host, port): | ||
| 347 | import glob | ||
| 348 | ip = socket.gethostbyname(host) | ||
| 349 | pidfile = PIDPREFIX % (ip, port) | ||
| 350 | try: | ||
| 351 | with open(pidfile) as pf: | ||
| 352 | pid = int(pf.readline().strip()) | ||
| 353 | except IOError: | ||
| 354 | pid = None | ||
| 355 | |||
| 356 | if not pid: | ||
| 357 | # when server starts at port=0 (i.e. localhost:0), server actually takes another port, | ||
| 358 | # so at least advise the user which ports the corresponding server is listening | ||
| 359 | ports = [] | ||
| 360 | portstr = "" | ||
| 361 | for pf in glob.glob(PIDPREFIX % (ip, "*")): | ||
| 362 | bn = os.path.basename(pf) | ||
| 363 | root, _ = os.path.splitext(bn) | ||
| 364 | ports.append(root.split("_")[-1]) | ||
| 365 | if len(ports): | ||
| 366 | portstr = "Wrong port? Other ports listening at %s: %s" % (host, " ".join(ports)) | ||
| 367 | |||
| 368 | sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n" | ||
| 369 | % (pidfile, portstr)) | ||
| 370 | return 1 | ||
| 371 | |||
| 372 | try: | ||
| 373 | if is_running(pid): | ||
| 374 | print("Sending SIGTERM to pr-server.") | ||
| 375 | os.kill(pid, signal.SIGTERM) | ||
| 376 | time.sleep(0.1) | ||
| 377 | |||
| 378 | try: | ||
| 379 | os.remove(pidfile) | ||
| 380 | except FileNotFoundError: | ||
| 381 | # The PID file might have been removed by the exiting process | ||
| 382 | pass | ||
| 383 | |||
| 384 | except OSError as e: | ||
| 385 | err = str(e) | ||
| 386 | if err.find("No such process") <= 0: | ||
| 387 | raise e | ||
| 388 | |||
| 389 | return 0 | ||
| 390 | |||
| 391 | def is_running(pid): | ||
| 392 | try: | ||
| 393 | os.kill(pid, 0) | ||
| 394 | except OSError as err: | ||
| 395 | if err.errno == errno.ESRCH: | ||
| 396 | return False | ||
| 397 | return True | ||
| 398 | |||
| 399 | def is_local_special(host, port): | ||
| 400 | if (host == "localhost" or host == "127.0.0.1") and not port: | ||
| 401 | return True | ||
| 402 | else: | ||
| 403 | return False | ||
| 404 | |||
| 405 | class PRServiceConfigError(Exception): | ||
| 406 | pass | ||
| 407 | |||
| 408 | def auto_start(d): | ||
| 409 | global singleton | ||
| 410 | |||
| 411 | host_params = list(filter(None, (d.getVar("PRSERV_HOST") or "").split(":"))) | ||
| 412 | if not host_params: | ||
| 413 | # Shutdown any existing PR Server | ||
| 414 | auto_shutdown() | ||
| 415 | return None | ||
| 416 | |||
| 417 | if len(host_params) != 2: | ||
| 418 | # Shutdown any existing PR Server | ||
| 419 | auto_shutdown() | ||
| 420 | logger.critical("\n".join(["PRSERV_HOST: incorrect format", | ||
| 421 | 'Usage: PRSERV_HOST = "<hostname>:<port>"'])) | ||
| 422 | raise PRServiceConfigError | ||
| 423 | |||
| 424 | host = host_params[0].strip().lower() | ||
| 425 | port = int(host_params[1]) | ||
| 426 | |||
| 427 | upstream = d.getVar("PRSERV_UPSTREAM") or None | ||
| 428 | |||
| 429 | if is_local_special(host, port): | ||
| 430 | import bb.utils | ||
| 431 | cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE")) | ||
| 432 | if not cachedir: | ||
| 433 | logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable") | ||
| 434 | raise PRServiceConfigError | ||
| 435 | dbfile = os.path.join(cachedir, "prserv.sqlite3") | ||
| 436 | logfile = os.path.join(cachedir, "prserv.log") | ||
| 437 | if singleton: | ||
| 438 | if singleton.dbfile != dbfile: | ||
| 439 | # Shutdown any existing PR Server as doesn't match config | ||
| 440 | auto_shutdown() | ||
| 441 | if not singleton: | ||
| 442 | bb.utils.mkdirhier(cachedir) | ||
| 443 | singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), host, port, upstream) | ||
| 444 | singleton.start() | ||
| 445 | if singleton: | ||
| 446 | host = singleton.host | ||
| 447 | port = singleton.port | ||
| 448 | |||
| 449 | try: | ||
| 450 | ping(host, port) | ||
| 451 | return str(host) + ":" + str(port) | ||
| 452 | |||
| 453 | except Exception: | ||
| 454 | logger.critical("PRservice %s:%d not available" % (host, port)) | ||
| 455 | raise PRServiceConfigError | ||
| 456 | |||
| 457 | def auto_shutdown(): | ||
| 458 | global singleton | ||
| 459 | if singleton and singleton.process: | ||
| 460 | singleton.process.terminate() | ||
| 461 | singleton.process.join() | ||
| 462 | singleton = None | ||
| 463 | |||
| 464 | def ping(host, port): | ||
| 465 | from . import client | ||
| 466 | |||
| 467 | with client.PRClient() as conn: | ||
| 468 | conn.connect_tcp(host, port) | ||
| 469 | return conn.ping() | ||
| 470 | |||
| 471 | def connect(host, port): | ||
| 472 | from . import client | ||
| 473 | |||
| 474 | global singleton | ||
| 475 | |||
| 476 | if host.strip().lower() == "localhost" and not port: | ||
| 477 | host = "localhost" | ||
| 478 | port = singleton.port | ||
| 479 | |||
| 480 | conn = client.PRClient() | ||
| 481 | conn.connect_tcp(host, port) | ||
| 482 | return conn | ||
diff --git a/bitbake/lib/prserv/tests.py b/bitbake/lib/prserv/tests.py deleted file mode 100644 index df0c003003..0000000000 --- a/bitbake/lib/prserv/tests.py +++ /dev/null | |||
| @@ -1,388 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # Copyright (C) 2024 BitBake Contributors | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 6 | # | ||
| 7 | |||
| 8 | from . import create_server, create_client, increase_revision, revision_greater, revision_smaller, _revision_greater_or_equal | ||
| 9 | import prserv.db as db | ||
| 10 | from bb.asyncrpc import InvokeError | ||
| 11 | import logging | ||
| 12 | import os | ||
| 13 | import sys | ||
| 14 | import tempfile | ||
| 15 | import unittest | ||
| 16 | import socket | ||
| 17 | import subprocess | ||
| 18 | from pathlib import Path | ||
| 19 | |||
| 20 | THIS_DIR = Path(__file__).parent | ||
| 21 | BIN_DIR = THIS_DIR.parent.parent / "bin" | ||
| 22 | |||
| 23 | version = "dummy-1.0-r0" | ||
| 24 | pkgarch = "core2-64" | ||
| 25 | other_arch = "aarch64" | ||
| 26 | |||
| 27 | checksumX = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4f0" | ||
| 28 | checksum0 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a0" | ||
| 29 | checksum1 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a1" | ||
| 30 | checksum2 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a2" | ||
| 31 | checksum3 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a3" | ||
| 32 | checksum4 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a4" | ||
| 33 | checksum5 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a5" | ||
| 34 | checksum6 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a6" | ||
| 35 | checksum7 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a7" | ||
| 36 | checksum8 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a8" | ||
| 37 | checksum9 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a9" | ||
| 38 | checksum10 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4aa" | ||
| 39 | |||
| 40 | def server_prefunc(server, name): | ||
| 41 | logging.basicConfig(level=logging.DEBUG, filename='prserv-%s.log' % name, filemode='w', | ||
| 42 | format='%(levelname)s %(filename)s:%(lineno)d %(message)s') | ||
| 43 | server.logger.debug("Running server %s" % name) | ||
| 44 | sys.stdout = open('prserv-stdout-%s.log' % name, 'w') | ||
| 45 | sys.stderr = sys.stdout | ||
| 46 | |||
| 47 | class PRTestSetup(object): | ||
| 48 | |||
| 49 | def start_server(self, name, dbfile, upstream=None, read_only=False, prefunc=server_prefunc): | ||
| 50 | |||
| 51 | def cleanup_server(server): | ||
| 52 | if server.process.exitcode is not None: | ||
| 53 | return | ||
| 54 | server.process.terminate() | ||
| 55 | server.process.join() | ||
| 56 | |||
| 57 | server = create_server(socket.gethostbyname("localhost") + ":0", | ||
| 58 | dbfile, | ||
| 59 | upstream=upstream, | ||
| 60 | read_only=read_only) | ||
| 61 | |||
| 62 | server.serve_as_process(prefunc=prefunc, args=(name,)) | ||
| 63 | self.addCleanup(cleanup_server, server) | ||
| 64 | |||
| 65 | return server | ||
| 66 | |||
| 67 | def start_client(self, server_address): | ||
| 68 | def cleanup_client(client): | ||
| 69 | client.close() | ||
| 70 | |||
| 71 | client = create_client(server_address) | ||
| 72 | self.addCleanup(cleanup_client, client) | ||
| 73 | |||
| 74 | return client | ||
| 75 | |||
| 76 | class FunctionTests(unittest.TestCase): | ||
| 77 | |||
| 78 | def setUp(self): | ||
| 79 | self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv') | ||
| 80 | self.addCleanup(self.temp_dir.cleanup) | ||
| 81 | |||
| 82 | def test_increase_revision(self): | ||
| 83 | self.assertEqual(increase_revision("1"), "2") | ||
| 84 | self.assertEqual(increase_revision("1.0"), "1.1") | ||
| 85 | self.assertEqual(increase_revision("1.1.1"), "1.1.2") | ||
| 86 | self.assertEqual(increase_revision("1.1.1.3"), "1.1.1.4") | ||
| 87 | self.assertEqual(increase_revision("9"), "10") | ||
| 88 | self.assertEqual(increase_revision("1.9"), "1.10") | ||
| 89 | self.assertRaises(ValueError, increase_revision, "1.a") | ||
| 90 | self.assertRaises(ValueError, increase_revision, "1.") | ||
| 91 | self.assertRaises(ValueError, increase_revision, "") | ||
| 92 | |||
| 93 | def test_revision_greater_or_equal(self): | ||
| 94 | self.assertTrue(_revision_greater_or_equal("2", "2")) | ||
| 95 | self.assertTrue(_revision_greater_or_equal("2", "1")) | ||
| 96 | self.assertTrue(_revision_greater_or_equal("10", "2")) | ||
| 97 | self.assertTrue(_revision_greater_or_equal("1.10", "1.2")) | ||
| 98 | self.assertFalse(_revision_greater_or_equal("1.2", "1.10")) | ||
| 99 | self.assertTrue(_revision_greater_or_equal("1.10", "1")) | ||
| 100 | self.assertTrue(_revision_greater_or_equal("1.10.1", "1.10")) | ||
| 101 | self.assertFalse(_revision_greater_or_equal("1.10.1", "1.10.2")) | ||
| 102 | self.assertTrue(_revision_greater_or_equal("1.10.1", "1.10.1")) | ||
| 103 | self.assertTrue(_revision_greater_or_equal("1.10.1", "1")) | ||
| 104 | self.assertTrue(revision_greater("1.20", "1.3")) | ||
| 105 | self.assertTrue(revision_smaller("1.3", "1.20")) | ||
| 106 | |||
| 107 | # DB tests | ||
| 108 | |||
| 109 | def test_db(self): | ||
| 110 | dbfile = os.path.join(self.temp_dir.name, "testtable.sqlite3") | ||
| 111 | |||
| 112 | self.db = db.PRData(dbfile) | ||
| 113 | self.table = self.db["PRMAIN"] | ||
| 114 | |||
| 115 | self.table.store_value(version, pkgarch, checksum0, "0") | ||
| 116 | self.table.store_value(version, pkgarch, checksum1, "1") | ||
| 117 | # "No history" mode supports multiple PRs for the same checksum | ||
| 118 | self.table.store_value(version, pkgarch, checksum0, "2") | ||
| 119 | self.table.store_value(version, pkgarch, checksum2, "1.0") | ||
| 120 | |||
| 121 | self.assertTrue(self.table.test_package(version, pkgarch)) | ||
| 122 | self.assertFalse(self.table.test_package(version, other_arch)) | ||
| 123 | |||
| 124 | self.assertTrue(self.table.test_value(version, pkgarch, "0")) | ||
| 125 | self.assertTrue(self.table.test_value(version, pkgarch, "1")) | ||
| 126 | self.assertTrue(self.table.test_value(version, pkgarch, "2")) | ||
| 127 | |||
| 128 | self.assertEqual(self.table.find_package_max_value(version, pkgarch), "2") | ||
| 129 | |||
| 130 | self.assertEqual(self.table.find_min_value(version, pkgarch, checksum0), "0") | ||
| 131 | self.assertEqual(self.table.find_max_value(version, pkgarch, checksum0), "2") | ||
| 132 | |||
| 133 | # Test history modes | ||
| 134 | self.assertEqual(self.table.find_value(version, pkgarch, checksum0, True), "0") | ||
| 135 | self.assertEqual(self.table.find_value(version, pkgarch, checksum0, False), "2") | ||
| 136 | |||
| 137 | self.assertEqual(self.table.find_new_subvalue(version, pkgarch, "3"), "3.0") | ||
| 138 | self.assertEqual(self.table.find_new_subvalue(version, pkgarch, "1"), "1.1") | ||
| 139 | |||
| 140 | # Revision comparison tests | ||
| 141 | self.table.store_value(version, pkgarch, checksum1, "1.3") | ||
| 142 | self.table.store_value(version, pkgarch, checksum1, "1.20") | ||
| 143 | self.assertEqual(self.table.find_min_value(version, pkgarch, checksum1), "1") | ||
| 144 | self.assertEqual(self.table.find_max_value(version, pkgarch, checksum1), "1.20") | ||
| 145 | |||
| 146 | class PRBasicTests(PRTestSetup, unittest.TestCase): | ||
| 147 | |||
| 148 | def setUp(self): | ||
| 149 | self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv') | ||
| 150 | self.addCleanup(self.temp_dir.cleanup) | ||
| 151 | |||
| 152 | dbfile = os.path.join(self.temp_dir.name, "prtest-basic.sqlite3") | ||
| 153 | |||
| 154 | self.server1 = self.start_server("basic", dbfile) | ||
| 155 | self.client1 = self.start_client(self.server1.address) | ||
| 156 | |||
| 157 | def test_basic(self): | ||
| 158 | |||
| 159 | # Checks on non existing configuration | ||
| 160 | |||
| 161 | result = self.client1.test_pr(version, pkgarch, checksum0) | ||
| 162 | self.assertIsNone(result, "test_pr should return 'None' for a non existing PR") | ||
| 163 | |||
| 164 | result = self.client1.test_package(version, pkgarch) | ||
| 165 | self.assertFalse(result, "test_package should return 'False' for a non existing PR") | ||
| 166 | |||
| 167 | result = self.client1.max_package_pr(version, pkgarch) | ||
| 168 | self.assertIsNone(result, "max_package_pr should return 'None' for a non existing PR") | ||
| 169 | |||
| 170 | # Add a first configuration | ||
| 171 | |||
| 172 | result = self.client1.getPR(version, pkgarch, checksum0) | ||
| 173 | self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'") | ||
| 174 | |||
| 175 | result = self.client1.test_pr(version, pkgarch, checksum0) | ||
| 176 | self.assertEqual(result, "0", "test_pr should return '0' here, matching the result of getPR") | ||
| 177 | |||
| 178 | result = self.client1.test_package(version, pkgarch) | ||
| 179 | self.assertTrue(result, "test_package should return 'True' for an existing PR") | ||
| 180 | |||
| 181 | result = self.client1.max_package_pr(version, pkgarch) | ||
| 182 | self.assertEqual(result, "0", "max_package_pr should return '0' in the current test series") | ||
| 183 | |||
| 184 | # Check that the same request gets the same value | ||
| 185 | |||
| 186 | result = self.client1.getPR(version, pkgarch, checksum0) | ||
| 187 | self.assertEqual(result, "0", "getPR: asking for the same PR a second time in a row should return the same value.") | ||
| 188 | |||
| 189 | # Add new configurations | ||
| 190 | |||
| 191 | result = self.client1.getPR(version, pkgarch, checksum1) | ||
| 192 | self.assertEqual(result, "1", "getPR: second PR of a package should be '1'") | ||
| 193 | |||
| 194 | result = self.client1.test_pr(version, pkgarch, checksum1) | ||
| 195 | self.assertEqual(result, "1", "test_pr should return '1' here, matching the result of getPR") | ||
| 196 | |||
| 197 | result = self.client1.max_package_pr(version, pkgarch) | ||
| 198 | self.assertEqual(result, "1", "max_package_pr should return '1' in the current test series") | ||
| 199 | |||
| 200 | result = self.client1.getPR(version, pkgarch, checksum2) | ||
| 201 | self.assertEqual(result, "2", "getPR: second PR of a package should be '2'") | ||
| 202 | |||
| 203 | result = self.client1.test_pr(version, pkgarch, checksum2) | ||
| 204 | self.assertEqual(result, "2", "test_pr should return '2' here, matching the result of getPR") | ||
| 205 | |||
| 206 | result = self.client1.max_package_pr(version, pkgarch) | ||
| 207 | self.assertEqual(result, "2", "max_package_pr should return '2' in the current test series") | ||
| 208 | |||
| 209 | result = self.client1.getPR(version, pkgarch, checksum3) | ||
| 210 | self.assertEqual(result, "3", "getPR: second PR of a package should be '3'") | ||
| 211 | |||
| 212 | result = self.client1.test_pr(version, pkgarch, checksum3) | ||
| 213 | self.assertEqual(result, "3", "test_pr should return '3' here, matching the result of getPR") | ||
| 214 | |||
| 215 | result = self.client1.max_package_pr(version, pkgarch) | ||
| 216 | self.assertEqual(result, "3", "max_package_pr should return '3' in the current test series") | ||
| 217 | |||
| 218 | # Ask again for the first configuration | ||
| 219 | |||
| 220 | result = self.client1.getPR(version, pkgarch, checksum0) | ||
| 221 | self.assertEqual(result, "4", "getPR: should return '4' in this configuration") | ||
| 222 | |||
| 223 | # Ask again with explicit "no history" mode | ||
| 224 | |||
| 225 | result = self.client1.getPR(version, pkgarch, checksum0, False) | ||
| 226 | self.assertEqual(result, "4", "getPR: should return '4' in this configuration") | ||
| 227 | |||
| 228 | # Ask again with explicit "history" mode. This should return the first recorded PR for checksum0 | ||
| 229 | |||
| 230 | result = self.client1.getPR(version, pkgarch, checksum0, True) | ||
| 231 | self.assertEqual(result, "0", "getPR: should return '0' in this configuration") | ||
| 232 | |||
| 233 | # Check again that another pkgarg resets the counters | ||
| 234 | |||
| 235 | result = self.client1.test_pr(version, other_arch, checksum0) | ||
| 236 | self.assertIsNone(result, "test_pr should return 'None' for a non existing PR") | ||
| 237 | |||
| 238 | result = self.client1.test_package(version, other_arch) | ||
| 239 | self.assertFalse(result, "test_package should return 'False' for a non existing PR") | ||
| 240 | |||
| 241 | result = self.client1.max_package_pr(version, other_arch) | ||
| 242 | self.assertIsNone(result, "max_package_pr should return 'None' for a non existing PR") | ||
| 243 | |||
| 244 | # Now add the configuration | ||
| 245 | |||
| 246 | result = self.client1.getPR(version, other_arch, checksum0) | ||
| 247 | self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'") | ||
| 248 | |||
| 249 | result = self.client1.test_pr(version, other_arch, checksum0) | ||
| 250 | self.assertEqual(result, "0", "test_pr should return '0' here, matching the result of getPR") | ||
| 251 | |||
| 252 | result = self.client1.test_package(version, other_arch) | ||
| 253 | self.assertTrue(result, "test_package should return 'True' for an existing PR") | ||
| 254 | |||
| 255 | result = self.client1.max_package_pr(version, other_arch) | ||
| 256 | self.assertEqual(result, "0", "max_package_pr should return '0' in the current test series") | ||
| 257 | |||
| 258 | result = self.client1.is_readonly() | ||
| 259 | self.assertFalse(result, "Server should not be described as 'read-only'") | ||
| 260 | |||
| 261 | class PRUpstreamTests(PRTestSetup, unittest.TestCase): | ||
| 262 | |||
| 263 | def setUp(self): | ||
| 264 | |||
| 265 | self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv') | ||
| 266 | self.addCleanup(self.temp_dir.cleanup) | ||
| 267 | |||
| 268 | dbfile2 = os.path.join(self.temp_dir.name, "prtest-upstream2.sqlite3") | ||
| 269 | self.server2 = self.start_server("upstream2", dbfile2) | ||
| 270 | self.client2 = self.start_client(self.server2.address) | ||
| 271 | |||
| 272 | dbfile1 = os.path.join(self.temp_dir.name, "prtest-upstream1.sqlite3") | ||
| 273 | self.server1 = self.start_server("upstream1", dbfile1, upstream=self.server2.address) | ||
| 274 | self.client1 = self.start_client(self.server1.address) | ||
| 275 | |||
| 276 | dbfile0 = os.path.join(self.temp_dir.name, "prtest-local.sqlite3") | ||
| 277 | self.server0 = self.start_server("local", dbfile0, upstream=self.server1.address) | ||
| 278 | self.client0 = self.start_client(self.server0.address) | ||
| 279 | self.shared_db = dbfile0 | ||
| 280 | |||
| 281 | def test_upstream_and_readonly(self): | ||
| 282 | |||
| 283 | # For identical checksums, all servers should return the same PR | ||
| 284 | |||
| 285 | result = self.client2.getPR(version, pkgarch, checksum0) | ||
| 286 | self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'") | ||
| 287 | |||
| 288 | result = self.client1.getPR(version, pkgarch, checksum0) | ||
| 289 | self.assertEqual(result, "0", "getPR: initial PR of a package should be '0' (same as upstream)") | ||
| 290 | |||
| 291 | result = self.client0.getPR(version, pkgarch, checksum0) | ||
| 292 | self.assertEqual(result, "0", "getPR: initial PR of a package should be '0' (same as upstream)") | ||
| 293 | |||
| 294 | # Now introduce new checksums on server1 for, same version | ||
| 295 | |||
| 296 | result = self.client1.getPR(version, pkgarch, checksum1) | ||
| 297 | self.assertEqual(result, "0.0", "getPR: first PR of a package which has a different checksum upstream should be '0.0'") | ||
| 298 | |||
| 299 | result = self.client1.getPR(version, pkgarch, checksum2) | ||
| 300 | self.assertEqual(result, "0.1", "getPR: second PR of a package that has a different checksum upstream should be '0.1'") | ||
| 301 | |||
| 302 | # Now introduce checksums on server0 for, same version | ||
| 303 | |||
| 304 | result = self.client1.getPR(version, pkgarch, checksum1) | ||
| 305 | self.assertEqual(result, "0.2", "getPR: can't decrease for known PR") | ||
| 306 | |||
| 307 | result = self.client1.getPR(version, pkgarch, checksum2) | ||
| 308 | self.assertEqual(result, "0.3") | ||
| 309 | |||
| 310 | result = self.client1.max_package_pr(version, pkgarch) | ||
| 311 | self.assertEqual(result, "0.3") | ||
| 312 | |||
| 313 | result = self.client0.getPR(version, pkgarch, checksum3) | ||
| 314 | self.assertEqual(result, "0.3.0", "getPR: first PR of a package that doesn't exist upstream should be '0.3.0'") | ||
| 315 | |||
| 316 | result = self.client0.getPR(version, pkgarch, checksum4) | ||
| 317 | self.assertEqual(result, "0.3.1", "getPR: second PR of a package that doesn't exist upstream should be '0.3.1'") | ||
| 318 | |||
| 319 | result = self.client0.getPR(version, pkgarch, checksum3) | ||
| 320 | self.assertEqual(result, "0.3.2") | ||
| 321 | |||
| 322 | # More upstream updates | ||
| 323 | # Here, we assume no communication between server2 and server0. server2 only impacts server0 | ||
| 324 | # after impacting server1 | ||
| 325 | |||
| 326 | self.assertEqual(self.client2.getPR(version, pkgarch, checksum5), "1") | ||
| 327 | self.assertEqual(self.client1.getPR(version, pkgarch, checksum6), "1.0") | ||
| 328 | self.assertEqual(self.client1.getPR(version, pkgarch, checksum7), "1.1") | ||
| 329 | self.assertEqual(self.client0.getPR(version, pkgarch, checksum8), "1.1.0") | ||
| 330 | self.assertEqual(self.client0.getPR(version, pkgarch, checksum9), "1.1.1") | ||
| 331 | |||
| 332 | # "history" mode tests | ||
| 333 | |||
| 334 | self.assertEqual(self.client2.getPR(version, pkgarch, checksum0, True), "0") | ||
| 335 | self.assertEqual(self.client1.getPR(version, pkgarch, checksum2, True), "0.1") | ||
| 336 | self.assertEqual(self.client0.getPR(version, pkgarch, checksum3, True), "0.3.0") | ||
| 337 | |||
| 338 | # More "no history" mode tests | ||
| 339 | |||
| 340 | self.assertEqual(self.client2.getPR(version, pkgarch, checksum0), "2") | ||
| 341 | self.assertEqual(self.client1.getPR(version, pkgarch, checksum0), "2") # Same as upstream | ||
| 342 | self.assertEqual(self.client0.getPR(version, pkgarch, checksum0), "2") # Same as upstream | ||
| 343 | self.assertEqual(self.client1.getPR(version, pkgarch, checksum7), "3") # This could be surprising, but since the previous revision was "2", increasing it yields "3". | ||
| 344 | # We don't know how many upstream servers we have | ||
| 345 | # Start read-only server with server1 as upstream | ||
| 346 | self.server_ro = self.start_server("local-ro", self.shared_db, upstream=self.server1.address, read_only=True) | ||
| 347 | self.client_ro = self.start_client(self.server_ro.address) | ||
| 348 | |||
| 349 | self.assertTrue(self.client_ro.is_readonly(), "Database should be described as 'read-only'") | ||
| 350 | |||
| 351 | # Checks on non existing configurations | ||
| 352 | self.assertIsNone(self.client_ro.test_pr(version, pkgarch, checksumX)) | ||
| 353 | self.assertFalse(self.client_ro.test_package("unknown", pkgarch)) | ||
| 354 | |||
| 355 | # Look up existing configurations | ||
| 356 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum0), "3") # "no history" mode | ||
| 357 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum0, True), "0") # "history" mode | ||
| 358 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum3), "3") | ||
| 359 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum3, True), "0.3.0") | ||
| 360 | self.assertEqual(self.client_ro.max_package_pr(version, pkgarch), "2") # normal as "3" was never saved | ||
| 361 | |||
| 362 | # Try to insert a new value. Here this one is know upstream. | ||
| 363 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum7), "3") | ||
| 364 | # Try to insert a completely new value. As the max upstream value is already "3", it should be "3.0" | ||
| 365 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum10), "3.0") | ||
| 366 | # Same with another value which only exists in the upstream upstream server | ||
| 367 | # This time, as the upstream server doesn't know it, it will ask its upstream server. So that's a known one. | ||
| 368 | self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum9), "3") | ||
| 369 | |||
| 370 | class ScriptTests(unittest.TestCase): | ||
| 371 | |||
| 372 | def setUp(self): | ||
| 373 | |||
| 374 | self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv') | ||
| 375 | self.addCleanup(self.temp_dir.cleanup) | ||
| 376 | self.dbfile = os.path.join(self.temp_dir.name, "prtest.sqlite3") | ||
| 377 | |||
| 378 | def test_1_start_bitbake_prserv(self): | ||
| 379 | try: | ||
| 380 | subprocess.check_call([BIN_DIR / "bitbake-prserv", "--start", "-f", self.dbfile]) | ||
| 381 | except subprocess.CalledProcessError as e: | ||
| 382 | self.fail("Failed to start bitbake-prserv: %s" % e.returncode) | ||
| 383 | |||
| 384 | def test_2_stop_bitbake_prserv(self): | ||
| 385 | try: | ||
| 386 | subprocess.check_call([BIN_DIR / "bitbake-prserv", "--stop"]) | ||
| 387 | except subprocess.CalledProcessError as e: | ||
| 388 | self.fail("Failed to stop bitbake-prserv: %s" % e.returncode) | ||
