179 lines
6.1 KiB
Python
Executable File
179 lines
6.1 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
|
|
|
|
|
|
# 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',
|
|
'/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')
|
|
channel = config('channel')
|
|
|
|
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'])
|