|
| 1 | +#!/usr/bin/python3 |
| 2 | + |
| 3 | +""" |
| 4 | +This Python script allows you to list the |
| 5 | +latest version numbers of Scylla and Cassandra. |
| 6 | +
|
| 7 | +You can specify whether you want the |
| 8 | +versions of Scylla OSS or Scylla Enterprise, |
| 9 | +either N latest stable X.Y.latest or |
| 10 | +all non-obsolete RCs. You can also fetch |
| 11 | +the latest version of Cassandra 3. |
| 12 | +
|
| 13 | +How are those versions fetched? We use Docker Hub |
| 14 | +tags API. |
| 15 | +""" |
| 16 | + |
| 17 | +import requests |
| 18 | +import re |
| 19 | +import json |
| 20 | +from itertools import groupby, islice |
| 21 | +import sys |
| 22 | + |
| 23 | +DOCKER_HUB_TAGS_ENDPOINT = 'https://registry.hub.docker.com/v1/repositories/%s/tags' |
| 24 | +DOCKER_HUB_SCYLLA_ORG = 'scylladb/' |
| 25 | + |
| 26 | +SCYLLA_OSS = DOCKER_HUB_SCYLLA_ORG + 'scylla' |
| 27 | +SCYLLA_OSS_RELEASED_VERSION_REGEX = re.compile(r'(\d+)\.(\d+)\.(\d+)') |
| 28 | +SCYLLA_OSS_RC_VERSION_REGEX = re.compile(r'(\d+)\.(\d+)\.rc(\d+)') |
| 29 | + |
| 30 | +SCYLLA_ENTERPRISE = DOCKER_HUB_SCYLLA_ORG + 'scylla-enterprise' |
| 31 | +SCYLLA_ENTERPRISE_RELEASED_VERSION_REGEX = re.compile(r'(\d{4})\.(\d+)\.(\d+)') |
| 32 | +SCYLLA_ENTERPRISE_RC_VERSION_REGEX = re.compile(r'(\d{4})\.(\d+)\.rc(\d+)') |
| 33 | + |
| 34 | +CASSANDRA_ENDPOINT = 'https://dlcdn.apache.org/cassandra/' |
| 35 | + |
| 36 | +CASSANDRA3_REGEX = re.compile(r'a href="(3)\.(\d+)\.(\d+)/"') |
| 37 | + |
| 38 | +COMMAND_LINE_ARGUMENT = re.compile( |
| 39 | + r'((?:(scylla-oss-stable):(\d+))|(?:(scylla-enterprise-stable):(\d+))|(?:(cassandra3-stable):(\d+))|(?:(scylla-oss-rc))|(?:(scylla-enterprise-rc)))') |
| 40 | + |
| 41 | + |
| 42 | +def fetch_last_scylla_oss_minor_versions(count): |
| 43 | + # Download Docker tags for repository |
| 44 | + tags_data = requests.get(DOCKER_HUB_TAGS_ENDPOINT % (SCYLLA_OSS)).json() |
| 45 | + tags_data = map(lambda e: e['name'], tags_data) |
| 46 | + |
| 47 | + # Parse only those tags which match 'NUM.NUM.NUM' |
| 48 | + # into tuple (NUM, NUM, NUM) |
| 49 | + tags_data = filter(SCYLLA_OSS_RELEASED_VERSION_REGEX.fullmatch, tags_data) |
| 50 | + tags_data = map(lambda e: SCYLLA_OSS_RELEASED_VERSION_REGEX.match( |
| 51 | + e).groups(), tags_data) |
| 52 | + tags_data = map(lambda e: tuple(map(int, e)), tags_data) |
| 53 | + |
| 54 | + # Group by (major, minor) and select latest patch version |
| 55 | + tags_data = sorted(tags_data) |
| 56 | + tags_data = groupby(tags_data, key=lambda e: (e[0], e[1])) |
| 57 | + tags_data = ((e[0][0], e[0][1], max(e[1])[2]) |
| 58 | + for e in tags_data) |
| 59 | + |
| 60 | + # Return the latest ones |
| 61 | + tags_data = list(tags_data)[-count:] |
| 62 | + tags_data = [f'{e[0]}.{e[1]}.{e[2]}' for e in tags_data] |
| 63 | + return tags_data |
| 64 | + |
| 65 | + |
| 66 | +def fetch_all_scylla_oss_rc_versions(): |
| 67 | + # Download Docker tags for repository |
| 68 | + tags_data = requests.get(DOCKER_HUB_TAGS_ENDPOINT % (SCYLLA_OSS)).json() |
| 69 | + tags_data = list(map(lambda e: e['name'], tags_data)) |
| 70 | + |
| 71 | + # Parse only those tags which match 'NUM.NUM.rcNUM' |
| 72 | + # into tuple (NUM, NUM, NUM) |
| 73 | + rc_tags_data = filter(SCYLLA_OSS_RC_VERSION_REGEX.fullmatch, tags_data) |
| 74 | + rc_tags_data = map(lambda e: SCYLLA_OSS_RC_VERSION_REGEX.match( |
| 75 | + e).groups(), rc_tags_data) |
| 76 | + rc_tags_data = map(lambda e: tuple(map(int, e)), rc_tags_data) |
| 77 | + |
| 78 | + # Parse only those tags which match 'NUM.NUM.NUM' |
| 79 | + # into tuple (NUM, NUM) |
| 80 | + stable_tags_data = filter( |
| 81 | + SCYLLA_OSS_RELEASED_VERSION_REGEX.fullmatch, tags_data) |
| 82 | + stable_tags_data = map(lambda e: SCYLLA_OSS_RELEASED_VERSION_REGEX.match( |
| 83 | + e).groups(), stable_tags_data) |
| 84 | + stable_tags_data = map(lambda e: tuple(map(int, e[0:2])), stable_tags_data) |
| 85 | + stable_tags_data = set(stable_tags_data) |
| 86 | + |
| 87 | + # Group by (major, minor) and select latest RC version |
| 88 | + rc_tags_data = sorted(rc_tags_data) |
| 89 | + rc_tags_data = groupby(rc_tags_data, key=lambda e: (e[0], e[1])) |
| 90 | + rc_tags_data = ((e[0][0], e[0][1], max(e[1])[2]) |
| 91 | + for e in rc_tags_data) |
| 92 | + |
| 93 | + # Filter out those RCs that are obsoleted by released stable version |
| 94 | + rc_tags_data = filter(lambda e: ( |
| 95 | + e[0], e[1]) not in stable_tags_data, rc_tags_data) |
| 96 | + rc_tags_data = [f'{e[0]}.{e[1]}.rc{e[2]}' for e in rc_tags_data] |
| 97 | + return rc_tags_data |
| 98 | + |
| 99 | + |
| 100 | +def fetch_last_scylla_enterprise_minor_versions(count): |
| 101 | + # Download Docker tags for repository |
| 102 | + tags_data = requests.get(DOCKER_HUB_TAGS_ENDPOINT % |
| 103 | + (SCYLLA_ENTERPRISE)).json() |
| 104 | + tags_data = map(lambda e: e['name'], tags_data) |
| 105 | + |
| 106 | + # Parse only those tags which match 'YEAR.NUM.NUM' |
| 107 | + # into tuple (YEAR, NUM, NUM) |
| 108 | + tags_data = filter( |
| 109 | + SCYLLA_ENTERPRISE_RELEASED_VERSION_REGEX.fullmatch, tags_data) |
| 110 | + tags_data = map(lambda e: SCYLLA_ENTERPRISE_RELEASED_VERSION_REGEX.match( |
| 111 | + e).groups(), tags_data) |
| 112 | + tags_data = map(lambda e: tuple(map(int, e)), tags_data) |
| 113 | + |
| 114 | + # Group by (major, minor) and select latest patch version |
| 115 | + tags_data = sorted(tags_data) |
| 116 | + tags_data = groupby(tags_data, key=lambda e: (e[0], e[1])) |
| 117 | + tags_data = ((e[0][0], e[0][1], max(e[1])[2]) |
| 118 | + for e in tags_data) |
| 119 | + |
| 120 | + # Return the latest ones |
| 121 | + tags_data = list(tags_data)[-count:] |
| 122 | + tags_data = [f'{e[0]}.{e[1]}.{e[2]}' for e in tags_data] |
| 123 | + return tags_data |
| 124 | + |
| 125 | + |
| 126 | +def fetch_all_scylla_enterprise_rc_versions(): |
| 127 | + # Download Docker tags for repository |
| 128 | + tags_data = requests.get(DOCKER_HUB_TAGS_ENDPOINT % |
| 129 | + (SCYLLA_ENTERPRISE)).json() |
| 130 | + tags_data = list(map(lambda e: e['name'], tags_data)) |
| 131 | + |
| 132 | + # Parse only those tags which match 'YEAR.NUM.rcNUM' |
| 133 | + # into tuple (YEAR, NUM, NUM) |
| 134 | + rc_tags_data = filter( |
| 135 | + SCYLLA_ENTERPRISE_RC_VERSION_REGEX.fullmatch, tags_data) |
| 136 | + rc_tags_data = map(lambda e: SCYLLA_ENTERPRISE_RC_VERSION_REGEX.match( |
| 137 | + e).groups(), rc_tags_data) |
| 138 | + rc_tags_data = map(lambda e: tuple(map(int, e)), rc_tags_data) |
| 139 | + |
| 140 | + # Parse only those tags which match 'YEAR.NUM.NUM' |
| 141 | + # into tuple (YEAR, NUM) |
| 142 | + stable_tags_data = filter( |
| 143 | + SCYLLA_ENTERPRISE_RELEASED_VERSION_REGEX.fullmatch, tags_data) |
| 144 | + stable_tags_data = map(lambda e: SCYLLA_ENTERPRISE_RELEASED_VERSION_REGEX.match( |
| 145 | + e).groups(), stable_tags_data) |
| 146 | + stable_tags_data = map(lambda e: tuple(map(int, e[0:2])), stable_tags_data) |
| 147 | + |
| 148 | + # Group by (major, minor) and select latest RC version |
| 149 | + rc_tags_data = sorted(rc_tags_data) |
| 150 | + rc_tags_data = groupby(rc_tags_data, key=lambda e: (e[0], e[1])) |
| 151 | + rc_tags_data = ((e[0][0], e[0][1], max(e[1])[2]) |
| 152 | + for e in rc_tags_data) |
| 153 | + |
| 154 | + # Filter out those RCs that are obsoleted by released stable version |
| 155 | + rc_tags_data = filter(lambda e: ( |
| 156 | + e[0], e[1]) not in stable_tags_data, rc_tags_data) |
| 157 | + rc_tags_data = [f'{e[0]}.{e[1]}.rc{e[2]}' for e in rc_tags_data] |
| 158 | + return rc_tags_data |
| 159 | + |
| 160 | + |
| 161 | +def fetch_last_cassandra3_minor_versions(count): |
| 162 | + # Download folder listing for Cassandra download site |
| 163 | + data = requests.get(CASSANDRA_ENDPOINT).text |
| 164 | + |
| 165 | + # Parse only those version numbers which match '3.NUM.NUM' |
| 166 | + # into tuple (3, NUM, NUM) |
| 167 | + data = CASSANDRA3_REGEX.finditer(data) |
| 168 | + data = map(lambda e: e.groups(), data) |
| 169 | + data = map(lambda e: tuple(map(int, e)), data) |
| 170 | + |
| 171 | + # Group by (major, minor) and select latest patch version |
| 172 | + data = sorted(data) |
| 173 | + data = groupby(data, key=lambda e: (e[0], e[1])) |
| 174 | + data = ((e[0][0], e[0][1], max(e[1])[2]) |
| 175 | + for e in data) |
| 176 | + |
| 177 | + # Return the latest ones |
| 178 | + data = list(data)[-count:] |
| 179 | + data = [f'{e[0]}.{e[1]}.{e[2]}' for e in data] |
| 180 | + return data |
| 181 | + |
| 182 | + |
| 183 | +if __name__ == '__main__': |
| 184 | + names = set() |
| 185 | + |
| 186 | + for arg in sys.argv[1:]: |
| 187 | + if not COMMAND_LINE_ARGUMENT.fullmatch(arg): |
| 188 | + print("Usage:", sys.argv[0], "[scylla-oss-stable:COUNT] [scylla-oss-rc] [scylla-enterprise-stable:COUNT] [scylla-enterprise-rc] [cassandra3-stable:COUNT]...", file=sys.stderr) |
| 189 | + sys.exit(1) |
| 190 | + |
| 191 | + groups = COMMAND_LINE_ARGUMENT.match(arg).groups() |
| 192 | + groups = [g for g in groups if g][1:] |
| 193 | + |
| 194 | + mode_name = groups[0] |
| 195 | + if mode_name == 'scylla-oss-stable': |
| 196 | + names.update(fetch_last_scylla_oss_minor_versions(int(groups[1]))) |
| 197 | + elif mode_name == 'scylla-enterprise-stable': |
| 198 | + names.update( |
| 199 | + fetch_last_scylla_enterprise_minor_versions(int(groups[1]))) |
| 200 | + elif mode_name == 'cassandra3-stable': |
| 201 | + names.update( |
| 202 | + fetch_last_cassandra3_minor_versions(int(groups[1]))) |
| 203 | + elif mode_name == 'scylla-oss-rc': |
| 204 | + names.update(fetch_all_scylla_oss_rc_versions()) |
| 205 | + elif mode_name == 'scylla-enterprise-rc': |
| 206 | + names.update(fetch_all_scylla_enterprise_rc_versions()) |
| 207 | + |
| 208 | + print(json.dumps(list(names))) |
0 commit comments