charm-vault/src/actions/actions.py

270 lines
8.6 KiB
Python
Executable File

#!/usr/local/sbin/charm-env python3
# Copyright 2018 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import os
import sys
from traceback import format_exc
# Load modules from $CHARM_DIR/lib
sys.path.append('lib')
from charms.layer import basic
basic.bootstrap_charm_deps()
basic.init_config_states()
import charmhelpers.core.hookenv as hookenv
import charmhelpers.core.unitdata as unitdata
import charmhelpers.core.host as host
import charm.vault as vault
import charm.vault_pki as vault_pki
import charms.reactive
import reactive.vault_handlers as handlers # noqa: E402
from charms.reactive.flags import set_flag, clear_flag
def authorize_charm_action(*args):
"""Create a role allowing the charm to perform certain vault actions.
"""
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
return
action_config = hookenv.action_get()
role_id = vault.setup_charm_vault_access(action_config['token'])
hookenv.leader_set({vault.CHARM_ACCESS_ROLE_ID: role_id})
set_flag('secrets.refresh')
def refresh_secrets(*args):
"""Refresh secret_id's and re-issue tokens for secret_id retrieval
on secrets end-points"""
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
set_flag('secrets.refresh')
def get_intermediate_csrs(*args):
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
return
action_config = hookenv.action_get() or {}
csrs = vault_pki.get_csr(
ttl=action_config.get('ttl'),
country=action_config.get('country'),
common_name=action_config.get('common-name'),
locality=action_config.get('locality'),
province=action_config.get('province'),
organization=action_config.get('organization'),
organizational_unit=action_config.get('organizational-unit'))
# We have to clear both the reactive flag, as well as the leadership
# managed root-ca option, otherwise, we will end up with the flag being
# reset in the reactive handler after this is run.
clear_flag('charm.vault.ca.ready')
hookenv.leader_set(
{'root-ca': None})
hookenv.action_set({'output': csrs})
def upload_signed_csr(*args):
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
return
action_config = hookenv.action_get()
root_ca = action_config.get('root-ca')
if root_ca:
hookenv.leader_set(
{'root-ca': base64.b64decode(root_ca).decode("utf-8")})
vault_pki.upload_signed_csr(
base64.b64decode(action_config['pem']).decode("utf-8"),
allowed_domains=action_config.get('allowed-domains'),
allow_subdomains=action_config.get('allow-subdomains'),
enforce_hostnames=action_config.get('enforce-hostnames'),
allow_any_name=action_config.get('allow-any-name'),
max_ttl=action_config.get('max-ttl'))
set_flag('charm.vault.ca.ready')
set_flag('pki.backend.tuned')
# reissue any certificates we might previously have provided
set_flag('certificates.reissue.requested')
set_flag('certificates.reissue.global.requested')
def generate_root_ca(*args):
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
return
action_config = hookenv.action_get()
root_ca = vault_pki.generate_root_ca(
ttl=action_config['ttl'],
allow_any_name=action_config['allow-any-name'],
allowed_domains=action_config['allowed-domains'],
allow_bare_domains=action_config['allow-bare-domains'],
allow_subdomains=action_config['allow-subdomains'],
allow_glob_domains=action_config['allow-glob-domains'],
enforce_hostnames=action_config['enforce-hostnames'],
max_ttl=action_config['max-ttl'])
hookenv.leader_set({'root-ca': root_ca})
hookenv.action_set({'output': root_ca})
set_flag('charm.vault.ca.ready')
set_flag('pki.backend.tuned')
# reissue any certificates we might previously have provided
set_flag('certificates.reissue.requested')
set_flag('certificates.reissue.global.requested')
def get_root_ca(*args):
hookenv.action_set({'output': vault_pki.get_ca()})
def disable_pki(*args):
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
return
vault_pki.disable_pki_backend()
clear_flag('charm.vault.ca.ready')
clear_flag('pki.backend.tuned')
hookenv.leader_set({'root-ca': None})
def reissue_certificates(*args):
set_flag('certificates.reissue.requested')
set_flag('certificates.reissue.global.requested')
def pause(args):
"""Pauses the Vault service.
The result of this action will be to have vault daemon
stopped. juju will report the unit status as "Blocked, vault
service not running".
"""
handlers.pause_vault_unit()
def resume(args):
"""Resumes the Vault service.
The result of this action will be to have vault daemon
resumed. User will have to unseal the service. juju will report
the unit status as "Blocked, unit is sealed".
"""
handlers.resume_vault_unit()
def restart(args):
"""Restart the Vault service.
The result of this action will be to have vault daemon
restarted.
Mind that this action will cause the Vault to be sealed.
"""
host.service_restart(service_name='vault')
def reload(args):
"""Reload the Vault service.
The result of this action will be to have vault daemon
reloaded (preferably with HUP signal in systemd).
That allows for live changes in listener (only certs)
without need of User intervention to unseal the vault.
Unfortunately other options like disable_mlock, ui
are not supported.
"""
host.service_reload(service_name='vault')
def generate_cert(*args):
"""Generates a certificate and sets it in the action output.
The certificate parameters are provided via the action parameters from
the user. If the current unit is not the leader or the vault calls fail,
this will result in a failed command.
"""
if not hookenv.is_leader():
hookenv.action_fail('Please run action on lead unit')
return
action_config = hookenv.action_get()
sans_list = action_config.get('sans')
try:
new_crt = vault_pki.generate_certificate(
cert_type='server',
common_name=action_config.get('common-name'),
sans=list(sans_list.split()),
ttl=action_config.get('ttl'),
max_ttl=action_config.get('max-ttl'))
hookenv.action_set({'output': new_crt})
except vault.VaultError as e:
hookenv.action_fail(str(e))
# Actions to function mapping, to allow for illegal python action names that
# can map to a python function.
ACTIONS = {
"authorize-charm": authorize_charm_action,
"refresh-secrets": refresh_secrets,
"get-csr": get_intermediate_csrs,
"upload-signed-csr": upload_signed_csr,
"reissue-certificates": reissue_certificates,
"generate-root-ca": generate_root_ca,
"get-root-ca": get_root_ca,
"disable-pki": disable_pki,
"pause": pause,
"resume": resume,
"restart": restart,
"reload": reload,
"generate-certificate": generate_cert
}
def main(args):
action_name = os.path.basename(args[0])
try:
action = ACTIONS[action_name]
except KeyError:
return "Action %s undefined" % action_name
else:
try:
action(args)
except vault.VaultError as e:
hookenv.action_fail(str(e))
except Exception:
exc = format_exc()
hookenv.log(exc, hookenv.ERROR)
hookenv.action_fail(exc.splitlines()[-1])
else:
# we were successful, so commit changes from the action
unitdata.kv().flush()
# try running handlers based on new state
try:
charms.reactive.main()
except Exception:
exc = format_exc()
hookenv.log(exc, hookenv.ERROR)
hookenv.action_fail(exc.splitlines()[-1])
if __name__ == "__main__":
sys.exit(main(sys.argv))