197 lines
6.2 KiB
Python
Executable File
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"])
|