Charmed-Kubernetes/etcd/actions/snap-upgrade.py

197 lines
6.2 KiB
Python
Executable File

#!/usr/local/sbin/charm-env python3
from charms.layer import snap
from charmhelpers.core import unitdata
from charmhelpers.core.hookenv import action_get
from charmhelpers.core.hookenv import action_set
from charmhelpers.core.hookenv import action_fail
from charmhelpers.core.hookenv import config
from charmhelpers.core.hookenv import log
from charms.reactive import is_state
from charms.reactive import remove_state
from charms.reactive import set_state
# from charmhelpers.core.host import chdir
from datetime import datetime
from subprocess import call
from subprocess import check_call
from subprocess import CalledProcessError
from shlex import split
import os
import shutil
import sys
import tempfile
from reactive.etcd import get_target_etcd_channel
# Define some dict's containing paths of files we expect to see in
# scenarios
deb_paths = {
"config": [
"/etc/ssl/etcd/ca.crt",
"/etc/ssl/etcd/server.crt",
"/etc/ssl/etcd/server.key",
"/etc/ssl/etcd/client.crt",
"/etc/ssl/etcd/client.key",
"/etc/default/etcd",
],
"data": ["/var/lib/etcd/default"],
}
# Snappy only cares about the config objects. Data validation will come
# at a later date. We can etcdctl ls / and then verify the data made it
# post migration.
snap_paths = {
"config": [
"/var/snap/etcd/common/etcd.conf.yml",
"/var/snap/etcd/common/server.crt",
"/var/snap/etcd/common/server.key",
"/var/snap/etcd/common/ca.crt",
],
"client": ["/var/snap/etcd/common/client.crt", "/var/snap/etcd/common/client.key"],
"common": "/var/snap/etcd/common",
}
def create_migration_backup(backup_package=""):
"""Backup existing Etcd config/data paths if found and create a
tarball consisting of that discovered configuration"""
datestring = datetime.strftime(datetime.now(), "%Y%m%d_%H%M%S")
if not backup_package:
pkg = "/home/ubuntu/etcd_migration_{}"
backup_package = pkg.format(datestring)
if os.path.exists(backup_package):
msg = "Backup package exists: {}".format(backup_package)
action_set({"fail.message": msg})
return False
with tempfile.TemporaryDirectory() as tmpdir:
# Create a temporary path to perform the backup, and date the contents.
dated_path = "{0}/etcd_migration_{1}".format(tmpdir, datestring)
os.makedirs(dated_path)
# backup all the configuration data
for p in deb_paths["config"]:
if os.path.exists(p):
shutil.copy(p, dated_path)
else:
log("Skipping copy for: {} - file not found".format(p), "WARN")
# backup the actual state of etcd's data
for p in deb_paths["data"]:
if os.path.exists(p):
cmd = "rsync -avzp {} {}".format(p, dated_path)
check_call(split(cmd))
try:
# Create the tarball in its final location
shutil.make_archive(backup_package, "gztar", tmpdir)
except Exception as ex:
action_set({"fail.message": ex.message})
return False
log("Created backup {}".format(backup_package))
return True
def install_snap(channel, classic=False):
"""Handle installation of snaps, both from resources and from the snap
store. The only indicator we need is classic mode and the channel"""
snap.install("etcd", channel=channel, classic=classic)
def deb_to_snap_migration():
has_migrated = has_migrated_from_deb()
if not has_migrated:
try:
cmd = "/snap/bin/etcd.ingest"
check_call(split(cmd))
except CalledProcessError as cpe:
log("Error encountered during ingest.", "ERROR")
log("Error message: {}".format(cpe.message))
action_fail("Migration failed")
for key_path in snap_paths["client"]:
chmod = "chmod 644 {}".format(key_path)
call(split(chmod))
cmod = "chmod 755 {}".format(snap_paths["common"])
call(split(cmod))
def purge_deb_files():
probe_package_command = "dpkg --list etcd"
return_code = call(split(probe_package_command))
if return_code != 0:
# The return code from dpkg --list when the package is
# non existant
action_set(
{
"dpkg.list.message": "dpkg probe return_code > 0",
"skip.package.purge": "True",
}
)
return
log("Purging deb configuration files post migration", "INFO")
cmd = "apt-get purge -y etcd"
try:
check_call(split(cmd))
except CalledProcessError as cpe:
action_fail({"apt.purge.message": cpe.message})
for f in deb_paths["config"]:
try:
log("Removing file {}".format(f), "INFO")
os.remove(f)
except FileNotFoundError:
k = "purge.missing.{}".format(os.path.basename(f))
msg = "Did not purge {}. File not found.".format(f)
action_set({k: msg})
except:
k = "purge.error.{}".format(f)
msg = "Failed to purge {}. Manual removal required.".format(k)
action_set({k: msg})
def has_migrated_from_deb():
for p in snap_paths["config"]:
# helpful when debugging
log("Scanning for file: {} {}".format(p, os.path.exists(p)), "DEBUG")
if not os.path.exists(p):
return False
return True
if __name__ == "__main__":
# Control flow of the action
backup_package = action_get("target")
backup = action_get("backup")
# We need to determine a default fallback channel since, there isn't an "auto" channel
channel = get_target_etcd_channel()
if not channel:
channel = "3.4/stable"
if backup:
backup_status = create_migration_backup(backup_package)
if not backup_status:
action_fail("Failed creating the backup. Refusing to proceed.")
sys.exit(0)
if not is_state("etcd.deb.migrated"):
install_snap("ingest/stable", True)
deb_to_snap_migration()
install_snap(channel, False)
purge_deb_files()
remove_state("etcd.installed")
set_state("snap.installed.etcd")
remove_state("etcd.pillowmints")
unitdata.kv().flush()
call(["hooks/config-changed"])