200 lines
5.9 KiB
Python
Executable File
200 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
__metaclass__ = type
|
|
|
|
import argparse
|
|
import gzip
|
|
import pathlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
from urllib import request
|
|
from xml.etree import ElementTree
|
|
|
|
import yaml
|
|
|
|
|
|
BASE_REPO_URL = "https://packagecloud.io/sensu/stable/el/8/x86_64/"
|
|
FILENAME_TEMPLATE = "sensu-go-agent_{0}.{1}.{2}.{3}_en-US.{arch}.msi"
|
|
DOWNLOAD_URL_TEMPLATE = (
|
|
"https://s3-us-west-2.amazonaws.com/sensu.io/sensu-go/{0}.{1}.{2}/" +
|
|
FILENAME_TEMPLATE
|
|
)
|
|
MINIMAL_VERSION = (5, 20, 0)
|
|
|
|
|
|
class ArgParser(argparse.ArgumentParser):
|
|
"""An argument parser that displays help on error."""
|
|
|
|
def error(self, message):
|
|
sys.stderr.write("error: {0}\n".format(message))
|
|
self.print_help()
|
|
sys.exit(2)
|
|
|
|
def add_subparsers(self, **kwargs):
|
|
# Workaround for http://bugs.python.org/issue9253
|
|
subparsers = super(ArgParser, self).add_subparsers()
|
|
subparsers.required = True
|
|
subparsers.dest = "command"
|
|
return subparsers
|
|
|
|
|
|
def _fetch_available_versions():
|
|
available_versions = set()
|
|
|
|
response = request.urlopen(BASE_REPO_URL + "repodata/repomd.xml", timeout=30)
|
|
root = ElementTree.parse(response).getroot()
|
|
for data in root.iter("{http://linux.duke.edu/metadata/repo}data"):
|
|
if data.get("type") == "primary":
|
|
break
|
|
else:
|
|
return available_versions
|
|
|
|
location = next(data.iter("{http://linux.duke.edu/metadata/repo}location"))
|
|
path = location.attrib["href"]
|
|
|
|
response = request.urlopen(BASE_REPO_URL + path, timeout=30)
|
|
root = ElementTree.fromstring(gzip.decompress(response.read()))
|
|
for package in root.iter("{http://linux.duke.edu/metadata/common}package"):
|
|
name = next(package.iter("{http://linux.duke.edu/metadata/common}name"))
|
|
if name.text != "sensu-go-agent":
|
|
continue
|
|
|
|
version = next(package.iter("{http://linux.duke.edu/metadata/common}version"))
|
|
version_tuple = tuple(int(c) for c in version.get("ver").split("."))
|
|
if version_tuple < MINIMAL_VERSION:
|
|
continue
|
|
|
|
available_versions.add(version_tuple + (int(version.get("rel")), ))
|
|
|
|
return available_versions
|
|
|
|
|
|
def _load_versions_from_vars(vars):
|
|
return set(
|
|
(tuple(int(c) for c in item["version"].split(".")) + (item["build"],))
|
|
for item in vars["_msi_lookup"].values()
|
|
)
|
|
|
|
|
|
def _sync_versions(vars, available_versions, cache_dir):
|
|
new_vars = dict(vars, _msi_lookup={})
|
|
|
|
old_msis = vars["_msi_lookup"]
|
|
new_msis = new_vars["_msi_lookup"]
|
|
|
|
cache = pathlib.Path(cache_dir)
|
|
|
|
for version in sorted(available_versions):
|
|
version_str = ".".join(map(str, version[:3]))
|
|
|
|
if version_str in old_msis:
|
|
# Happy path: we already have this version sorted
|
|
new_msis[version_str] = old_msis[version_str]
|
|
continue
|
|
|
|
# Sad path: we need to download packages and extract product codes
|
|
product_codes = {}
|
|
for arch in ("x86", "x64"):
|
|
url = DOWNLOAD_URL_TEMPLATE.format(*version, arch=arch)
|
|
filename = FILENAME_TEMPLATE.format(*version, arch=arch)
|
|
file = cache / filename
|
|
|
|
if not file.is_file():
|
|
print("Downloading " + filename)
|
|
with open(file, "wb") as fp:
|
|
response = request.urlopen(url)
|
|
shutil.copyfileobj(response, fp)
|
|
else:
|
|
print("Reusing " + filename)
|
|
|
|
process = subprocess.run(
|
|
("msiinfo", "export", str(file), "Property"), capture_output=True,
|
|
check=True
|
|
)
|
|
for line in process.stdout.splitlines():
|
|
field, value = line.split(b"\t")
|
|
if field == b"ProductCode":
|
|
product_codes[arch] = value.decode("ascii")
|
|
|
|
new_msis[version_str] = dict(
|
|
version=version_str, build=version[-1], product_codes=product_codes
|
|
)
|
|
|
|
new_msis["latest"] = new_msis[version_str]
|
|
|
|
return new_vars
|
|
|
|
|
|
def _load_windows_vars_file(filename):
|
|
with open(filename, "r") as fd:
|
|
return yaml.safe_load(fd)
|
|
|
|
|
|
def _save_windows_vars_file(filename, vars):
|
|
with open(filename, "w") as fd:
|
|
yaml.safe_dump(vars, fd)
|
|
|
|
|
|
def _check(args):
|
|
vars_data = _load_windows_vars_file(args.vars)
|
|
current = _load_versions_from_vars(vars_data)
|
|
available = _fetch_available_versions()
|
|
|
|
missing = available - current
|
|
obsolete = current - available
|
|
|
|
if missing:
|
|
print("The following versions are missing: {0}".format(
|
|
", ".join(".".join(map(str, v)) for v in missing)
|
|
))
|
|
if obsolete:
|
|
print("The following versions are obsolete: {0}".format(
|
|
", ".join(".".join(map(str, v)) for v in obsolete)
|
|
))
|
|
|
|
return len(missing) + len(obsolete)
|
|
|
|
|
|
def _update(args):
|
|
vars_data = _load_windows_vars_file(args.vars)
|
|
current = _load_versions_from_vars(vars_data)
|
|
available = _fetch_available_versions()
|
|
|
|
if current == available:
|
|
return 0
|
|
|
|
new_vars_data = _sync_versions(vars_data, available, args.cache)
|
|
_save_windows_vars_file(args.vars, new_vars_data)
|
|
|
|
return 0
|
|
|
|
|
|
def main():
|
|
parser = ArgParser(
|
|
description="Windows agent version updater",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
)
|
|
subparsers = parser.add_subparsers()
|
|
|
|
check = subparsers.add_parser("check", help="Check for version updates")
|
|
check.add_argument("vars", help="Variable file with Windows lookup table")
|
|
check.set_defaults(func=_check)
|
|
|
|
update = subparsers.add_parser("update", help="Update lookup table")
|
|
update.add_argument("vars", help="Variable file with Windows lookup table")
|
|
update.add_argument(
|
|
"--cache", help="Directory used for caching downloads", default="/tmp"
|
|
)
|
|
update.set_defaults(func=_update)
|
|
|
|
args = parser.parse_args()
|
|
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|