| #!/usr/bin/python -tt |
| ## |
| # Copyright (C) 2013 by Kernel.org |
| # |
| # This program is free software; you can redistribute it and/or |
| # modify it under the terms of the GNU General Public License |
| # as published by the Free Software Foundation; either version 2 |
| # of the License, or (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA |
| # 02111-1307, USA. |
| # |
| import os |
| import time |
| import datetime |
| import re |
| |
| from git import Repo |
| |
| import requests |
| |
| from distutils.version import StrictVersion |
| |
| from pelican import signals, utils |
| |
| from feedgenerator import Rss201rev2Feed |
| import json |
| |
| # Pelican plugin API |
| def register(): |
| signals.article_generator_init.connect(kernel_releases_init) |
| signals.article_generate_context.connect(fetch_kernel_releases) |
| |
| |
| def kernel_releases_init(gen): |
| gen.releases_instance = KernelReleases(gen) |
| |
| |
| def fetch_kernel_releases(gen, metadata): |
| gen.context['current_releases'] = gen.releases_instance.current_releases |
| gen.context['latest_release'] = gen.releases_instance.latest_release |
| |
| class KernelReleases(): |
| def __init__(self, generator): |
| self.release_tracker = '/var/lib/mirror/release-tracker.json' |
| |
| self.rss_path = os.path.join(generator.output_path, 'feeds', 'kdist.xml') |
| self.finger_path = os.path.join(generator.output_path, 'finger_banner') |
| self.json_path = os.path.join(generator.output_path, 'releases.json') |
| |
| self.reqs = requests.Session() |
| |
| settings = generator.settings |
| |
| GIT_MAINLINE = settings.get('GIT_MAINLINE') |
| GIT_STABLE = settings.get('GIT_STABLE') |
| GIT_NEXT = settings.get('GIT_NEXT') |
| |
| LONGTERM_KERNELS = settings.get('LONGTERM_KERNELS') |
| EOL_KERNELS = settings.get('EOL_KERNELS') |
| |
| repo = Repo(GIT_MAINLINE) |
| tagrefs = self.get_tagref_list(repo) |
| |
| rc_reg = re.compile('v.*-rc\d+$') |
| mainline_rc = self.find_latest_matching(tagrefs, rc_reg) |
| rel_reg = re.compile('v[^-]*\d+$') |
| mainline_rel = self.find_latest_matching(tagrefs, rel_reg) |
| |
| # if mainline_rel is newer than mainline_rc, ignore mainline_rc |
| if mainline_rc[1] < mainline_rel[1]: |
| mainline_rc = None |
| |
| # Move on to the linux-stable repo |
| repo = Repo(GIT_STABLE) |
| # ignore any tags older than 12 months |
| cutoff = time.time() - 31536000 |
| |
| # EOL cutoff is 30 days |
| eol_cutoff = time.time() - 2592000 |
| |
| tagrefs = self.get_tagref_list(repo, cutoff) |
| |
| # find all tags matching vX.X.X |
| rel_reg = re.compile('v\d+\.\d+\.\d+$') |
| matched = [] |
| for tagref in tagrefs: |
| if rel_reg.match(tagref.name): |
| matched.append(tagref) |
| |
| stable = [] |
| seen = [] |
| |
| for tagref in matched: |
| # does it match a longterm release? |
| ignore = False |
| for ver in LONGTERM_KERNELS: |
| if tagref.name.find(ver+'.') == 1: |
| # this is a long-term release, ignore |
| ignore = True |
| continue |
| if ignore: |
| continue |
| |
| # Drop the final \.\d+ and find the latest matching |
| components = tagref.name.split('.') |
| regex = re.compile('^' + '\\.'.join(components[:-1])) |
| |
| if regex in seen: |
| continue |
| |
| latest_matching = self.find_latest_matching(matched, regex) |
| if latest_matching is not None: |
| stable.append(latest_matching) |
| |
| seen.append(regex) |
| |
| stable = sorted(stable, key=lambda x: StrictVersion(x[0][1:]), reverse=True) |
| |
| releases = [] |
| |
| if mainline_rc is not None: |
| releases.append(self.make_release_line(mainline_rc, 'mainline')) |
| |
| # look at the last stable and see if mainline_rel fits into it |
| latest_stable = stable.pop(0) |
| |
| if latest_stable[0].find(mainline_rel[0]) != 0: |
| releases.append(self.make_release_line(mainline_rel, 'mainline')) |
| latest = self.make_release_line(mainline_rel, 'latest') |
| else: |
| latest = self.make_release_line(latest_stable, 'latest') |
| # if latest stable is newer than latest mainline, but there are no |
| # -rc kernels, list the latest mainline in the releases table anyway. |
| if mainline_rc is None: |
| releases.append(self.make_release_line(mainline_rel, 'mainline')) |
| |
| releases.append(self.make_release_line(latest_stable, 'stable')) |
| |
| # add other stable kernels and mark EOL accordingly |
| for rel in stable: |
| iseol = False |
| for eolrel in EOL_KERNELS: |
| if rel[0].find(eolrel) == 1: |
| iseol = True |
| break |
| |
| # Make sure EOL kernels stick around for max 30 days |
| if iseol and rel[1] < eol_cutoff: |
| continue |
| |
| releases.append(self.make_release_line(rel, 'stable', iseol)) |
| |
| # find latest long-term releases |
| for rel in LONGTERM_KERNELS: |
| components = rel.split('.') |
| regex = re.compile('^v' + '\\.'.join(components) + '\\.\\d+$') |
| found = self.find_latest_matching(tagrefs, regex) |
| if found is not None: |
| iseol = False |
| for eolrel in EOL_KERNELS: |
| if found[0].find(eolrel) == 1: |
| iseol = True |
| break |
| |
| if iseol and found[1] < eol_cutoff: |
| # Too old to list on the front page |
| continue |
| |
| releases.append(self.make_release_line(found, 'longterm', iseol)) |
| |
| # now find latest tag in linux-next |
| repo = Repo(GIT_NEXT) |
| tagrefs = self.get_tagref_list(repo, cutoff) |
| |
| regex = re.compile('^next-') |
| rel = self.find_latest_matching(tagrefs, regex) |
| releases.append(self.make_release_line(rel, 'linux-next')) |
| |
| self.current_releases = releases |
| self.latest_release = latest |
| |
| self.generate_rss_feed() |
| self.generate_finger_banner() |
| self.generate_releases_json() |
| self.check_release_tracker() |
| |
| def check_url_exists(self, path): |
| r = self.reqs.head('https://www.kernel.org/%s' % path) |
| return r.status_code == 200 |
| |
| def check_release_tracker(self): |
| if 'PELICAN_DRYRUN' in os.environ.keys(): |
| print 'PELICAN_DRYRUN found, not doing release tracking' |
| return |
| |
| import socket |
| send_mail = False |
| |
| if socket.gethostname() == 'fe-sync.int.kernel.org': |
| send_mail = True |
| |
| # Load known releases from the release tracker file |
| known = [] |
| |
| if os.path.exists(self.release_tracker): |
| try: |
| fh = open(self.release_tracker, 'r') |
| known = json.load(fh) |
| fh.close() |
| except: |
| # it's better to not announce something than to spam |
| # people needlessly. |
| send_mail = False |
| |
| for chunks in self.current_releases: |
| release = chunks[1] |
| if release not in known: |
| # This appears to be a new release. |
| if chunks[5] is None or chunks[6] is None or chunks[7] is None: |
| # Don't announce anything that doesn't have source, patch or sign. |
| continue |
| |
| known.append(release) |
| if send_mail: |
| self.send_release_email(chunks) |
| |
| fh = open(self.release_tracker, 'w') |
| json.dump(known, fh, indent=4) |
| fh.close() |
| |
| def send_release_email(self, chunks): |
| (label, release, iseol, timestamp, isodate, source, sign, patch, incr, changelog, gitweb, diffview) = chunks |
| if iseol: |
| eol = ' (EOL)' |
| else: |
| eol = '' |
| |
| import smtplib |
| from email.mime.text import MIMEText |
| from email.Utils import COMMASPACE, formatdate, make_msgid |
| |
| announce_list = 'linux-kernel-announce@vger.kernel.org' |
| smtp_server = 'mail.kernel.org' |
| |
| # This allows us to run tests in pre-prod |
| if 'ANNOUNCE_LIST' in os.environ.keys(): |
| announce_list = os.environ['ANNOUNCE_LIST'] |
| if 'SMTP_SERVER' in os.environ.keys(): |
| smtp_server = os.environ['SMTP_SERVER'] |
| |
| body = ("Linux kernel version %s%s has been released. It is available from:\r\n" % (release, eol) |
| + "\r\n" |
| + "Patch: https://www.kernel.org/%s\r\n" % patch |
| + "Full source: https://www.kernel.org/%s\r\n" % source |
| + "PGP Signature: https://www.kernel.org/%s\r\n" % sign |
| + "\r\n" |
| + "-----------------------------------------------------------------------------\r\n" |
| + "This is an automatically generated message. To unsubscribe from this list,\r\n" |
| + "please send a message to majordomo@vger.kernel.org containing the line:\r\n" |
| + "\r\n" |
| + "\tunsubscribe linux-kernel-announce <your_email_address>\r\n" |
| + "\r\n" |
| + "... where <your_email_address> is the email address you used to subscribe\r\n" |
| + "to this list.\r\n" |
| + "-----------------------------------------------------------------------------\r\n") |
| |
| if diffview is not None: |
| body += ("\r\n" |
| + "You can view the summary of the changes at the following URL:\r\n" |
| + "%s\r\n" % diffview) |
| |
| msg = MIMEText(body) |
| msg['Subject'] = "Linux kernel %s released" % release |
| msg['From'] = 'Linux Kernel Distribution System <kdist@linux.kernel.org>' |
| msg['To'] = announce_list |
| msg['Date'] = formatdate(localtime=True) |
| msg['Message-Id'] = make_msgid('kdist.linux.kernel.org') |
| msg['X-Linux-Kernel-Version'] = release |
| msg['X-Linux-Kernel-Patch-URL'] = "https://www.kernel.org/%s" % patch |
| msg['X-Linux-Kernel-Full-URL'] = "https://www.kernel.org/%s" % source |
| |
| s = smtplib.SMTP(smtp_server) |
| s.sendmail('kdist@linux.kernel.org', [announce_list,], msg.as_string()) |
| s.quit() |
| |
| def generate_releases_json(self): |
| # Put release info into a .json file for easy import and parse |
| out = {'releases': []} |
| for entry in self.current_releases: |
| (label, release, iseol, timestamp, isodate, source, sign, patch, incr, changelog, gitweb, diffview) = entry |
| |
| if source: |
| source = 'https://www.kernel.org/%s' % source |
| if sign: |
| sign = 'https://www.kernel.org/%s' % sign |
| if patch: |
| patch = 'https://www.kernel.org/%s' % patch |
| if incr: |
| incr = 'https://www.kernel.org/%s' % incr |
| if changelog: |
| changelog = 'https://www.kernel.org/%s' % changelog |
| |
| relhash = { |
| 'moniker': label, |
| 'version': release, |
| 'iseol': iseol, |
| 'released': { |
| 'timestamp': timestamp, |
| 'isodate': isodate, |
| }, |
| 'source': source, |
| 'pgp': sign, |
| 'patch': { |
| 'full': patch, |
| 'incremental': incr, |
| }, |
| 'changelog': changelog, |
| 'gitweb': gitweb, |
| 'diffview': diffview |
| } |
| |
| out['releases'].append(relhash) |
| |
| out['latest_stable'] = { |
| 'version': self.latest_release[1], |
| } |
| |
| fh = open(self.json_path, 'w') |
| json.dump(out, fh, indent=4) |
| fh.close() |
| |
| def generate_finger_banner(self): |
| # Just a bit of legacy silliness |
| contents = '' |
| for chunks in self.current_releases: |
| label = chunks[0] |
| release = chunks[1] |
| iseol = chunks[2] |
| line = '' |
| |
| if label not in ('mainline', 'linux-next'): |
| bits = release.split('.') |
| bits.pop(-1) |
| line = ' ' + '.'.join(bits) |
| |
| if iseol: |
| eol = ' (EOL)' |
| else: |
| eol = '' |
| |
| leftside = 'The latest %s%s version of the Linux kernel is:' % (label, line) |
| contents += '{0:<61} {1}{2}\n'.format(leftside, release, eol) |
| |
| fp = open(self.finger_path, 'w') |
| fp.write(contents) |
| fp.close() |
| |
| def generate_rss_feed(self): |
| feed = Rss201rev2Feed( |
| title='Latest Linux Kernel Versions', |
| link='http://www.kernel.org', |
| feed_url='http://www.kernel.org/feeds/kdist.xml', |
| description='Latest Linux Kernel Versions', |
| creator='FTP Admin <ftpadmin@kernel.org>' |
| ) |
| for entry in self.current_releases: |
| (label, release, iseol, timestamp, isodate, source, sign, patch, incr, changelog, gitweb, diffview) = entry |
| |
| if iseol: |
| eol = ' (EOL)' |
| else: |
| eol = '' |
| |
| contents = ''' |
| <table> |
| <tr><th align="right">Version:</th><td><strong>%s%s</strong> (%s)</td></tr> |
| <tr><th align="right">Released:</th><td>%s</td></tr> |
| ''' % (release, eol, label, isodate) |
| |
| if source: |
| contents += ''' |
| <tr><th align="right">Source:</th><td><a href="https://www.kernel.org/%s">linux-%s.tar.xz</a></td></tr>''' % (source, release) |
| |
| if sign: |
| contents += ''' |
| <tr><th align="right">PGP Signature:</th><td><a href="https://www.kernel.org/%s">linux-%s.tar.sign</a></td></tr>''' % (sign, release) |
| |
| if patch: |
| contents += ''' |
| <tr><th align="right">Patch:</th><td><a href="https://www.kernel.org/%s">patch-%s.xz</a>''' % (patch, release) |
| if incr: |
| contents += ''' (<a href="https://www.kernel.org/%s">Incremental</a>)''' % incr |
| contents += '''</td></tr>''' |
| |
| if changelog: |
| contents += ''' |
| <tr><th align="right">ChangeLog:</th><td><a href="https://www.kernel.org/%s">ChangeLog-%s</a></td></tr>''' % (changelog, release) |
| |
| contents += ''' |
| </table>''' |
| |
| feed.add_item( |
| title='%s: %s' % (release, label), |
| link='http://www.kernel.org/', |
| unique_id='kernel.org,%s,%s,%s' % (label, release, isodate), |
| description=contents, |
| pubdate=datetime.datetime.fromtimestamp(timestamp) |
| ) |
| |
| # We really should be generating after site is done, |
| # but I'm too lazy to figure out the plugin hooks for that |
| utils.mkdir_p(os.path.dirname(self.rss_path)) |
| |
| fp = open(self.rss_path, 'w') |
| feed.write(fp, 'utf-8') |
| fp.close() |
| |
| def _get_release_dir_by_version(self, version): |
| # some magic here to calculate where the source is |
| urlpath = 'pub/linux/kernel' |
| |
| if version.find('3.') == 0: |
| # This is version 3.x, so will be in /v3.x/ |
| urlpath += '/v3.x' |
| |
| elif version.find('4.') == 0: |
| # When Linus feels like releasing a 4.x, it'll be in /v4.x/ |
| urlpath += '/v4.x' |
| |
| elif version.find('2.6') == 0: |
| # We're hardcoding ourselves here, but this will rarely change |
| urlpath += '/v2.6/longterm' |
| |
| if version.find('2.6.32') == 0: |
| urlpath += '/v2.6.32' |
| else: |
| urlpath += '/v2.6.34' |
| |
| if version.find('-rc') > 0: |
| # This is a testing kernel, so will be in /testing/ |
| urlpath += '/testing' |
| |
| return urlpath |
| |
| def _get_source_path_by_version(self, version): |
| dirpath = self._get_release_dir_by_version(version) |
| path = '%s/linux-%s.tar.xz' % (dirpath, version) |
| |
| return path |
| |
| def _get_sign_path_by_version(self, version): |
| dirpath = self._get_release_dir_by_version(version) |
| path = '%s/linux-%s.tar.sign' % (dirpath, version) |
| |
| return path |
| |
| def _get_patch_path_by_version(self, version): |
| dirpath = self._get_release_dir_by_version(version) |
| path = '%s/patch-%s.xz' % (dirpath, version) |
| |
| return path |
| |
| def _get_prev_release(self, release, label): |
| # Returns (previous mainline, previous incremental) |
| # for 3.17-rc2: (3.16, 3.17-rc1) |
| # for 3.16.2: (3.16, 3.16.1) |
| # for 2.6.32.63: (2.6.32, 2.6.32.62) |
| # for 3.17-rc1: (3.16, None) |
| # for 3.17: (3.16, None) |
| # for 3.16.1: (3.16, None) |
| |
| if release.find('next') != -1: |
| # Futile for next kernels, as we have to look at actual tags |
| return None, None |
| |
| bits = release.split('.') |
| |
| if len(bits) < 2: |
| # What happen? Someone set up us something weird. |
| return None, None |
| |
| mainline = None |
| incremental = None |
| |
| if label.find('mainline') == 0: |
| # Does the last bit have '-rc' in it? |
| if bits[-1].find('-rc') > 0: |
| rcbits = bits[-1].split('-rc') |
| try: |
| lastbit = int(rcbits.pop(-1)) |
| mainlbit = int(rcbits.pop(-1)) |
| |
| if lastbit > 1: |
| # We have an incremental |
| prevbit = str(lastbit - 1) |
| prevrc = '%s-rc%s' % (mainlbit, prevbit) |
| newbits = bits[:-1] + [prevrc] |
| incremental = '.'.join(newbits) |
| # Now arrive at mainline |
| if mainlbit == 0: |
| # We're looking at 4.0-rcX, then. Cowardly bail out with only incremental |
| return None, incremental |
| prevbit = str(mainlbit - 1) |
| newbits = bits[:-1] + [prevbit] |
| mainline = '.'.join(newbits) |
| |
| except ValueError: |
| pass |
| |
| return mainline, incremental |
| |
| # First, most unusual case -- a mainline release ending in .0 |
| try: |
| lastbit = int(bits.pop(-1)) |
| if lastbit == 0: |
| # We can't really figure out what the previous release was based on this info, so |
| # we cowardly pretend it didn't happen |
| return None, None |
| |
| prevbit = str(lastbit - 1) |
| bits.append(prevbit) |
| mainline = '.'.join(bits) |
| except ValueError: |
| # Not sure what happened, but let's pretend it didn't work |
| return None, None |
| |
| return mainline, incremental |
| |
| # Not a mainline kernel, so a simpler logic |
| try: |
| lastbit = int(bits.pop(-1)) |
| # Combining the remaining bits gives us mainline |
| mainline = '.'.join(bits) |
| |
| if lastbit > 1: |
| prevbit = str(lastbit - 1) |
| bits.append(prevbit) |
| incremental = '.'.join(bits) |
| else: |
| # We don't have a previous incremental in this case |
| incremental = None |
| |
| return mainline, incremental |
| |
| except ValueError: |
| # Not sure what happened, but let's pretend it didn't work |
| return None, None |
| |
| return None, None |
| |
| |
| def make_release_line(self, rel, label, iseol=False): |
| # drop the leading 'v': |
| if rel[0][0] == 'v': |
| release = rel[0][1:] |
| else: |
| release = rel[0] |
| |
| timestamp = rel[1] |
| isodate = time.strftime('%Y-%m-%d', time.gmtime(rel[1])) |
| |
| source = None |
| sign = None |
| patch = None |
| incr = None |
| changelog = None |
| gitweb = None |
| diffview = None |
| |
| if release.find('next') != 0: |
| # next don't have anything besides a tag |
| |
| source = self._get_source_path_by_version(release) |
| sign = self._get_sign_path_by_version(release) |
| patch = self._get_patch_path_by_version(release) |
| |
| prevmainline, previncremental = self._get_prev_release(release, label) |
| |
| if label.find('stable') == 0 or label.find('longterm') == 0: |
| dirpath = self._get_release_dir_by_version(release) |
| changelog = '%s/ChangeLog-%s' % (dirpath, release) |
| cgitpath = 'https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git' |
| gitweb = ('%s/log/?id=refs/tags/v%s' % (cgitpath, release)) |
| |
| # incr patches are named incr/3.5.(X-1)-(X).xz |
| if previncremental: |
| lastbit = release.split('.')[-1] |
| incr = '%s/incr/patch-%s-%s.xz' % (dirpath, previncremental, lastbit) |
| diffview = '%s/diff/?id=v%s&id2=v%s&dt=2' % (cgitpath, release, previncremental) |
| elif prevmainline: |
| # diffview to previous mainline |
| diffview = ('%s/diff/?id=v%s&id2=v%s&dt=2' % (cgitpath, release, prevmainline)) |
| |
| elif label.find('mainline') == 0: |
| cgitpath = 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git' |
| gitweb = '%s/log/?id=refs/tags/v%s' % (cgitpath, release) |
| if previncremental: |
| diffview = '%s/diff/?id=v%s&id2=v%s&dt=2' % (cgitpath, release, previncremental) |
| elif prevmainline: |
| diffview = ('%s/diff/?id=v%s&id2=v%s&dt=2' % (cgitpath, release, prevmainline)) |
| |
| else: |
| gitweb = 'https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/log/?id=refs/tags/%s' % release |
| # Not offering a diffview, as it's too hard to calculate |
| diffview = None |
| |
| # Verify that patch, changelog and incremental patch exist |
| if patch and not self.check_url_exists(patch): |
| patch = None |
| if changelog and not self.check_url_exists(changelog): |
| changelog = None |
| if incr and not self.check_url_exists(incr): |
| incr = None |
| |
| # This needs to be refactored into a hash. In my defense, |
| # it started with 3 entries. |
| return label, release, iseol, timestamp, isodate, source, sign, patch, incr, changelog, gitweb, diffview |
| |
| def get_tagref_list(self, repo, cutoff=None): |
| tagrefs = [] |
| for tagref in repo.tags: |
| try: |
| tdate = tagref.tag.tagged_date |
| except: |
| # Some of the tags break git-python due to not having an associated |
| # commit. Work around this limitation by ignoring those tags. |
| continue |
| |
| # Is it too old? |
| if cutoff is not None and tdate < cutoff: |
| continue |
| |
| tagrefs.append(tagref) |
| |
| return tagrefs |
| |
| def _check_tarball_by_tagname(self, tagname): |
| # Only check those starting with "v" |
| if tagname[0] == 'v': |
| # Ignore this check for next-YYYYMMDD kernels |
| source = self._get_source_path_by_version(tagname[1:]) |
| |
| if not self.check_url_exists(source): |
| return False |
| |
| return True |
| |
| def find_latest_matching(self, tagrefs, regex, blacklist=[]): |
| current = None |
| |
| for tagref in tagrefs: |
| # Does it match the regex? |
| if not regex.match(tagref.name): |
| continue |
| |
| if tagref.name in blacklist: |
| continue |
| |
| tdate = tagref.tag.tagged_date |
| |
| # is it older than current? |
| if current is None or current.tag.tagged_date < tdate: |
| current = tagref |
| |
| if current is None: |
| return None |
| |
| if not self._check_tarball_by_tagname(current.name): |
| # Tarball for the latest tag does not exist. |
| # Add it to blacklist and try again. |
| blacklist.append(current.name) |
| return self.find_latest_matching(tagrefs, regex, blacklist) |
| |
| return current.name, current.tag.tagged_date |
| |