alpine-zdt-images/alpine.py

115 lines
4.0 KiB
Python

# vim: ts=4 et:
import json
import re
from datetime import datetime, timedelta
from urllib.request import urlopen
class Alpine():
DEFAULT_RELEASES_URL = 'https://alpinelinux.org/releases.json'
DEFAULT_POSTS_URL = 'https://alpinelinux.org/posts/'
DEFAULT_CDN_URL = 'https://dl-cdn.alpinelinux.org/alpine'
DEFAULT_WEB_TIMEOUT = 5
def __init__(self, releases_url=None, posts_url=None, cdn_url=None, web_timeout=None):
self.now = datetime.utcnow()
self.release_today = self.now.strftime('%Y%m%d')
self.eol_tomorrow = (self.now + timedelta(days=1)).strftime('%F')
self.latest = None
self.versions = {}
self.releases_url = releases_url or self.DEFAULT_RELEASES_URL
self.posts_url = posts_url or self.DEFAULT_POSTS_URL
self.web_timeout = web_timeout or self.DEFAULT_WEB_TIMEOUT
self.cdn_url = cdn_url or self.DEFAULT_CDN_URL
# get all Alpine versions, and their EOL and latest release
res = urlopen(self.releases_url, timeout=self.web_timeout)
r = json.load(res)
branches = sorted(
r['release_branches'], reverse=True,
key=lambda x: x.get('branch_date', '0000-00-00')
)
for b in branches:
ver = b['rel_branch'].lstrip('v')
if not self.latest:
self.latest = ver
rel = None
notes = None
if releases := b.get('releases', None):
r = sorted(
releases, reverse=True, key=lambda x: x['date']
)[0]
rel = r['version']
notes = r.get('notes', None)
if notes:
notes = self.posts_url + notes.removeprefix('posts/').replace('.md', '.html')
elif ver == 'edge':
# edge "releases" is today's YYYYMMDD
rel = self.release_today
self.versions[ver] = {
'version': ver,
'release': rel,
'end_of_life': b.get('eol_date', self.eol_tomorrow),
'arches': b.get('arches'),
'notes': notes,
}
def _ver(self, ver=None):
if not ver or ver == 'latest' or ver == 'latest-stable':
ver = self.latest
return ver
def repo_url(self, repo, arch, ver=None):
ver = self._ver(ver)
if ver != 'edge':
ver = 'v' + ver
return f"{self.cdn_url}/{ver}/{repo}/{arch}"
def virt_iso_url(self, arch, ver=None):
ver = self._ver(ver)
rel = self.versions[ver]['release']
return f"{self.cdn_url}/v{ver}/releases/{arch}/alpine-virt-{rel}-{arch}.iso"
def version_info(self, ver=None):
ver = self._ver(ver)
if ver not in self.versions:
# perhaps a release candidate?
apk_ver = self.apk_version('main', 'x86_64', 'alpine-base', ver=ver)
rel = apk_ver.split('-')[0]
ver = '.'.join(rel.split('.')[:2])
self.versions[ver] = {
'version': ver,
'release': rel,
'end_of_life': self.eol_tomorrow,
'arches': self.versions['edge']['arches'], # reasonable assumption
'notes': None,
}
return self.versions[ver]
# TODO? maybe implement apk_info() to read from APKINDEX, but for now
# this apk_version() seems faster and gets what we need
def apk_version(self, repo, arch, apk, ver=None):
ver = self._ver(ver)
repo_url = self.repo_url(repo, arch, ver=ver)
apks_re = re.compile(f'"{apk}-(\\d.*)\\.apk"')
res = urlopen(repo_url, timeout=self.web_timeout)
for line in map(lambda x: x.decode('utf8'), res):
if not line.startswith('<a href="'):
continue
match = apks_re.search(line)
if match:
return match.group(1)
# didn't find it?
raise RuntimeError(f"Unable to find {apk} APK via {repo_url}")