181 lines
6.5 KiB
Python
Executable File
181 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Example .ipxe.json:
|
|
# {
|
|
# "date": "Thu, 21 Jan 2021 06:42:21 +0000",
|
|
# "variant": "efi",
|
|
# "sha512": "b4d2e517c69224bf14f79e155(...)"
|
|
# }
|
|
# They don't version the ISO, so we use the file date on the mirror listing.
|
|
|
|
import datetime
|
|
import json
|
|
import os
|
|
import re
|
|
##
|
|
import requests
|
|
from bs4 import BeautifulSoup
|
|
##
|
|
import _base
|
|
|
|
try:
|
|
import lxml
|
|
_has_lxml = True
|
|
except ImportError:
|
|
_has_lxml = False
|
|
|
|
|
|
class Updater(_base.BaseUpdater):
|
|
_fname_re = re.compile(r'^(?:.*/)?ipxe\.'
|
|
r'(?P<variant>(iso|efi))$')
|
|
_allowed_variants = ('iso', 'efi')
|
|
_tpl_file = 'ipxe_grub.conf.j2'
|
|
# I think this *technically* should be '%Y-%m-%d %H:%M %z' but it seems datetime cannot parse that if %z is empty.
|
|
_datever_fmt = '%Y-%m-%d %H:%M'
|
|
|
|
def __init__(self,
|
|
variant = 'iso',
|
|
dest_dir = '/boot/iso', # Should be subdir of boot_dir
|
|
dest_file = 'ipxe.iso',
|
|
ver_file = '.ipxe.json',
|
|
lock_path = '/tmp/.ipxe.lck',
|
|
dl_base = 'https://boot.ipxe.org/',
|
|
do_grub_cfg = True,
|
|
boot_dir = '/boot', # ESP or boot partition mount; where GRUB files are installed *under*
|
|
grub_cfg = '/etc/grub.d/40_custom_ipxe',
|
|
# check_gpg = True, # TODO: GPG sig checking, http://mirror.rit.edu/grml//gnupg-michael-prokop.txt
|
|
# hash_type = 'sha512'):
|
|
):
|
|
if variant not in self._allowed_variants:
|
|
raise ValueError('variant must be one of: {0}'.format(', '.join(self._allowed_variants)))
|
|
else:
|
|
self.variant = variant.lower()
|
|
if self.variant == 'efi':
|
|
dest_file = dest_file.replace('.iso', '.efi')
|
|
super().__init__(dest_dir,
|
|
dest_file,
|
|
ver_file,
|
|
lock_path,
|
|
do_grub_cfg,
|
|
boot_dir,
|
|
grub_cfg,
|
|
hash_type = 'sha512')
|
|
self.dl_base = dl_base
|
|
self._init_vars()
|
|
|
|
def _init_vars(self):
|
|
if self.getRunning():
|
|
return(None)
|
|
self.getCurVer()
|
|
self.getNewVer()
|
|
return(None)
|
|
|
|
def getCurVer(self):
|
|
if self.getRunning():
|
|
return(None)
|
|
if not os.path.isfile(self.dest_ver):
|
|
self.do_update = True
|
|
self.force_update = True
|
|
return(None)
|
|
with open(self.dest_ver, 'rb') as fh:
|
|
ver_info = json.load(fh)
|
|
self.old_date = datetime.datetime.strptime(ver_info['date'], self._date_fmt)
|
|
self.old_hash = ver_info.get(self.hash_type)
|
|
self.old_ver = datetime.datetime.strptime(ver_info['ver'], self._datever_fmt)
|
|
self.variant = ver_info.get('variant', self.variant)
|
|
# We do this to avoid the hash check in _base.BaseUpdater.download()
|
|
self._new_hash = self.old_hash
|
|
self.new_date = self.old_date
|
|
self.new_ver = self.old_ver
|
|
if ver_info.get('arch') != self.arch:
|
|
self.do_update = True
|
|
self.force_update = True
|
|
return(None)
|
|
if not os.path.isfile(self.dest_iso):
|
|
self.do_update = True
|
|
self.force_update = True
|
|
return(None)
|
|
realhash = self.getISOHash()
|
|
if self.old_hash != realhash:
|
|
self.do_update = True
|
|
self.force_update = True
|
|
return(None)
|
|
return(None)
|
|
|
|
def getNewVer(self):
|
|
if self.getRunning():
|
|
return(None)
|
|
req = requests.get(self.dl_base, headers = {'User-Agent': 'curl/7.74.0'})
|
|
if not req.ok:
|
|
raise RuntimeError('Received non-200/30x {0} for {1}'.format(req.status_code, self.dl_base))
|
|
html = BeautifulSoup(req.content.decode('utf-8'), ('lxml' if _has_lxml else 'html.parser'))
|
|
# This is a little hacky.
|
|
filelist = html.find('table')
|
|
# Get the header, and the index for the proper columns.
|
|
file_col = 0
|
|
date_col = 0
|
|
header = filelist.find('tr')
|
|
# Icon, Name, Modified, Size, Description
|
|
file_len = len(header.find_all('th'))
|
|
if header is None:
|
|
raise RuntimeError('Could not find header row')
|
|
for idx, cell in enumerate(header.find_all('th')):
|
|
link = cell.find('a')
|
|
if link is None:
|
|
continue
|
|
# At least the header columns have predictable links (for sorting).
|
|
if link['href'] == '?C=N;O=D': # Name
|
|
file_col = idx
|
|
continue
|
|
if link['href'] == '?C=M;O=A': # Last Modified
|
|
date_col = idx
|
|
for idx, row in enumerate(filelist.find_all('tr')):
|
|
if idx == 0: # Header; skip.
|
|
continue
|
|
cells = row.find_all('td')
|
|
if len(cells) != file_len:
|
|
continue
|
|
name_html = cells[file_col]
|
|
date_html = cells[date_col]
|
|
if not all((name_html, date_html)):
|
|
continue
|
|
date = datetime.datetime.strptime(date_html.text.strip(), self._datever_fmt)
|
|
name_link = name_html.find('a')
|
|
if name_link is None:
|
|
continue
|
|
name = name_link.text
|
|
fname_r = self._fname_re.search(name)
|
|
if not fname_r:
|
|
continue
|
|
f_variant = fname_r.groupdict()['variant']
|
|
if f_variant != self.variant:
|
|
continue
|
|
self.new_ver = date
|
|
self.iso_url = os.path.join(self.dl_base, name_link['href'].replace(self.dl_base, ''))
|
|
if not all((self.old_ver, self.old_date)) or \
|
|
(self.new_ver > self.old_ver):
|
|
self.do_update = True
|
|
self.new_date = datetime.datetime.now(datetime.timezone.utc)
|
|
return(None)
|
|
|
|
def updateVer(self):
|
|
if self.getRunning():
|
|
return(None)
|
|
if any((self.do_update, self.force_update)):
|
|
self._new_hash = self.getISOHash()
|
|
d = {'date': self.new_date.strftime(self._date_fmt),
|
|
'variant': self.variant,
|
|
'ver': self.new_ver.strftime(self._datever_fmt),
|
|
self.hash_type: self._new_hash}
|
|
j = json.dumps(d, indent = 4)
|
|
with open(self.dest_ver, 'w') as fh:
|
|
fh.write(j)
|
|
fh.write('\n')
|
|
os.chmod(self.dest_ver, 0o0644)
|
|
return(None)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
u = Updater()
|
|
u.main()
|