Add amulet/bundle tests to charm-barbican-softhsm

This adds keystone v2 and v3 tests to the charm.
However, because of bug#1611393 the tests can't execute test 400 which
would check the API to barbican.  Thus, it is disabled and the tests
only check that the relation is set up properly.

Change-Id: Ibfcdd82b070f7688a815fcbb1a089090d9529e2a
This commit is contained in:
Alex Kavanagh 2016-08-09 16:43:23 +00:00
parent bb82897cda
commit 2651c4a038
9 changed files with 423 additions and 2 deletions

View File

@ -1,5 +1,13 @@
# Barbican SoftHSM2 Plugin
**Barbican + SoftHSM2 + OpenSSL < 1.0.2h is broken**
This charm cannot be used at present as Barbican expects a mechanism in the
PKCS#11 library that SoftHSM2 + OpenSSL < 1.0.2h does not support.
However, this charm can _still_ be used as a basis for implementing _actual_
hardward HSM charms, along with the `interface-barbican-hsm` interface.
Barbican is a REST API designed for the secure storage, provisioning and
management of secrets. It is aimed at being useful for all environments,
including large ephemeral Clouds. (see [Barbican

View File

@ -1 +1,20 @@
# Write me
# Overview
This charm provides the SoftHSM2 HSM plugin to Barbican. **Note that this plugin DOES NOT WORK at present due to
[bug#1611393](https://bugs.launchpad.net/barbican/+bug/1611393). It does, however, demonstrate how an HSM
plugin will work with Barbican.**
# Usage
barbican-softhsm is a subordinate charm and lives in the same unit as a barbican charm.
juju deploy barbican
juju deploy ... other services for barbican -- see barbican charm
juju deploy barbican-softhsm
juju add-relation barbican barbican-softhsm
# Bugs
Please report bugs on [Launchpad](https://bugs.launchpad.net/charm-barbican-softhsm/+filebug).
For general questions please refer to the OpenStack [Charm Guide](https://github.com/openstack/charm-guide).

View File

@ -14,6 +14,8 @@ description: |
The barbican-softhsm is for testing purposes only, and demonstrates
the barbican-hsm interface for interfacing real HSM providers with
Barbican.
tags:
- openstack
provides:
hsm:
interface: barbican-hsm

View File

@ -7,6 +7,7 @@ bzr+lp:charm-helpers#egg=charmhelpers
amulet>=1.14.3,<2.0
bundletester>=0.6.1,<1.0
python-keystoneclient>=1.7.1,<2.0
python-barbicanclient>=4.0.1,<5.0
python-designateclient>=1.5,<2.0
python-cinderclient>=1.4.0,<2.0
python-glanceclient>=1.1.0,<2.0

View File

@ -0,0 +1,354 @@
import amulet
import json
import subprocess
import time
import barbicanclient.client as barbican_client
from keystoneclient import session as keystone_session
from keystoneclient.auth import identity as keystone_identity
import keystoneclient.exceptions
from keystoneclient.v2_0 import client as keystone_v2_0_client
from keystoneclient.v3 import client as keystone_v3_client
from charmhelpers.contrib.openstack.amulet.deployment import (
OpenStackAmuletDeployment
)
from charmhelpers.contrib.openstack.amulet.utils import (
OpenStackAmuletUtils,
DEBUG,
)
# Use DEBUG to turn on debug logging
u = OpenStackAmuletUtils(DEBUG)
class SoftHSMBasicDeployment(OpenStackAmuletDeployment):
"""Amulet tests on a basic SoftHSM deployment."""
def __init__(self, series, openstack=None, source=None, stable=False,
keystone_version='2'):
"""Deploy the entire test environment.
The keystone_version controls whether keystone (and barbican) are set
up to use keystone v2.0 or v3. The options are <string> 2 or 3.
"""
super(SoftHSMBasicDeployment, self).__init__(
series, openstack, source, stable)
self._keystone_version = str(keystone_version)
self._add_services()
self._add_relations()
self._configure_services(keystone_version)
self._deploy()
u.log.info('Waiting on extended status checks...')
exclude_services = ['mysql', ]
self._auto_wait_for_status(exclude_services=exclude_services)
self._initialize_tests()
def _add_services(self):
"""Add services
Add the services that we're testing, where softhsm is local,
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': 'barbican-softhsm'}
other_services = [{'name': 'barbican'},
{'name': 'mysql'},
{'name': 'rabbitmq-server'},
{'name': 'keystone'}]
super(SoftHSMBasicDeployment, self)._add_services(
this_service, other_services)
def _add_relations(self):
"""Add all of the relations for the services."""
relations = {
'barbican:hsm': 'barbican-softhsm:hsm',
'barbican:shared-db': 'mysql:shared-db',
'barbican:amqp': 'rabbitmq-server:amqp',
'barbican:identity-service': 'keystone:identity-service',
'keystone:shared-db': 'mysql:shared-db',
}
super(SoftHSMBasicDeployment, self)._add_relations(relations)
def _configure_services(self, keystone_version='2'):
"""Configure all of the services."""
keystone_config = {
'admin-password': 'openstack',
'admin-token': 'ubuntutesting',
'preferred-api-version': str(keystone_version),
}
# say we don't need an HSM for these tests
barbican_config = {
'require-hsm-plugin': True,
'verbose': True,
'keystone-api-version': str(keystone_version),
}
configs = {
'keystone': keystone_config,
'barbican': barbican_config,
}
super(SoftHSMBasicDeployment, self)._configure_services(configs)
def _initialize_tests(self):
"""Perform final initialization before tests get run."""
# Access the sentries for inspecting service units
self.softhsm_sentry = self.d.sentry['barbican-softhsm'][0]
self.barbican_sentry = self.d.sentry['barbican'][0]
self.mysql_sentry = self.d.sentry['mysql'][0]
self.keystone_sentry = self.d.sentry['keystone'][0]
self.rabbitmq_sentry = self.d.sentry['rabbitmq-server'][0]
u.log.debug('openstack release val: {}'.format(
self._get_openstack_release()))
u.log.debug('openstack release str: {}'.format(
self._get_openstack_release_string()))
keystone_ip = self.keystone_sentry.relation(
'shared-db', 'mysql:shared-db')['private-address']
# We need to auth either to v2.0 or v3 keystone
if self._keystone_version == '2':
ep = ("http://{}:35357/v2.0"
.format(keystone_ip.strip().decode('utf-8')))
auth = keystone_identity.v2.Password(
username='admin',
password='openstack',
tenant_name='admin',
auth_url=ep)
keystone_client_lib = keystone_v2_0_client
elif self._keystone_version == '3':
ep = ("http://{}:35357/v3"
.format(keystone_ip.strip().decode('utf-8')))
auth = keystone_identity.v3.Password(
user_domain_name='admin_domain',
username='admin',
password='openstack',
domain_name='admin_domain',
auth_url=ep)
keystone_client_lib = keystone_v3_client
else:
raise RuntimeError("keystone version must be '2' or '3'")
sess = keystone_session.Session(auth=auth)
self.keystone = keystone_client_lib.Client(session=sess)
# The service_catalog is missing from V3 keystone client when auth is
# done with session (via authenticate_keystone_admin()
# See https://bugs.launchpad.net/python-keystoneclient/+bug/1508374
# using session construct client will miss service_catalog property
# workaround bug # 1508374 by forcing a pre-auth and therefore, getting
# the service-catalog --
# see https://bugs.launchpad.net/python-keystoneclient/+bug/1547331
self.keystone.auth_ref = auth.get_access(sess)
def _run_action(self, unit_id, action, *args):
command = ["juju", "action", "do", "--format=json", unit_id, action]
command.extend(args)
print("Running command: %s\n" % " ".join(command))
output = subprocess.check_output(command)
output_json = output.decode(encoding="UTF-8")
data = json.loads(output_json)
action_id = data[u'Action queued with id']
return action_id
def _wait_on_action(self, action_id):
command = ["juju", "action", "fetch", "--format=json", action_id]
while True:
try:
output = subprocess.check_output(command)
except Exception as e:
print(e)
return False
output_json = output.decode(encoding="UTF-8")
data = json.loads(output_json)
if data[u"status"] == "completed":
return True
elif data[u"status"] == "failed":
return False
time.sleep(2)
def test_200_barbican_softhsm_relation(self):
"""Verify the barbican to softhsm plugin relation data"""
u.log.debug('Checking barbican to barbican-softhsm '
'relation data...')
unit = self.softhsm_sentry
relation = ['hsm', 'barbican:barbican-hsm']
expected = {
'_name': 'softhsm2',
'_plugin_data': u.not_null,
'private-address': u.not_null,
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('barbican barbican-softhsm', ret)
amulet.raise_status(amulet.FAIL, msg=message)
u.log.debug('OK')
@staticmethod
def _find_or_create(items, key, create):
"""Find or create the thing in the items
:param items: the items to search using the key
:param key: a function that key(item) -> boolean if found.
:param create: a function to call if the key() never was true.
:returns: the item that was either found or created.
"""
for i in items:
if key(i):
return i
return create()
def test_400_api_connection(self):
"""Simple api calls to check service is up and responding"""
u.log.debug("Not running test_400_api_connection() as bug#1611393 "
"prevents the test from passing. Waiting until the "
"OpenSSL library has the EVP_aes_128_wrap_pad() function "
"that barbican needs to wrap a keys for secret.store() to "
"function.")
return
u.log.debug('Checking api functionality...')
# This handles both keystone v2 and v3.
# For keystone v2 we need a user:
# - 'demo' user
# - has a project 'demo'
# - in the 'demo' project
# - with an 'admin' role
# For keystone v3 we need a user:
# - 'default' domain
# - 'demo' user
# - 'demo' project
# - 'admin' role -- to be able to delete.
# barbican requires a user with creator or admin role on the project
# when creating a secret (which this test does). Therefore, we create
# a demo user, demo project, and then get a demo barbican client and do
# the secret. ensure that the default domain is created.
if self._keystone_version == '2':
# find or create the 'demo' tenant (project)
tenant = self._find_or_create(
items=self.keystone.tenants.list(),
key=lambda t: t.name == 'demo',
create=lambda: self.keystone.tenants.create(
tenant_name="demo",
description="Demo for testing barbican",
enabled=True))
# find or create the demo user
demo_user = self._find_or_create(
items=self.keystone.users.list(),
key=lambda u: u.name == 'demo',
create=lambda: self.keystone.users.create(
name='demo',
password='pass',
tenant_id=tenant.id))
# find the admin role
# already be created - if not, then this will fail later.
admin_role = self._find_or_create(
items=self.keystone.roles.list(),
key=lambda r: r.name.lower() == 'admin',
create=lambda: None)
# grant the role if it isn't already created.
# now grant the creator role to the demo user.
self._find_or_create(
items=self.keystone.roles.roles_for_user(
demo_user, tenant=tenant),
key=lambda r: r.name.lower() == admin_role.name.lower(),
create=lambda: self.keystone.roles.add_user_role(
demo_user, admin_role, tenant=tenant))
# now we can finally get the barbican client and create the secret
keystone_ep = self.keystone.service_catalog.url_for(
service_type='identity', endpoint_type='publicURL')
auth = keystone_identity.v2.Password(
username=demo_user.name,
password='pass',
tenant_name=tenant.name,
auth_url=keystone_ep)
else:
# find or create the 'default' domain
domain = self._find_or_create(
items=self.keystone.domains.list(),
key=lambda u: u.name == 'default',
create=lambda: self.keystone.domains.create(
"default",
description="domain for barbican testing",
enabled=True))
# find or create the 'demo' user
demo_user = self._find_or_create(
items=self.keystone.users.list(domain=domain.id),
key=lambda u: u.name == 'demo',
create=lambda: self.keystone.users.create(
'demo',
domain=domain.id,
description="Demo user for barbican tests",
enabled=True,
email="demo@example.com",
password="pass"))
# find or create the 'demo' project
demo_project = self._find_or_create(
items=self.keystone.projects.list(domain=domain.id),
key=lambda x: x.name == 'demo',
create=lambda: self.keystone.projects.create(
'demo',
domain=domain.id,
description='barbican testing project',
enabled=True))
# create the role for the user - needs to be admin so that the
# secret can be deleted - note there is only one admin role, and it
# should already be created - if not, then this will fail later.
admin_role = self._find_or_create(
items=self.keystone.roles.list(),
key=lambda r: r.name.lower() == 'admin',
create=lambda: None)
# now grant the creator role to the demo user.
try:
self.keystone.roles.check(
role=admin_role,
user=demo_user,
project=demo_project)
except keystoneclient.exceptions.NotFound:
# create it if it isn't found
self.keystone.roles.grant(
role=admin_role,
user=demo_user,
project=demo_project)
# now we can finally get the barbican client and create the secret
keystone_ep = self.keystone.service_catalog.url_for(
service_type='identity', endpoint_type='publicURL')
auth = keystone_identity.v3.Password(
user_domain_name=domain.name,
username=demo_user.name,
password='pass',
project_domain_name=domain.name,
project_name=demo_project.name,
auth_url=keystone_ep)
# Now we carry on with common v2 and v3 code
sess = keystone_session.Session(auth=auth)
# Authenticate admin with barbican endpoint
barbican_ep = self.keystone.service_catalog.url_for(
service_type='key-manager', endpoint_type='publicURL')
barbican = barbican_client.Client(session=sess,
endpoint=barbican_ep)
# before creating a secret with the HSM we have to ensure that the mkek
# is generated. This is done with an action WHICH will fail if the
# mkek has already been generated. We look for the duplicate message
# and if it exists then we assume that it is okay.
action_id = u.run_action(self.d.sentry['barbican'][0], 'generate-mkek')
assert u.wait_on_action(action_id), "generate-mkek action failed."
# now create the secret.
my_secret = barbican.secrets.create()
my_secret.name = u'Random plain text password'
my_secret.payload = u'password'
my_secret_ref = my_secret.store()
assert(my_secret_ref is not None)
# and now delete the secret
my_secret.delete()
u.log.debug('OK')

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
"""Amulet tests on a basic aodh deployment on xenial-mitaka for keystone v2.
"""
from basic_deployment import SoftHSMBasicDeployment
if __name__ == '__main__':
deployment = SoftHSMBasicDeployment(series='xenial', keystone_version=2)
deployment.run_tests()

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
"""Amulet tests on a basic aodh deployment on xenial-mitaka for keystone v3.
"""
from basic_deployment import SoftHSMBasicDeployment
if __name__ == '__main__':
deployment = SoftHSMBasicDeployment(series='xenial', keystone_version=3)
deployment.run_tests()

17
src/tests/tests.yaml Normal file
View File

@ -0,0 +1,17 @@
# Bootstrap the model if necessary.
bootstrap: True
# Re-use bootstrap node instead of destroying/re-bootstrapping.
reset: True
# Use tox/requirements to drive the venv instead of bundletester's venv feature.
virtualenv: False
# Leave makefile empty, otherwise unit/lint tests will rerun ahead of amulet.
makefile: []
# Do not specify juju PPA sources. Juju is presumed to be pre-installed
# and configured in all test runner environments.
#sources:
# Do not specify or rely on system packages.
#packages:
# Do not specify python packages here. Use test-requirements.txt
# and tox instead. ie. The venv is constructed before bundletester
# is invoked.
#python-packages:

View File

@ -34,7 +34,7 @@ commands =
# Run a specific test as an Amulet smoke test (expected to always pass)
basepython = python2.7
commands =
bundletester -vl DEBUG -r json -o func-results.json gate-basic-xenial-mitaka --no-destroy
bundletester -vl DEBUG -r json -o func-results.json gate-basic-xenial-mitaka-keystone-v2 --no-destroy
[testenv:func27-dfs]
# Charm Functional Test