Muehlenhoff has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/365263 )

Change subject: Migrate former Salt minion to standalone tools executed via 
Cumin (WIP)
......................................................................


Migrate former Salt minion to standalone tools executed via Cumin (WIP)

WIP and fully untested.

Change-Id: I772b3eaff0d5075627952dddf953608b795e8f1d
---
A clients/debdeploy-deploy
M debian/changelog
2 files changed, 300 insertions(+), 0 deletions(-)

Approvals:
  Muehlenhoff: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/clients/debdeploy-deploy b/clients/debdeploy-deploy
new file mode 100755
index 0000000..5b00dc4
--- /dev/null
+++ b/clients/debdeploy-deploy
@@ -0,0 +1,273 @@
+#! /usr/bin/python
+# -*- coding: utf-8 -*-
+'''
+Module for deploying DEB packages on wide scale
+'''
+
+import logging
+import pickle
+import subprocess
+import os
+import re
+import platform
+import sys
+import argparse
+import json
+import ConfigParser
+from logging.handlers import RotatingFileHandler
+from debian import deb822
+
+logger = logging.getLogger('debdeploy')
+
+
+def parse_args():
+    p = argparse.ArgumentParser(
+        description='debdeploy-deploy - Deploy a software update')
+    p.add_argument('--run-apt-update', action='store_true', default=False,
+                   help='If enabled, run apt-get update during deployments')
+    p.add_argument('--verbose', action='store_true', default=False,
+                   help='Include full output of apt')
+    p.add_argument('--console', action='store_true', default=False,
+                   help='Enable additional console output')
+    p.add_argument('--json', action='store_true', default=False,
+                   help='Return a JSONIf enabled, run apt-get update during 
deployments')
+    p.add_argument('--source', action='store', required=True,
+                   help='The name of the source package to be updated')
+    p.add_argument('--updatespec', action='store', nargs='+', required=True)
+
+    args = p.parse_args(sys.argv[1:])
+
+    for i in args.updatespec:
+        if len(i.split("_")) != 3:
+            p.error("Malformed update spec: " + i)
+
+    return args
+
+
+def setup_logger(verbose=False, console_output=False):
+    log_file = "/var/log/debdeploy/updates.log"
+
+    log_path = os.path.dirname(log_file)
+    if not os.path.exists(log_path):
+        os.makedirs(log_path, 0770)
+
+    log_formatter = logging.Formatter(fmt='%(asctime)s (%(levelname)s) 
%(message)s')
+    log_handler = RotatingFileHandler(log_file, maxBytes=(5 * (1024**2)), 
backupCount=30)
+    log_handler.setFormatter(log_formatter)
+    logger.addHandler(log_handler)
+    logger.raiseExceptions = False
+
+    if console_output:
+        console = logging.StreamHandler()
+        logging.getLogger('debdeploy').addHandler(console)
+
+    if verbose:
+        logger.setLevel(logging.DEBUG)
+    else:
+        logger.setLevel(logging.INFO)
+
+
+def get_os_version():
+    os_id = ""
+    os_version = "undef"  # Usually not used, but can be used to track unstable
+
+    try:
+        with open('/etc/os-release', 'r') as data:
+            for i in data.readlines():
+                if i.startswith("ID"):
+                    os_id = i.split("=")[1].strip().replace('"', 
"").replace("\n", "")
+                if i.startswith("VERSION_ID"):
+                    os_version = i.split("=")[1].strip().replace('"', 
"").replace("\n", "")
+    except IOError:
+        logger.info("Could not open /etc/os-release")
+        return "invalid"
+
+    if not os_id:
+        logger.info("Failed to parse OS release, no distro ID specified")
+        return "invalid"
+
+    return os_id + "_" + os_version
+
+
+def get_installed_binary_packages(source):
+    # Detect all locally installed binary packages of a given source package
+    # The only resource we can use for that is parsing the /var/lib/dpkg/status
+    # file. The format is a bit erratic: The Source: line is only present for
+    # binary packages not having the same name as the binary package
+    installed_binary_packages = []
+    for pkg in deb822.Packages.iter_paragraphs(file('/var/lib/dpkg/status')):
+
+        # skip packages in deinstalled status ("rc" in dpkg). These are 
irrelevant for
+        # upgrades and cause problems when binary package names have changed 
(since
+        # package installations are forced with a specific version which is 
not available
+        # for those outdated binary package names)
+        installation_status = pkg['Status'].split()[0]
+        if installation_status == "deinstall":
+            continue
+
+        # Source packages which have had a binNMU have a Source: entry with 
the source
+        # package version in brackets, so strip these
+        # If no Source: entry is present in /var/lib/dpkg/status, then the 
source package
+        # name is identical to the binary package name
+        if 'Source' in pkg and re.sub(r'\(.*?\)', '', pkg['Source']).strip() 
== source:
+            installed_binary_packages.append(pkg['Package'])
+        elif 'Package' in pkg and pkg['Package'] == source:
+            installed_binary_packages.append(pkg['Package'])
+
+    return installed_binary_packages
+
+
+def list_pkgs():
+    '''
+    This function returns a dictionary of installed Debian packages and their
+    respective installed version (keyed by the package name).
+
+    It is mostly used to determine whether packages were updated, installed or 
removed.
+    '''
+
+    pkgs = {}
+
+    try:
+        osarch = subprocess.check_output(["dpkg", "--print-architecture"])
+    except subprocess.CalledProcessError as e:
+        logger.info("Could not determine host architecture", e.returncode)
+        sys.exit(1)
+    try:
+        installed_packages = subprocess.check_output(
+            ["dpkg-query",
+             "--showformat='${Status} ${Package} ${Version} 
${Architecture}\n'", "-W"])
+    except subprocess.CalledProcessError as e:
+        logger.info("Could not determine list of installed packages", 
e.returncode)
+        sys.exit(1)
+
+    for line in installed_packages.splitlines():
+        cols = line.split()
+        try:
+            if len(cols) == 6:
+                linetype, status, name, version_num, arch = \
+                    [cols[x] for x in (0, 2, 3, 4, 5)]
+        except ValueError:
+            continue
+
+        if arch != 'all' and osarch == 'amd64' and osarch != arch:
+            name += ':{0}'.format(arch)
+        if ('install' in linetype or 'hold' in linetype) and 'installed' in 
status:
+            pkgs[name] = version_num
+
+    return pkgs
+
+
+def install_pkgs(binary_packages, version_num, downgrade=False):
+    '''
+    Installs software updates via apt
+
+    binary_packages: A list of Debian binary package names to update (list of 
tuples)
+    downgrade: If enabled, version downgrades are allowed (required for 
rollbacks
+    to earlier versions)
+
+    Returns a tuple of the apt exit code and the output of the installation 
process
+
+    '''
+
+    targets = []
+    for pkg in binary_packages:
+        if version_num is None:
+            targets.append(pkg)
+        else:
+            targets.append('{0}={1}'.format(pkg, version_num.lstrip('=')))
+
+    cmd = ['apt-get', '-q', '-y']
+    if downgrade:
+        cmd.append('--force-yes')
+    cmd = cmd + ['-o', 'DPkg::Options::=--force-confold']
+    cmd = cmd + ['-o', 'DPkg::Options::=--force-confdef']
+    cmd.append('install')
+    cmd.extend(targets)
+
+    logger.debug("apt invocation: ", cmd)
+
+    try:
+        update = (0, subprocess.check_output(cmd, stderr=subprocess.STDOUT))
+    except subprocess.CalledProcessError as e:
+        update = (e.returncode, e.output)
+
+    return update
+
+
+def result(status, updated_packages, log, json_output):
+    '''
+    Generates a data set to return to Cumin.
+    status: OK | ERROR: foo
+    updated_packages: dictionary sorted by binary package names with the
+        previous and the new version
+    log: the complete apt log
+    '''
+
+    if json_output:
+        return json.dumps([status, updated_packages, log])
+    else:
+        return [status, updated_packages, log]
+
+
+def main():
+    '''
+    Updates all installed binary packages of the source package
+    to the specified version.
+    '''
+    args = parse_args()
+
+    setup_logger(False, args.console)
+
+    versions = {}
+    for i in args.updatespec:
+        versions[i.split("_")[0] + "_" + i.split("_")[1]] = i.split("_")[2]
+
+    installed_distro = get_os_version()
+    if installed_distro == "invalid":
+        return result("ERROR: Could not parse installed distros", {}, "", 
args.json)
+
+    if versions.get(installed_distro, None) is None:
+        logger.info("Update doesn't apply to the installed distribution (" + 
str(installed_distro))
+        return result("OK", {}, "", args.json)
+
+    installed_binary_packages = get_installed_binary_packages(args.source)
+    logger.debug("Installed binary packages for " + args.source +
+                 ": " + str(installed_binary_packages))
+
+    if len(installed_binary_packages) == 0:
+        logger.info("No binary packages installed for source package " + 
args.source)
+        return result("OK", {}, "", args.json)
+
+    if args.run_apt_update:
+        try:
+            subprocess.call(["apt-get", "update"])
+        except subprocess.CalledProcessError as e:
+            logger.info("apt-get update failed: ", e.returncode)
+
+    old = list_pkgs()
+    apt_output = install_pkgs(installed_binary_packages, 
versions.get(installed_distro, None))
+
+    new = list_pkgs()
+
+    old_keys = set(old.keys())
+    new_keys = set(new.keys())
+
+    updated = []
+
+    intersect = old_keys.intersection(new_keys)
+    modified = {x: (old[x], new[x]) for x in intersect if old[x] != new[x]}
+
+    logger.info("Modified packages: " + str(modified))
+
+    if args.verbose:
+        return result("OK", modified, apt_output, args.json)
+    else:
+        return result("OK", modified, "", args.json)
+
+
+if __name__ == '__main__':
+    print main()
+
+# Local variables:
+# mode: python
+# End:
diff --git a/debian/changelog b/debian/changelog
index a6f0def..c4bec42 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,30 @@
+debdeploy (0.0.99-1) UNRELEASED; urgency=medium
+
+  * Migrate away from Salt towards Cumin and clean out some code
+    which turned out to be useful on paper, but not so useful
+    in practice
+  * Remove feature to trigger restarts based on the program name,
+    worked in general, but it's a saner choice to require the
+    service name (especially since all distros are converging to
+    systemd anyway) and it avoids dealing with a lot of special
+    cases. Anyone managing service restarts should be expected
+    to know the service name anyway.
+  * Remove feature to deploy and remove software, such tasks should
+    be handled by puppet or a similar system configuration tool.
+    This was part of the initial debdeploy releases, but was never
+    used in practice, so removing it.
+  * Restart detection after a library update is now decoupled from
+    the deployment process. This simplifies the deployment of staged
+    rollouts a lot (e.g. if two libraries are updated which are linked
+    by HHVM and if the deployed combines the HHVM restart for both
+    updates)
+  * In the clients operate on ID and VERSION_ID only, previous we used
+    the Debian code name populated by Salt. The YAML files still
+    address code names, that'll be parsed from a config file so that
+    supporting a new release only needs to be enabled on the server
+
+ -- Moritz Muehlenhoff <mmuhlenh...@wikimedia.org>  Fri, 07 Jul 2017 13:49:03 
+0200
+
 debdeploy (0.0.10-1) jessie-wikimedia; urgency=medium
 
   * Fix a traceback in status display

-- 
To view, visit https://gerrit.wikimedia.org/r/365263
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I772b3eaff0d5075627952dddf953608b795e8f1d
Gerrit-PatchSet: 1
Gerrit-Project: operations/debs/debdeploy
Gerrit-Branch: master
Gerrit-Owner: Muehlenhoff <mmuhlenh...@wikimedia.org>
Gerrit-Reviewer: Muehlenhoff <mmuhlenh...@wikimedia.org>
Gerrit-Reviewer: Volans <rcocci...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to