diff --git a/README.md b/README.md index f90840c..80d63c4 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,25 @@ designed for the secure storage, provisioning and management of secrets such as passwords, encryption keys and X.509 Certificates. It is aimed at being useful for all environments, including large ephemeral Clouds.' +Barbican can be used without an HSM for test purposes. + # Plugins The Barbican charm currently supports the following plugins: - charm-barbican-softhsm +However, due to an odd quirk of interelating software issues, barbican + +SoftHSM2 + OpenSSL < 1.0.2h is not functionaly due to a missing feature in +OpenSSL (EVP_aes_128_wrap_pad specifically). + +Thus the plugin interface is _currently_ provided to show how to interface an +HSM to the barbican charm. + # Creating the primary MKEK and primary HMAC Barbican (can use|uses) a Master Key Encryption Key (MKEK) scheme to wrap other -keys so that in the course of issuing new encryption keys, it doesn't exhaust +keys so that in the course of issuing new encryption keys, it does not exhaust the storage capacity of an HSM. See [KMIP MKEK Model diff --git a/manual_testing/barbican-keystone-setup.py b/manual_testing/barbican-keystone-setup.py deleted file mode 100755 index d2d7372..0000000 --- a/manual_testing/barbican-keystone-setup.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/python - -from keystoneclient.v3 import ( - client, - domains, - projects, - roles, - users, -) -import os - -class BarbicanDomain(): - def __init__(self, manager, reset=False): - self.manager = manager - self.domain = None - self.update_domain() - if reset: - self.delete_domain() - if not self.domain: - self.create_domain() - self.update_domain() - - def update_domain(self): - for dom in self.manager.list(): - if dom.name == "barbican-domain": - self.domain = dom - - def create_domain(self): - if not self.domain: - self.manager.create("barbican-domain", description="domain for Barbican test", enabled=True) - - def delete_domain(self): - if self.domain: - print "Deleting testdomain" - self.manager.update(self.domain, enabled=False) - self.manager.delete(self.domain) - self.domain = None - -class BarbicanProject(): - def __init__(self, manager, domain_id, reset=False): - self.manager = manager - self.domain_id = domain_id - self.project = None - self.update_project() - if reset: - self.delete_project() - if not self.project: - self.create_project() - self.update_project() - - def update_project(self): - for proj in self.manager.list(): - if proj.name == "barbican-project": - self.project = proj - - def create_project(self): - if not self.project: - self.manager.create("barbican-project", domain=self.domain_id, description="Barbican Project", enabled=True) - - def delete_project(self): - if self.project: - print "Deleting testproject" - self.manager.delete(self.project) - self.project = None - - -class BarbicanUser(): - def __init__(self, manager, domain_id, reset=False): - self.manager = manager - self.domain_id = domain_id - self.user = None - self.update_user() - if reset: - self.delete_user() - if not self.user: - self.create_user() - self.update_user() - - def update_user(self): - for user in self.manager.list(): - if user.name == "barbican-user": - self.user = user - - def create_user(self): - if not self.user: - self.manager.create("barbican-user", domain=self.domain_id, description="Barbican Project", enabled=True, email="test-user@testcorp.com", password="changeit") - - def delete_user(self): - if self.user: - print "Deleting testuser" - self.manager.delete(self.user) - self.user = None - - -def get_admin_role(manager): - for role in manager.list(): - if role.name == "Admin": - return role - - -if __name__ == '__main__': - reset=False - keystone = client.Client(user_domain_name='Default', - username=os.environ['OS_USERNAME'], - password=os.environ['OS_PASSWORD'], - project_domain_name='Default', - project_name='admin', - auth_url=os.environ['OS_AUTH_URL']) - domain_manager = domains.DomainManager(keystone) - project_manager = projects.ProjectManager(keystone) - user_manager = users.UserManager(keystone) - role_manager = roles.RoleManager(keystone) - barbican_domain=BarbicanDomain(domain_manager, reset=reset) - barbican_project=BarbicanProject(project_manager, barbican_domain.domain.id, reset=reset) - barbican_user=BarbicanUser(user_manager, barbican_domain.domain.id, reset=reset) - admin_role=get_admin_role(role_manager) - role_manager.grant(admin_role.id, - user=barbican_user.user.id, - project=barbican_project.project.id) - print "Domain ID: " + barbican_domain.domain.id - print "Project ID: " + barbican_project.project.id diff --git a/manual_testing/barbican.yaml b/manual_testing/barbican.yaml deleted file mode 100644 index aadeb64..0000000 --- a/manual_testing/barbican.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# vim: set ts=2 et: -# deployer bundle for development ('next') charms -# UOSCI relies on this for OS-on-OS deployment testing -openstack-services: - series: trusty - services: - mysql: - branch: lp:~openstack-charmers/charms/trusty/percona-cluster/next - constraints: mem=1G - options: - dataset-size: 50% - rabbitmq-server: - branch: lp:~openstack-charmers/charms/trusty/rabbitmq-server/next - constraints: mem=1G - keystone: - branch: lp:~gnuoy/charms/trusty/keystone/secret-store - constraints: mem=1G - options: - admin-password: openstack - admin-token: ubuntutesting - openstack-origin: cloud:trusty-kilo - barbican: - charm: barbican - options: - openstack-origin: cloud:trusty-kilo - relations: - - [ keystone, mysql ] - - [ barbican, mysql ] - - [ barbican, rabbitmq-server ] - - [ barbican, keystone ] diff --git a/manual_testing/keystone_setup.sh b/manual_testing/keystone_setup.sh deleted file mode 100755 index 5a75cc0..0000000 --- a/manual_testing/keystone_setup.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -ex - -# Create demo/testing users, tenants and flavor -openstack project create demo -openstack user create --project demo --password pass --email demo@dev.null demo -openstack role add --user demo --project demo Member -openstack project create alt_demo -openstack user create --project alt_demo --password secret --email demo@dev.null alt_demo -openstack role add --user alt_demo --project alt_demo Member diff --git a/manual_testing/kill_charms.sh b/manual_testing/kill_charms.sh deleted file mode 100755 index 7d07f29..0000000 --- a/manual_testing/kill_charms.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -for charm in $1 -do - juju destroy-service ${charm} -done - -for charm in $1 -do - while true; do - JS_OUT=$(juju status ${charm} --format=short | \ - sed -e 's!^.*new available version.*!!' | grep -vE '^$') - if [[ -z $JS_OUT ]]; then - exit 0 - fi - echo "!$JS_OUT!" - sleep 3 - done -done diff --git a/manual_testing/novarc b/manual_testing/novarc deleted file mode 100644 index 4c3c6c1..0000000 --- a/manual_testing/novarc +++ /dev/null @@ -1,8 +0,0 @@ -export OS_REGION_NAME=RegionOne -export OS_USER_DOMAIN_ID=Default -export OS_PROJECT_NAME=admin -export OS_PASSWORD=openstack -export OS_AUTH_URL=${OS_AUTH_PROTOCOL:-http}://`juju-deployer -f keystone`:5000/v3 -export OS_USERNAME=admin -export OS_TENANT_NAME=admin -export OS_PROJECT_DOMAIN_NAME=Default diff --git a/manual_testing/secret-store.py b/manual_testing/secret-store.py deleted file mode 100755 index 3cd92e5..0000000 --- a/manual_testing/secret-store.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/python -from keystoneclient.auth import identity -from keystoneclient import session -from barbicanclient import client -import subprocess - -keystone_ip = subprocess.check_output(['juju-deployer', '-f', 'keystone']).rstrip() -barbican_ip = subprocess.check_output(['juju-deployer', '-f', 'barbican']).rstrip() -auth = identity.v3.Password(user_domain_name='Default', - username='admin', - password='openstack', - project_domain_name='Default', - project_name='admin', - auth_url='http://{}:5000/v3'.format(keystone_ip)) - -sess = session.Session(auth=auth) -barbican = client.Client(session=sess, endpoint='http://{}:9311'.format(barbican_ip)) -secret = barbican.secrets.create(name='Self destruction sequence', - payload='the magic words are squeamish ossifrage', - payload_content_type='text/plain') -secret.store() -print(secret.secret_ref) -ref = secret.secret_ref.replace('localhost', barbican_ip) -retrieved_secret = barbican.secrets.get(secret.secret_ref) -print(retrieved_secret.payload) diff --git a/src/HACKING.md b/src/HACKING.md new file mode 100644 index 0000000..38bed01 --- /dev/null +++ b/src/HACKING.md @@ -0,0 +1,10 @@ +# Overview + +This charm is developed as part of the OpenStack Charms project, and as such you +should refer to the [OpenStack Charm Development Guide](https://github.com/openstack/charm-guide) for details on how +to contribute to this charm. + +You can find its source code here: . + + + diff --git a/src/README.md b/src/README.md index f77cfea..4c44023 100644 --- a/src/README.md +++ b/src/README.md @@ -1 +1,23 @@ -# Write me +# Overview + +This charm provides the Barbican secret service for an OpenStack Cloud. + +# Usage + +Barbican relies on services from the mysql/percona, rabbitmq-server and keystone charms: + + juju deploy barbican + juju deploy keystone + juju deploy mysql + juju deploy rabbitmq-server + juju add-relation barbican rabbitmq-server + juju add-relation barbican mysql + juju add-relation barbican keystone + +Optionally, but advisedly, Barbican should be deployed with an HSM subordinate charm. + +# Bugs + +Please report bugs on [Launchpad](https://bugs.launchpad.net/charm-barbican/+filebug). + +For general questions please refer to the OpenStack [Charm Guide](https://github.com/openstack/charm-guide). diff --git a/src/config.yaml b/src/config.yaml index 1bec251..8f7be81 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -43,11 +43,11 @@ options: type: string description: OpenStack Region keystone-api-version: - default: "3" + default: "2" type: string description: none, 2 or 3 require-hsm-plugin: - default: True + default: False type: boolean description: | If True (the default) then the barbcian-worker process won't be fully diff --git a/src/metadata.yaml b/src/metadata.yaml index 6365ac7..2ad00ac 100644 --- a/src/metadata.yaml +++ b/src/metadata.yaml @@ -12,9 +12,6 @@ series: # - trusty - xenial subordinate: false -provides: - secret-store: - interface: barbican requires: shared-db: interface: mysql-shared diff --git a/src/templates/mitaka/barbican-api-paste.ini b/src/templates/mitaka/barbican-api-paste.ini index aa70f4e..94c0fff 100644 --- a/src/templates/mitaka/barbican-api-paste.ini +++ b/src/templates/mitaka/barbican-api-paste.ini @@ -63,14 +63,15 @@ auth_version = v2.0 [filter:keystone_v3_authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory signing_dir = /var/lib/barbican/keystone-signing -auth_host = {{ identity_service.auth_host }} -#need ability to re-auth a token, thus admin url -auth_port = {{ identity_service.auth_port }} -auth_protocol = {{ identity_service.auth_protocol }} -admin_tenant_name = {{ identity_service.service_tenant }} -admin_user = {{ identity_service.service_username }} -admin_password = {{ identity_service.service_password }} -auth_version = v3.0 +username = {{ identity_service.service_username }} +password= {{ identity_service.service_password }} +# These are hardcoded as it is not passed in the relation but is needed for v3 auth +user_domain_name = default +project_domain_name = default +auth_plugin = password +auth_url = {{ identity_service.auth_protocol }}://{{ identity_service.auth_host }}:{{ identity_service.auth_port }}/v3 +auth_uri = {{ identity_service.service_protocol}}://{{ identity_service.service_host }}:{{ identity_service.service_port }}/ +project_name = {{ identity_service.service_tenant }} #delay failing perhaps to log the unauthorized request in barbican .. #delay_auth_decision = true diff --git a/src/templates/mitaka/barbican-api.conf b/src/templates/mitaka/barbican-api.conf index 20b8354..7aa1b84 100644 --- a/src/templates/mitaka/barbican-api.conf +++ b/src/templates/mitaka/barbican-api.conf @@ -1,10 +1,16 @@ Listen {{ options.service_listen_info.barbican_worker.public_port }} Listen {{ options.service_listen_info.barbican_worker.admin_port }} + +# workaround problem with Python Cryptography and libssl1.0.0 by adding +# WSGIApplicationGroup %{GLOBAL} +# See https://cryptography.io/en/latest/faq/#starting-cryptography-using-mod-wsgi-produces-an-internalerror-during-a-call-in-register-osrandom-engine + WSGIScriptAlias / /usr/share/barbican/app.wsgi WSGIDaemonProcess barbican-api user=barbican group=barbican processes=3 threads=10 WSGIProcessGroup barbican-api + WSGIApplicationGroup %{GLOBAL} ErrorLog /var/log/barbican/barbican-api.log CustomLog /var/log/barbican/barbican-api.log combined @@ -13,6 +19,7 @@ Listen {{ options.service_listen_info.barbican_worker.admin_port }} WSGIScriptAlias / /usr/share/barbican/app.wsgi WSGIDaemonProcess barbican-api-admin user=barbican group=barbican processes=3 threads=10 WSGIProcessGroup barbican-api-admin + WSGIApplicationGroup %{GLOBAL} ErrorLog /var/log/barbican/barbican-api.log CustomLog /var/log/barbican/barbican-api.log combined diff --git a/src/templates/mitaka/barbican.conf b/src/templates/mitaka/barbican.conf index d84268a..a710cba 100644 --- a/src/templates/mitaka/barbican.conf +++ b/src/templates/mitaka/barbican.conf @@ -274,15 +274,15 @@ plugin_working_dir = '/etc/barbican/dogtag' {% if hsm -%} [p11_crypto_plugin] # Path to vendor PKCS11 library -library_path = {{ hsm.library_path }} +library_path = '{{ hsm.library_path }}' # Password to login to PKCS11 session -login = {{ hsm.login }} +login = '{{ hsm.login }}' # Label to identify master KEK in the HSM (must not be the same as HMAC label) -mkek_label = {{ options.label_mkek }} +mkek_label = '{{ options.label_mkek }}' # Length in bytes of master KEK mkek_length = {{ options.mkek_key_length }} # Label to identify HMAC key in the HSM (must not be the same as MKEK label) -hmac_label = {{ options.label_hmac }} +hmac_label = '{{ options.label_hmac }}' # HSM Slot id (Should correspond to a configured PKCS11 slot). Default: 1 slot_id = {{ hsm.slot_id }} # Enable Read/Write session with the HSM? @@ -294,7 +294,7 @@ slot_id = {{ hsm.slot_id }} # Max number of items in pkek cache # pkek_cache_limit = 100 # Seedfile to generate random data from. -seed_file = /dev/random +seed_file = '/dev/urandom' # Seed length to read the random data for seeding the RNG seed_length = 32 {%- endif %} diff --git a/src/test-requirements.txt b/src/test-requirements.txt index 3ffd9c2..5326ccc 100644 --- a/src/test-requirements.txt +++ b/src/test-requirements.txt @@ -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 diff --git a/src/tests/basic_deployment.py b/src/tests/basic_deployment.py new file mode 100644 index 0000000..bd750cc --- /dev/null +++ b/src/tests/basic_deployment.py @@ -0,0 +1,570 @@ +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 BarbicanBasicDeployment(OpenStackAmuletDeployment): + """Amulet tests on a basic Barbican 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 2 or 3. + """ + super(BarbicanBasicDeployment, 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 barbican 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'} + other_services = [{'name': 'mysql'}, + {'name': 'rabbitmq-server'}, + {'name': 'keystone'}] + super(BarbicanBasicDeployment, self)._add_services( + this_service, other_services) + + def _add_relations(self): + """Add all of the relations for the services.""" + relations = { + 'barbican:shared-db': 'mysql:shared-db', + 'barbican:amqp': 'rabbitmq-server:amqp', + 'barbican:identity-service': 'keystone:identity-service', + 'keystone:shared-db': 'mysql:shared-db', + } + super(BarbicanBasicDeployment, 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': False, + 'verbose': True, + 'keystone-api-version': str(keystone_version), + } + configs = { + 'keystone': keystone_config, + 'barbican': barbican_config, + } + super(BarbicanBasicDeployment, self)._configure_services(configs) + + def _initialize_tests(self): + """Perform final initialization before tests get run.""" + # Access the sentries for inspecting service units + 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_100_services(self): + """Verify the expected services are running on the corresponding + service units.""" + u.log.debug('Checking system services on units...') + + barbican_svcs = [ + 'apache2', 'barbican-worker', + ] + + service_names = { + self.barbican_sentry: barbican_svcs, + } + + ret = u.validate_services_by_name(service_names) + if ret: + amulet.raise_status(amulet.FAIL, msg=ret) + + u.log.debug('OK') + + def test_110_service_catalog(self): + """Verify that the service catalog endpoint data is valid.""" + u.log.debug('Checking keystone service catalog data...') + + actual = self.keystone.service_catalog.get_endpoints() + + if self._keystone_version == '2': + endpoint_check = [{ + 'adminURL': u.valid_url, + 'id': u.not_null, + 'region': 'RegionOne', + 'publicURL': u.valid_url, + 'internalURL': u.valid_url, + }] + validate_catalog = u.validate_svc_catalog_endpoint_data + else: + # v3 endpoint check + endpoint_check = [ + { + 'id': u.not_null, + 'interface': interface, + 'region': 'RegionOne', + 'region_id': 'RegionOne', + 'url': u.valid_url, + } + for interface in ('admin', 'public', 'internal')] + validate_catalog = u.validate_v3_svc_catalog_endpoint_data + + expected = { + 'key-manager': endpoint_check, + 'identity': endpoint_check, + } + + ret = validate_catalog(expected, actual) + if ret: + amulet.raise_status(amulet.FAIL, msg=ret) + + u.log.debug('OK') + + def test_114_barbican_api_endpoint(self): + """Verify the barbican api endpoint data.""" + u.log.debug('Checking barbican api endpoint data...') + endpoints = self.keystone.endpoints.list() + u.log.debug(endpoints) + admin_port = '9312' + internal_port = public_port = '9311' + if self._keystone_version == '2': + expected = {'id': u.not_null, + 'region': 'RegionOne', + 'adminurl': u.valid_url, + 'internalurl': u.valid_url, + 'publicurl': u.valid_url, + 'service_id': u.not_null} + + ret = u.validate_endpoint_data( + endpoints, admin_port, internal_port, public_port, expected) + elif self._keystone_version == '3': + # For keystone v3 it's slightly different. + expected = {'id': u.not_null, + 'region': 'RegionOne', + 'region_id': 'RegionOne', + 'url': u.valid_url, + 'interface': u.not_null, # we match this in the test + 'service_id': u.not_null} + + ret = u.validate_v3_endpoint_data( + endpoints, admin_port, internal_port, public_port, expected) + else: + raise RuntimeError("Unexpected self._keystone_version: {}" + .format(self._keystone_version)) + + if ret: + message = 'barbican endpoint: {}'.format(ret) + amulet.raise_status(amulet.FAIL, msg=message) + + u.log.debug('OK') + + def test_200_barbican_identity_relation(self): + """Verify the barbican to keystone identity-service relation data""" + u.log.debug('Checking barbican to keystone identity-service ' + 'relation data...') + unit = self.barbican_sentry + relation = ['identity-service', 'keystone:identity-service'] + barbican_ip = unit.relation(*relation)['private-address'] + barbican_admin_endpoint = "http://%s:9312" % (barbican_ip) + barbican_endpoint = "http://%s:9311" % (barbican_ip) + + expected = { + 'admin_url': barbican_admin_endpoint, + 'internal_url': barbican_endpoint, + 'private-address': barbican_ip, + 'public_url': barbican_endpoint, + 'region': 'RegionOne', + 'service': 'barbican', + } + + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('barbican identity-service', ret) + amulet.raise_status(amulet.FAIL, msg=message) + + u.log.debug('OK') + + def test_201_keystone_barbican_identity_relation(self): + """Verify the keystone to barbican identity-service relation data""" + u.log.debug('Checking keystone:barbican identity relation data...') + unit = self.keystone_sentry + relation = ['identity-service', 'barbican:identity-service'] + id_relation = unit.relation(*relation) + id_ip = id_relation['private-address'] + expected = { + 'admin_token': 'ubuntutesting', + 'auth_host': id_ip, + 'auth_port': "35357", + 'auth_protocol': 'http', + 'private-address': id_ip, + 'service_host': id_ip, + 'service_password': u.not_null, + 'service_port': "5000", + 'service_protocol': 'http', + 'service_tenant': 'services', + 'service_tenant_id': u.not_null, + 'service_username': 'barbican', + } + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('keystone identity-service', ret) + amulet.raise_status(amulet.FAIL, msg=message) + + u.log.debug('OK') + + def test_203_barbican_amqp_relation(self): + """Verify the barbican to rabbitmq-server amqp relation data""" + u.log.debug('Checking barbican:rabbitmq amqp relation data...') + unit = self.barbican_sentry + relation = ['amqp', 'rabbitmq-server:amqp'] + expected = { + 'username': 'barbican', + 'private-address': u.valid_ip, + 'vhost': 'openstack' + } + + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('barbican amqp', ret) + amulet.raise_status(amulet.FAIL, msg=message) + + u.log.debug('OK') + + def test_204_barbican_amqp_relation(self): + """Verify the rabbitmq-server to barbican amqp relation data""" + u.log.debug('Checking rabbitmq:barbican barbican relation data...') + unit = self.rabbitmq_sentry + relation = ['amqp', 'barbican:amqp'] + expected = { + 'hostname': u.valid_ip, + 'private-address': u.valid_ip, + 'password': u.not_null, + } + + ret = u.validate_relation_data(unit, relation, expected) + if ret: + message = u.relation_error('rabbitmq barbican', 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('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) + # 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') + + def test_900_restart_on_config_change(self): + """Verify that the specified services are restarted when the config + is changed. + """ + sentry = self.barbican_sentry + juju_service = 'barbican' + + # Expected default and alternate values + set_default = {'debug': 'False'} + set_alternate = {'debug': 'True'} + + # Services which are expected to restart upon config change, + # and corresponding config files affected by the change + conf_file = '/etc/barbican/barbican.conf' + services = { + 'barbican-worker': conf_file, + } + + # Make config change, check for service restarts + u.log.debug('Making config change on {}...'.format(juju_service)) + mtime = u.get_sentry_time(sentry) + self.d.configure(juju_service, set_alternate) + + sleep_time = 40 + for s, conf_file in services.iteritems(): + u.log.debug("Checking that service restarted: {}".format(s)) + if not u.validate_service_config_changed(sentry, mtime, s, + conf_file, + retry_count=4, + retry_sleep_time=20, + sleep_time=sleep_time): + self.d.configure(juju_service, set_default) + msg = "service {} didn't restart after config change".format(s) + amulet.raise_status(amulet.FAIL, msg=msg) + sleep_time = 0 + + self.d.configure(juju_service, set_default) + u.log.debug('OK') + + def _test_910_pause_and_resume(self): + """The services can be paused and resumed. """ + # test disabled as feature is not implemented yet - kept for future + # usage. + return + u.log.debug('Checking pause and resume actions...') + unit_name = "barbican/0" + juju_service = 'barbican' + unit = self.d.sentry[juju_service][0] + + assert u.status_get(unit)[0] == "active" + + action_id = self._run_action(unit_name, "pause") + assert self._wait_on_action(action_id), "Pause action failed." + assert u.status_get(unit)[0] == "maintenance" + + # trigger config-changed to ensure that services are still stopped + u.log.debug("Making config change on barbican ...") + self.d.configure(juju_service, {'debug': 'True'}) + assert u.status_get(unit)[0] == "maintenance" + self.d.configure(juju_service, {'debug': 'False'}) + assert u.status_get(unit)[0] == "maintenance" + + action_id = self._run_action(unit_name, "resume") + assert self._wait_on_action(action_id), "Resume action failed." + assert u.status_get(unit)[0] == "active" + u.log.debug('OK') diff --git a/src/tests/gate-basic-xenial-mitaka-keystone-v2 b/src/tests/gate-basic-xenial-mitaka-keystone-v2 new file mode 100755 index 0000000..b5bedec --- /dev/null +++ b/src/tests/gate-basic-xenial-mitaka-keystone-v2 @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +"""Amulet tests on a basic barbican deployment on xenial-mitaka for keystone v2. +""" + +from basic_deployment import BarbicanBasicDeployment + +if __name__ == '__main__': + deployment = BarbicanBasicDeployment(series='xenial', keystone_version=2) + deployment.run_tests() diff --git a/src/tests/gate-basic-xenial-mitaka-keystone-v3 b/src/tests/gate-basic-xenial-mitaka-keystone-v3 new file mode 100755 index 0000000..69bec56 --- /dev/null +++ b/src/tests/gate-basic-xenial-mitaka-keystone-v3 @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +"""Amulet tests on a basic barbican deployment on xenial-mitaka for keystone v3. +""" + +from basic_deployment import BarbicanBasicDeployment + +if __name__ == '__main__': + deployment = BarbicanBasicDeployment(series='xenial', keystone_version=3) + deployment.run_tests() diff --git a/src/tests/tests.yaml b/src/tests/tests.yaml new file mode 100644 index 0000000..e3185c6 --- /dev/null +++ b/src/tests/tests.yaml @@ -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: diff --git a/src/tox.ini b/src/tox.ini index 4a6291e..b608ad3 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -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