Merge "Ensure auth_encryption_key is identical for all units"
This commit is contained in:
commit
d9c603d0fb
@ -12,11 +12,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
|
||||
from charmhelpers.contrib.openstack import context
|
||||
from charmhelpers.core.hookenv import config, leader_get
|
||||
from charmhelpers.core.host import pwgen
|
||||
from charmhelpers.contrib.hahelpers.cluster import (
|
||||
determine_apache_port,
|
||||
determine_api_port,
|
||||
@ -51,21 +48,10 @@ class HeatIdentityServiceContext(context.IdentityServiceContext):
|
||||
|
||||
|
||||
def get_encryption_key():
|
||||
encryption_path = os.path.join(HEAT_PATH, 'encryption-key')
|
||||
if os.path.isfile(encryption_path):
|
||||
with open(encryption_path, 'r') as enc:
|
||||
encryption = enc.read()
|
||||
else:
|
||||
# create encryption key and store it
|
||||
if not os.path.isdir(HEAT_PATH):
|
||||
os.makedirs(HEAT_PATH)
|
||||
encryption = config("encryption-key")
|
||||
if not encryption:
|
||||
# generate random key
|
||||
encryption = pwgen(16)
|
||||
with open(encryption_path, 'w') as enc:
|
||||
enc.write(encryption)
|
||||
return encryption
|
||||
encryption_key = config("encryption-key")
|
||||
if not encryption_key:
|
||||
encryption_key = leader_get('heat-auth-encryption-key')
|
||||
return encryption_key
|
||||
|
||||
|
||||
class HeatSecurityContext(context.OSContextGenerator):
|
||||
@ -73,10 +59,9 @@ class HeatSecurityContext(context.OSContextGenerator):
|
||||
def __call__(self):
|
||||
ctxt = {}
|
||||
# check if we have stored encryption key
|
||||
encryption = get_encryption_key()
|
||||
ctxt['encryption_key'] = encryption
|
||||
ctxt['heat_domain_admin_passwd'] = \
|
||||
leader_get('heat-domain-admin-passwd')
|
||||
ctxt['encryption_key'] = get_encryption_key()
|
||||
ctxt['heat_domain_admin_passwd'] = (
|
||||
leader_get('heat-domain-admin-passwd'))
|
||||
return ctxt
|
||||
|
||||
|
||||
|
@ -95,6 +95,7 @@ from heat_utils import (
|
||||
|
||||
from heat_context import (
|
||||
API_PORTS,
|
||||
HEAT_PATH,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.openstack.context import ADDRESS_TYPES
|
||||
@ -154,6 +155,25 @@ def config_changed():
|
||||
@hooks.hook('upgrade-charm')
|
||||
@harden()
|
||||
def upgrade_charm():
|
||||
if is_leader():
|
||||
# if we are upgrading, then the old version might have used the
|
||||
# HEAT_PATH/encryption-key. So we grab the key from that, and put it in
|
||||
# leader settings to ensure that the key remains the same during an
|
||||
# upgrade.
|
||||
encryption_path = os.path.join(HEAT_PATH, 'encryption-key')
|
||||
if os.path.isfile(encryption_path):
|
||||
with open(encryption_path, 'r') as f:
|
||||
encryption_key = f.read()
|
||||
try:
|
||||
leader_set({'heat-auth-encryption-key': encryption_key})
|
||||
except subprocess.CalledProcessError as e:
|
||||
log("upgrade: leader_set: heat-auth-encryption-key failed,"
|
||||
" didn't delete the existing file: {}.\n"
|
||||
"Error was: ".format(encryption_path, str(e)),
|
||||
level=WARNING)
|
||||
else:
|
||||
# now we just delete the file
|
||||
os.remove(encryption_path)
|
||||
leader_elected()
|
||||
|
||||
|
||||
@ -283,8 +303,19 @@ def relation_broken():
|
||||
|
||||
@hooks.hook('leader-elected')
|
||||
def leader_elected():
|
||||
if is_leader() and not leader_get('heat-domain-admin-passwd'):
|
||||
if is_leader():
|
||||
if not leader_get('heat-domain-admin-passwd'):
|
||||
try:
|
||||
leader_set({'heat-domain-admin-passwd': pwgen(32)})
|
||||
except subprocess.CalledProcessError as e:
|
||||
log('leader_set: heat-domain-admin-password failed: {}'
|
||||
.format(str(e)), level=WARNING)
|
||||
if not leader_get('heat-auth-encryption-key'):
|
||||
try:
|
||||
leader_set({'heat-auth-encryption-key': pwgen(32)})
|
||||
except subprocess.CalledProcessError as e:
|
||||
log('leader_set: heat-domain-admin-password failed: {}'
|
||||
.format(str(e)), level=WARNING)
|
||||
|
||||
|
||||
@hooks.hook('cluster-relation-joined')
|
||||
|
@ -17,6 +17,9 @@
|
||||
"""
|
||||
Basic heat functional test.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
import amulet
|
||||
from heatclient.common import template_utils
|
||||
|
||||
@ -69,7 +72,9 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
|
||||
and the rest of the service are from lp branches that are
|
||||
compatible with the local charm (e.g. stable or next).
|
||||
"""
|
||||
this_service = {'name': 'heat', 'constraints': {'mem': '2G'}}
|
||||
this_service = {'name': 'heat',
|
||||
'constraints': {'mem': '2G'},
|
||||
'units': 2}
|
||||
other_services = [
|
||||
{'name': 'keystone'},
|
||||
{'name': 'rabbitmq-server'},
|
||||
@ -417,10 +422,7 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
|
||||
expected = {
|
||||
'private-address': u.valid_ip,
|
||||
'db_host': u.valid_ip,
|
||||
'heat_allowed_units': '{}/{}'.format(
|
||||
self.heat_sentry.info['service'],
|
||||
self.heat_sentry.info['unit']
|
||||
),
|
||||
'heat_allowed_units': u.not_null,
|
||||
'heat_password': u.not_null
|
||||
}
|
||||
|
||||
@ -613,6 +615,52 @@ class HeatBasicDeployment(OpenStackAmuletDeployment):
|
||||
self._image_delete()
|
||||
self._keypair_delete()
|
||||
|
||||
def test_500_auth_encryption_key_same_on_units(self):
|
||||
"""Test that the auth_encryption_key in heat.conf is the same on all of
|
||||
the units.
|
||||
"""
|
||||
u.log.debug("Checking the 'auth_encryption_key' is the same on "
|
||||
"all units.")
|
||||
output, ret = self._run_arbitrary(
|
||||
"--application heat "
|
||||
"--format json "
|
||||
"grep auth_encryption_key /etc/heat/heat.conf")
|
||||
if ret:
|
||||
msg = "juju run returned error: ({}) -> {}".format(ret, output)
|
||||
amulet.raise_status(amulet.FAIL, msg=msg)
|
||||
output = json.loads(output)
|
||||
keys = {}
|
||||
for r in output:
|
||||
k = r['Stdout'].split('=')[1].strip()
|
||||
keys[r['UnitId']] = k
|
||||
# see if keys are different.
|
||||
ks = keys.values()
|
||||
if any(((k != ks[0]) for k in ks[1:])):
|
||||
msg = ("'auth_encryption_key' is not identical on every unit: {}"
|
||||
.format("{}={}".format(k, v) for k, v in keys.items()))
|
||||
amulet.raise_status(amulet.FAIL, msg=msg)
|
||||
|
||||
@staticmethod
|
||||
def _run_arbitrary(command, timeout=300):
|
||||
"""Run an arbitrary command (as root), but not necessarily on a unit.
|
||||
|
||||
(Otherwise the self.run(...) command could have been used for the unit
|
||||
|
||||
:param str command: The command to run.
|
||||
:param int timeout: Seconds to wait before timing out.
|
||||
:return: A 2-tuple containing the output of the command and the exit
|
||||
code of the command.
|
||||
"""
|
||||
cmd = ['juju', 'run', '--timeout', "{}s".format(timeout),
|
||||
] + command.split()
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
output = stdout if p.returncode == 0 else stderr
|
||||
return output.decode('utf8').strip(), p.returncode
|
||||
|
||||
def test_900_heat_restart_on_config_change(self):
|
||||
"""Verify that the specified services are restarted when the config
|
||||
is changed."""
|
||||
|
@ -12,10 +12,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from mock import call, patch, MagicMock
|
||||
from test_utils import CharmTestCase
|
||||
from test_utils import CharmTestCase, patch_open
|
||||
|
||||
# python-apt is not installed as part of test-requirements but is imported by
|
||||
# some charmhelpers modules so create a fake import.
|
||||
@ -44,6 +45,9 @@ TO_PATCH = [
|
||||
# charmhelpers.core.hookenv
|
||||
'Hooks',
|
||||
'config',
|
||||
'is_leader',
|
||||
'leader_set',
|
||||
'log',
|
||||
'open_port',
|
||||
'relation_set',
|
||||
'related_units',
|
||||
@ -123,6 +127,30 @@ class HeatRelationTests(CharmTestCase):
|
||||
|
||||
self.assertFalse(self.do_openstack_upgrade.called)
|
||||
|
||||
@patch('os.path.isfile')
|
||||
@patch('os.remove')
|
||||
@patch.object(relations, 'leader_elected')
|
||||
def test_upgrade_charm(self, leader_elected, os_remove, os_path_isfile):
|
||||
os_path_isfile.return_value = False
|
||||
self.is_leader.return_value = False
|
||||
relations.upgrade_charm()
|
||||
leader_elected.assert_called_once_with()
|
||||
os_path_isfile.assert_not_called()
|
||||
# now say we are the leader
|
||||
self.is_leader.return_value = True
|
||||
os_path_isfile.return_value = False
|
||||
relations.upgrade_charm()
|
||||
self.leader_set.assert_not_called()
|
||||
os_path_isfile.return_value = True
|
||||
with patch_open() as (mock_open, mock_file):
|
||||
mock_file.read.return_value = "abc"
|
||||
relations.upgrade_charm()
|
||||
file = os.path.join(relations.HEAT_PATH, 'encryption-key')
|
||||
mock_open.assert_called_once_with(file, 'r')
|
||||
self.leader_set.assert_called_once_with(
|
||||
{'heat-auth-encryption-key': 'abc'})
|
||||
os_remove.assert_called_once_with(file)
|
||||
|
||||
def test_db_joined(self):
|
||||
self.get_relation_ip.return_value = '192.168.20.1'
|
||||
relations.db_joined()
|
||||
|
Loading…
Reference in New Issue
Block a user