charm-vault/src/reactive/vault.py

349 lines
9.2 KiB
Python

import base64
import hvac
import psycopg2
import requests
import subprocess
from charmhelpers.contrib.charmsupport.nrpe import (
NRPE,
add_init_service_checks,
get_nagios_hostname,
get_nagios_unit_name,
)
from charmhelpers.core.hookenv import (
DEBUG,
config,
log,
open_port,
status_set,
unit_private_ip,
application_version_set,
atexit,
)
from charmhelpers.core.host import (
service_restart,
service_running,
service_start,
write_file,
)
from charmhelpers.core.templating import (
render,
)
from charms.reactive import (
hook,
is_state,
remove_state,
set_state,
when,
when_not,
)
from charms.reactive.relations import (
endpoint_from_flag,
)
# See https://www.vaultproject.io/docs/configuration/storage/postgresql.html
VAULT_TABLE_DDL = """
CREATE TABLE IF NOT EXISTS vault_kv_store (
parent_path TEXT COLLATE "C" NOT NULL,
path TEXT COLLATE "C",
key TEXT COLLATE "C",
value BYTEA,
CONSTRAINT pkey PRIMARY KEY (path, key)
);
"""
VAULT_INDEX_DDL = """
CREATE INDEX IF NOT EXISTS parent_path_idx ON vault_kv_store (parent_path);
"""
VAULT_HEALTH_URL = '{vault_addr}/v1/sys/health'
def get_client():
return hvac.Client(url=get_api_url())
def get_vault_health():
response = requests.get(VAULT_HEALTH_URL.format(vault_addr=get_api_url()))
return response.json()
def can_restart():
safe_restart = False
if not service_running('vault'):
safe_restart = True
else:
client = get_client()
if not client.is_initialized():
safe_restart = True
elif client.is_sealed():
safe_restart = True
log("Safe to restart: {}".format(safe_restart), level=DEBUG)
return safe_restart
def ssl_available(config):
if '' in (config['ssl-cert'], config['ssl-key']):
return False
return True
def configure_vault(context):
context['disable_mlock'] = config()['disable-mlock']
context['ssl_available'] = is_state('vault.ssl.available')
log("Running configure_vault", level=DEBUG)
context['disable_mlock'] = config()['disable-mlock']
context['ssl_available'] = is_state('vault.ssl.available')
etcd = endpoint_from_flag('etcd.available')
if etcd:
log("Etcd detected, adding to context", level=DEBUG)
context['etcd_conn'] = etcd.connection_string()
context['etcd_tls_ca_file'] = '/var/snap/vault/common/etcd-ca.pem'
context['etcd_tls_cert_file'] = '/var/snap/vault/common/etcd-cert.pem'
context['etcd_tls_key_file'] = '/var/snap/vault/common/etcd.key'
etcd.save_client_credentials(
context['etcd_tls_key_file'],
context['etcd_tls_cert_file'],
context['etcd_tls_ca_file'])
context['vault_api_url'] = get_api_url()
log("Etcd detected, setting vault_api_url to {}".format(
context['vault_api_url']))
else:
log("Etcd not detected", level=DEBUG)
status_set('maintenance', 'creating vault config')
log("Rendering vault.hcl.j2", level=DEBUG)
render(
'vault.hcl.j2',
'/var/snap/vault/common/vault.hcl',
context,
perms=0o600)
status_set('maintenance', 'creating vault unit file')
render(
'vault.service.j2',
'/etc/systemd/system/vault.service',
{},
perms=0o644)
status_set('maintenance', 'starting vault')
if can_restart():
service_restart('vault')
else:
service_start('vault') # restart seals the vault
status_set('maintenance', 'opening vault port')
open_port(8200)
set_state('configured')
if config()['disable-mlock']:
status_set(
'active',
'WARNING: DISABLE-MLOCK IS SET -- SECRETS MAY BE LEAKED')
else:
status_set('active', '=^_^=')
def get_api_url():
protocol = 'http'
port = '8200'
ip = unit_private_ip()
if is_state('vault.ssl.available'):
protocol = 'https'
return '{}://{}:{}'.format(protocol, ip, port)
@when('snap.installed.vault')
@when_not('configured')
@when('db.master.available')
@when('vault.schema.created')
@when('vault.ssl.configured')
def configure_vault_psql(psql):
context = {
'storage_name': 'psql',
'psql_db_conn': psql.master,
}
configure_vault(context)
@when('snap.installed.vault')
@when_not('configured')
@when('shared-db.available')
@when('vault.ssl.configured')
def configure_vault_mysql(mysql):
context = {
'storage_name': 'mysql',
'mysql_db_relation': mysql,
}
configure_vault(context)
@when('config.changed.disable-mlock')
def disable_mlock_changed():
remove_state('configured')
@hook('upgrade-charm')
def upgrade_charm():
remove_state('configured')
remove_state('vault.nrpe.configured')
remove_state('vault.ssl.configured')
@when('db.connected')
def request_db(pgsql):
pgsql.set_database('vault')
@when('shared-db.connected')
def mysql_setup(database):
"""Handle the default database connection setup
"""
db = {
'database': 'vault',
'username': 'vault',
}
database.configure(**db)
@when('db.master.available')
@when_not('vault.schema.created')
def create_vault_table(pgsql):
status_set('maintenance', 'connecting to database')
conn = psycopg2.connect(str(pgsql.master))
cur = conn.cursor()
status_set('maintenance', 'creating vault table')
cur.execute(VAULT_TABLE_DDL)
status_set('maintenance', 'creating vault index')
cur.execute(VAULT_INDEX_DDL)
status_set('maintenance', 'committing database schema')
conn.commit()
cur.close()
conn.close()
set_state('vault.schema.created')
status_set('active', 'database schema created and committed')
@when_not('db.master.available')
def database_not_ready():
remove_state('vault.schema.created')
@when('snap.installed.vault')
@when_not('vault.ssl.configured')
def configure_ssl():
c = config()
if ssl_available(c):
status_set('maintenance', 'installing SSL key and cert')
ssl_key = base64.decodestring(c['ssl-key'].encode())
write_file('/var/snap/vault/common/vault.key', ssl_key, perms=0o600)
ssl_cert = base64.decodestring(c['ssl-cert'].encode())
if c['ssl-chain']:
ssl_cert = ssl_cert + base64.decodestring(c['ssl-chain'].encode())
write_file('/var/snap/vault/common/vault.crt', ssl_cert, perms=0o600)
set_state('vault.ssl.available')
else:
remove_state('vault.ssl.available')
if c['ssl-ca']:
ssl_ca = base64.decodestring(c['ssl-ca'].encode())
write_file('/usr/local/share/ca-certificates/vault-ca.crt',
ssl_ca, perms=0o644)
subprocess.check_call(['update-ca-certificates', '--fresh'])
set_state('vault.ssl.configured')
status_set('active', 'SSL key and cert installed')
remove_state('configured')
@when('config.changed.ssl-cert')
def ssl_cert_changed():
remove_state('vault.ssl.configured')
@when('config.changed.ssl-chain')
def ssl_chain_changed():
remove_state('vault.ssl.configured')
@when('config.changed.ssl-key')
def ssl_key_changed():
remove_state('vault.ssl.configured')
@when('config.changed.ssl-ca')
def ssl_ca_changed():
remove_state('vault.ssl.configured')
@when_not('etcd.local.configured')
@when('etcd.available')
def etcd_setup(etcd):
log("Detected etcd.available, removing configured", level=DEBUG)
remove_state('configured')
remove_state('etcd.local.unconfigured')
set_state('etcd.local.configured')
@when_not('etcd.local.unconfigured')
@when_not('etcd.available')
def etcd_not_ready():
log("Detected etcd_not_ready, removing configured", level=DEBUG)
set_state('etcd.local.unconfigured')
remove_state('etcd.local.configured')
remove_state('configured')
@when('configured')
@when('nrpe-external-master.available')
@when_not('vault.nrpe.configured')
def update_nagios(svc):
status_set('maintenance', 'configuring Nagios checks')
hostname = get_nagios_hostname()
current_unit = get_nagios_unit_name()
nrpe = NRPE(hostname=hostname)
add_init_service_checks(nrpe, ['vault'], current_unit)
write_file(
'/usr/lib/nagios/plugins/check_vault_version.py',
open('files/nagios/check_vault_version.py', 'rb').read(),
perms=0o755)
nrpe.add_check(
'vault_version',
'Check running vault server version is same as installed snap',
'/usr/lib/nagios/plugins/check_vault_version.py',
)
nrpe.write()
set_state('vault.nrpe.configured')
status_set('active', 'Nagios checks configured')
@when('config.changed.nagios_context')
def nagios_context_changed():
remove_state('vault.nrpe.configured')
@when('config.changed.nagios_servicegroups')
def nagios_servicegroups_changed():
remove_state('vault.nrpe.configured')
@when('snap.installed.vault')
def prime_assess_status():
atexit(_assess_status)
def _assess_status():
if service_running('vault'):
health = get_vault_health()
application_version_set(health.get('version'))
@when('ha.connected')
def cluster_connected(hacluster):
"""Configure HA resources in corosync"""
vip = config('vip')
hacluster.add_vip('vault', vip)
hacluster.bind_resources()