Charmed-Kubernetes/flannel/reactive/flannel.py

360 lines
12 KiB
Python

import os
import json
from shlex import split
from subprocess import check_output, check_call, CalledProcessError, STDOUT
from charms.flannel.common import retry
from charms.reactive import set_state, remove_state, when, when_not, hook
from charms.reactive import when_any
from charms.templating.jinja2 import render
from charmhelpers.core.host import service_start, service_stop, service_restart
from charmhelpers.core.host import service_running, service
from charmhelpers.core.hookenv import log, resource_get
from charmhelpers.core.hookenv import config, application_version_set
from charmhelpers.core.hookenv import network_get
from charmhelpers.contrib.charmsupport import nrpe
from charms.reactive.helpers import data_changed
from charms.layer import status
ETCD_PATH = '/etc/ssl/flannel'
ETCD_KEY_PATH = os.path.join(ETCD_PATH, 'client-key.pem')
ETCD_CERT_PATH = os.path.join(ETCD_PATH, 'client-cert.pem')
ETCD_CA_PATH = os.path.join(ETCD_PATH, 'client-ca.pem')
@when_not('flannel.binaries.installed')
def install_flannel_binaries():
''' Unpack the Flannel binaries. '''
try:
resource_name = 'flannel-{}'.format(arch())
archive = resource_get(resource_name)
except Exception:
message = 'Error fetching the flannel resource.'
log(message)
status.blocked(message)
return
if not archive:
message = 'Missing flannel resource.'
log(message)
status.blocked(message)
return
filesize = os.stat(archive).st_size
if filesize < 1000000:
message = 'Incomplete flannel resource'
log(message)
status.blocked(message)
return
status.maintenance('Unpacking flannel resource.')
charm_dir = os.getenv('CHARM_DIR')
unpack_path = os.path.join(charm_dir, 'files', 'flannel')
os.makedirs(unpack_path, exist_ok=True)
cmd = ['tar', 'xfz', archive, '-C', unpack_path]
log(cmd)
check_call(cmd)
apps = [
{'name': 'flanneld', 'path': '/usr/local/bin'},
{'name': 'etcdctl', 'path': '/usr/local/bin'}
]
for app in apps:
unpacked = os.path.join(unpack_path, app['name'])
app_path = os.path.join(app['path'], app['name'])
install = ['install', '-v', '-D', unpacked, app_path]
check_call(install)
set_state('flannel.binaries.installed')
@when('cni.is-worker')
@when_not('flannel.cni.configured')
def configure_cni(cni):
''' Set up the flannel cni configuration file. '''
render('10-flannel.conflist', '/etc/cni/net.d/10-flannel.conflist', {})
set_state('flannel.cni.configured')
@when('etcd.tls.available')
@when_not('flannel.etcd.credentials.installed')
def install_etcd_credentials(etcd):
''' Install the etcd credential files. '''
etcd.save_client_credentials(ETCD_KEY_PATH, ETCD_CERT_PATH, ETCD_CA_PATH)
set_state('flannel.etcd.credentials.installed')
def default_route_interface():
''' Returns the network interface of the system's default route '''
default_interface = None
cmd = ['route']
output = check_output(cmd).decode('utf8')
for line in output.split('\n'):
if 'default' in line:
default_interface = line.split(' ')[-1]
return default_interface
def get_bind_address_interface():
''' Returns a non-fan bind-address interface for the cni endpoint.
Falls back to default_route_interface() if bind-address is not available.
'''
try:
data = network_get('cni')
except NotImplementedError:
# Juju < 2.1
return default_route_interface()
if 'bind-addresses' not in data:
# Juju < 2.3
return default_route_interface()
for bind_address in data['bind-addresses']:
if bind_address['interfacename'].startswith('fan-'):
continue
return bind_address['interfacename']
# If we made it here, we didn't find a non-fan CNI bind-address, which is
# unexpected. Let's log a message and play it safe.
log('Could not find a non-fan bind-address. Using fallback interface.')
return default_route_interface()
@when('flannel.binaries.installed', 'flannel.etcd.credentials.installed',
'etcd.tls.available')
@when_not('flannel.service.installed')
def install_flannel_service(etcd):
''' Install the flannel service. '''
status.maintenance('Installing flannel service.')
# keep track of our etcd conn string and cert info so we can detect when it
# changes later
data_changed('flannel_etcd_connections', etcd.get_connection_string())
data_changed('flannel_etcd_client_cert', etcd.get_client_credentials())
iface = config('iface') or get_bind_address_interface()
context = {'iface': iface,
'connection_string': etcd.get_connection_string(),
'cert_path': ETCD_PATH}
render('flannel.service', '/lib/systemd/system/flannel.service', context)
service('enable', 'flannel')
set_state('flannel.service.installed')
remove_state('flannel.service.started')
@when('config.changed.iface')
def reconfigure_flannel_service():
''' Handle interface configuration change. '''
remove_state('flannel.service.installed')
@when('etcd.available', 'flannel.service.installed')
def etcd_changed(etcd):
if data_changed('flannel_etcd_connections', etcd.get_connection_string()):
remove_state('flannel.service.installed')
if data_changed('flannel_etcd_client_cert', etcd.get_client_credentials()):
etcd.save_client_credentials(ETCD_KEY_PATH,
ETCD_CERT_PATH,
ETCD_CA_PATH)
remove_state('flannel.service.installed')
@when('flannel.binaries.installed', 'flannel.etcd.credentials.installed',
'etcd.available')
@when_not('flannel.network.configured')
def invoke_configure_network(etcd):
''' invoke network configuration and adjust states '''
status.maintenance('Negotiating flannel network subnet.')
if configure_network(etcd):
set_state('flannel.network.configured')
remove_state('flannel.service.started')
else:
status.waiting('Waiting on etcd.')
@retry(times=3, delay_secs=20)
def configure_network(etcd):
''' Store initial flannel data in etcd.
Returns True if the operation completed successfully.
'''
flannel_config = {
'Network': config('cidr'),
'Backend': {
'Type': 'vxlan'
}
}
vni = config('vni')
if vni:
flannel_config['Backend']['VNI'] = vni
port = config('port')
if port:
flannel_config['Backend']['Port'] = port
data = json.dumps(flannel_config)
cmd = "etcdctl "
cmd += "--endpoint '{0}' ".format(etcd.get_connection_string())
cmd += "--cert-file {0} ".format(ETCD_CERT_PATH)
cmd += "--key-file {0} ".format(ETCD_KEY_PATH)
cmd += "--ca-file {0} ".format(ETCD_CA_PATH)
cmd += "set /coreos.com/network/config '{0}'".format(data)
try:
check_call(split(cmd))
return True
except CalledProcessError:
log('Unexpected error configuring network. Assuming etcd not'
' ready. Will retry in 20s')
return False
@when_any('config.changed.cidr', 'config.changed.port', 'config.changed.vni')
def reconfigure_network():
''' Trigger the network configuration method. '''
remove_state('flannel.network.configured')
@when('flannel.binaries.installed', 'flannel.service.installed',
'flannel.network.configured')
@when_not('flannel.service.started')
def start_flannel_service():
''' Start the flannel service. '''
status.maintenance('Starting flannel service.')
if service_running('flannel'):
service_restart('flannel')
else:
service_start('flannel')
set_state('flannel.service.started')
@when('cni.connected', 'flannel.service.started')
@when_any('flannel.cni.configured', 'cni.is-master')
@when_not('flannel.cni.available')
def set_available(cni):
''' Indicate to the CNI provider that we're ready. '''
cni.set_config(cidr=config('cidr'), cni_conf_file='10-flannel.conflist')
set_state('flannel.cni.available')
@when('flannel.binaries.installed')
@when_not('flannel.version.set')
def set_flannel_version():
''' Surface the currently deployed version of flannel to Juju '''
cmd = 'flanneld -version'
version = check_output(split(cmd), stderr=STDOUT).decode('utf-8')
if version:
application_version_set(version.split('v')[-1].strip())
set_state('flannel.version.set')
@when('nrpe-external-master.available')
@when_not('nrpe-external-master.initial-config')
def initial_nrpe_config(nagios=None):
set_state('nrpe-external-master.initial-config')
update_nrpe_config(nagios)
@when('flannel.service.started')
@when('nrpe-external-master.available')
@when_any('config.changed.nagios_context',
'config.changed.nagios_servicegroups')
def update_nrpe_config(unused=None):
# List of systemd services that will be checked
services = ('flannel',)
# The current nrpe-external-master interface doesn't handle a lot of logic,
# use the charm-helpers code for now.
hostname = nrpe.get_nagios_hostname()
current_unit = nrpe.get_nagios_unit_name()
nrpe_setup = nrpe.NRPE(hostname=hostname, primary=False)
nrpe.add_init_service_checks(nrpe_setup, services, current_unit)
nrpe_setup.write()
@when('flannel.service.started')
@when('flannel.cni.available')
def ready():
''' Indicate that flannel is active. '''
try:
status.active('Flannel subnet ' + get_flannel_subnet())
except FlannelSubnetNotFound:
status.waiting('Waiting for Flannel')
@when_not('etcd.connected')
def halt_execution():
''' send a clear message to the user that we are waiting on etcd '''
status.blocked('Waiting for etcd relation.')
@hook('upgrade-charm')
def reset_states_and_redeploy():
''' Remove state and redeploy '''
remove_state('flannel.cni.available')
remove_state('flannel.binaries.installed')
remove_state('flannel.service.started')
remove_state('flannel.version.set')
remove_state('flannel.network.configured')
remove_state('flannel.service.installed')
remove_state('flannel.cni.configured')
try:
log('Deleting /etc/cni/net.d/10-flannel.conf')
os.remove('/etc/cni/net.d/10-flannel.conf')
except FileNotFoundError as e:
log(str(e))
@hook('pre-series-upgrade')
def pre_series_upgrade():
status.blocked('Series upgrade in progress')
@hook('stop')
def cleanup_deployment():
''' Terminate services, and remove the deployed bins '''
service_stop('flannel')
down = 'ip link set flannel.1 down'
delete = 'ip link delete flannel.1'
try:
check_call(split(down))
check_call(split(delete))
except CalledProcessError:
log('Unable to remove iface flannel.1')
log('Potential indication that cleanup is not possible')
files = ['/usr/local/bin/flanneld',
'/lib/systemd/system/flannel',
'/lib/systemd/system/flannel.service',
'/run/flannel/subnet.env',
'/usr/local/bin/flanneld',
'/usr/local/bin/etcdctl',
'/etc/cni/net.d/10-flannel.conflist',
ETCD_KEY_PATH,
ETCD_CERT_PATH,
ETCD_CA_PATH]
for f in files:
if os.path.exists(f):
log('Removing {}'.format(f))
os.remove(f)
def get_flannel_subnet():
''' Returns the flannel subnet reserved for this unit '''
try:
with open('/run/flannel/subnet.env') as f:
raw_data = dict(line.strip().split('=') for line in f)
return raw_data['FLANNEL_SUBNET']
except FileNotFoundError as e:
raise FlannelSubnetNotFound() from e
def arch():
'''Return the package architecture as a string.'''
# Get the package architecture for this system.
architecture = check_output(['dpkg', '--print-architecture']).rstrip()
# Convert the binary result into a string.
architecture = architecture.decode('utf-8')
return architecture
class FlannelSubnetNotFound(Exception):
pass