Browse Source

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
changes/20/353020/2
Alex Kavanagh 2 years ago
parent
commit
2651c4a038

+ 8
- 0
README.md View File

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

+ 20
- 1
src/README.md View File

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

+ 2
- 0
src/metadata.yaml View File

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

+ 1
- 0
src/test-requirements.txt View File

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

+ 354
- 0
src/tests/basic_deployment.py View File

@@ -0,0 +1,354 @@
1
+import amulet
2
+import json
3
+import subprocess
4
+import time
5
+
6
+import barbicanclient.client as barbican_client
7
+from keystoneclient import session as keystone_session
8
+from keystoneclient.auth import identity as keystone_identity
9
+import keystoneclient.exceptions
10
+from keystoneclient.v2_0 import client as keystone_v2_0_client
11
+from keystoneclient.v3 import client as keystone_v3_client
12
+
13
+from charmhelpers.contrib.openstack.amulet.deployment import (
14
+    OpenStackAmuletDeployment
15
+)
16
+
17
+from charmhelpers.contrib.openstack.amulet.utils import (
18
+    OpenStackAmuletUtils,
19
+    DEBUG,
20
+)
21
+
22
+# Use DEBUG to turn on debug logging
23
+u = OpenStackAmuletUtils(DEBUG)
24
+
25
+
26
+class SoftHSMBasicDeployment(OpenStackAmuletDeployment):
27
+    """Amulet tests on a basic SoftHSM deployment."""
28
+
29
+    def __init__(self, series, openstack=None, source=None, stable=False,
30
+                 keystone_version='2'):
31
+        """Deploy the entire test environment.
32
+
33
+        The keystone_version controls whether keystone (and barbican) are set
34
+        up to use keystone v2.0 or v3.  The options are <string> 2 or 3.
35
+        """
36
+        super(SoftHSMBasicDeployment, self).__init__(
37
+            series, openstack, source, stable)
38
+        self._keystone_version = str(keystone_version)
39
+        self._add_services()
40
+        self._add_relations()
41
+        self._configure_services(keystone_version)
42
+        self._deploy()
43
+
44
+        u.log.info('Waiting on extended status checks...')
45
+        exclude_services = ['mysql', ]
46
+        self._auto_wait_for_status(exclude_services=exclude_services)
47
+
48
+        self._initialize_tests()
49
+
50
+    def _add_services(self):
51
+        """Add services
52
+
53
+           Add the services that we're testing, where softhsm is local,
54
+           and the rest of the service are from lp branches that are
55
+           compatible with the local charm (e.g. stable or next).
56
+           """
57
+        this_service = {'name': 'barbican-softhsm'}
58
+        other_services = [{'name': 'barbican'},
59
+                          {'name': 'mysql'},
60
+                          {'name': 'rabbitmq-server'},
61
+                          {'name': 'keystone'}]
62
+        super(SoftHSMBasicDeployment, self)._add_services(
63
+            this_service, other_services)
64
+
65
+    def _add_relations(self):
66
+        """Add all of the relations for the services."""
67
+        relations = {
68
+            'barbican:hsm': 'barbican-softhsm:hsm',
69
+            'barbican:shared-db': 'mysql:shared-db',
70
+            'barbican:amqp': 'rabbitmq-server:amqp',
71
+            'barbican:identity-service': 'keystone:identity-service',
72
+            'keystone:shared-db': 'mysql:shared-db',
73
+        }
74
+        super(SoftHSMBasicDeployment, self)._add_relations(relations)
75
+
76
+    def _configure_services(self, keystone_version='2'):
77
+        """Configure all of the services."""
78
+        keystone_config = {
79
+            'admin-password': 'openstack',
80
+            'admin-token': 'ubuntutesting',
81
+            'preferred-api-version': str(keystone_version),
82
+        }
83
+        # say we don't need an HSM for these tests
84
+        barbican_config = {
85
+            'require-hsm-plugin': True,
86
+            'verbose': True,
87
+            'keystone-api-version': str(keystone_version),
88
+        }
89
+        configs = {
90
+            'keystone': keystone_config,
91
+            'barbican': barbican_config,
92
+        }
93
+        super(SoftHSMBasicDeployment, self)._configure_services(configs)
94
+
95
+    def _initialize_tests(self):
96
+        """Perform final initialization before tests get run."""
97
+        # Access the sentries for inspecting service units
98
+        self.softhsm_sentry = self.d.sentry['barbican-softhsm'][0]
99
+        self.barbican_sentry = self.d.sentry['barbican'][0]
100
+        self.mysql_sentry = self.d.sentry['mysql'][0]
101
+        self.keystone_sentry = self.d.sentry['keystone'][0]
102
+        self.rabbitmq_sentry = self.d.sentry['rabbitmq-server'][0]
103
+        u.log.debug('openstack release val: {}'.format(
104
+            self._get_openstack_release()))
105
+        u.log.debug('openstack release str: {}'.format(
106
+            self._get_openstack_release_string()))
107
+
108
+        keystone_ip = self.keystone_sentry.relation(
109
+            'shared-db', 'mysql:shared-db')['private-address']
110
+
111
+        # We need to auth either to v2.0 or v3 keystone
112
+        if self._keystone_version == '2':
113
+            ep = ("http://{}:35357/v2.0"
114
+                  .format(keystone_ip.strip().decode('utf-8')))
115
+            auth = keystone_identity.v2.Password(
116
+                username='admin',
117
+                password='openstack',
118
+                tenant_name='admin',
119
+                auth_url=ep)
120
+            keystone_client_lib = keystone_v2_0_client
121
+        elif self._keystone_version == '3':
122
+            ep = ("http://{}:35357/v3"
123
+                  .format(keystone_ip.strip().decode('utf-8')))
124
+            auth = keystone_identity.v3.Password(
125
+                user_domain_name='admin_domain',
126
+                username='admin',
127
+                password='openstack',
128
+                domain_name='admin_domain',
129
+                auth_url=ep)
130
+            keystone_client_lib = keystone_v3_client
131
+        else:
132
+            raise RuntimeError("keystone version must be '2' or '3'")
133
+
134
+        sess = keystone_session.Session(auth=auth)
135
+        self.keystone = keystone_client_lib.Client(session=sess)
136
+        # The service_catalog is missing from V3 keystone client when auth is
137
+        # done with session (via authenticate_keystone_admin()
138
+        # See https://bugs.launchpad.net/python-keystoneclient/+bug/1508374
139
+        # using session construct client will miss service_catalog property
140
+        # workaround bug # 1508374 by forcing a pre-auth and therefore, getting
141
+        # the service-catalog --
142
+        # see https://bugs.launchpad.net/python-keystoneclient/+bug/1547331
143
+        self.keystone.auth_ref = auth.get_access(sess)
144
+
145
+    def _run_action(self, unit_id, action, *args):
146
+        command = ["juju", "action", "do", "--format=json", unit_id, action]
147
+        command.extend(args)
148
+        print("Running command: %s\n" % " ".join(command))
149
+        output = subprocess.check_output(command)
150
+        output_json = output.decode(encoding="UTF-8")
151
+        data = json.loads(output_json)
152
+        action_id = data[u'Action queued with id']
153
+        return action_id
154
+
155
+    def _wait_on_action(self, action_id):
156
+        command = ["juju", "action", "fetch", "--format=json", action_id]
157
+        while True:
158
+            try:
159
+                output = subprocess.check_output(command)
160
+            except Exception as e:
161
+                print(e)
162
+                return False
163
+            output_json = output.decode(encoding="UTF-8")
164
+            data = json.loads(output_json)
165
+            if data[u"status"] == "completed":
166
+                return True
167
+            elif data[u"status"] == "failed":
168
+                return False
169
+            time.sleep(2)
170
+
171
+    def test_200_barbican_softhsm_relation(self):
172
+        """Verify the barbican to softhsm plugin relation data"""
173
+        u.log.debug('Checking barbican to barbican-softhsm '
174
+                    'relation data...')
175
+        unit = self.softhsm_sentry
176
+        relation = ['hsm', 'barbican:barbican-hsm']
177
+
178
+        expected = {
179
+            '_name': 'softhsm2',
180
+            '_plugin_data': u.not_null,
181
+            'private-address': u.not_null,
182
+        }
183
+
184
+        ret = u.validate_relation_data(unit, relation, expected)
185
+        if ret:
186
+            message = u.relation_error('barbican barbican-softhsm', ret)
187
+            amulet.raise_status(amulet.FAIL, msg=message)
188
+
189
+        u.log.debug('OK')
190
+
191
+    @staticmethod
192
+    def _find_or_create(items, key, create):
193
+        """Find or create the thing in the items
194
+
195
+        :param items: the items to search using the key
196
+        :param key: a function that key(item) -> boolean if found.
197
+        :param create: a function to call if the key() never was true.
198
+        :returns: the item that was either found or created.
199
+        """
200
+        for i in items:
201
+            if key(i):
202
+                return i
203
+        return create()
204
+
205
+    def test_400_api_connection(self):
206
+        """Simple api calls to check service is up and responding"""
207
+        u.log.debug("Not running test_400_api_connection() as bug#1611393 "
208
+                    "prevents the test from passing.  Waiting until the "
209
+                    "OpenSSL library has the EVP_aes_128_wrap_pad() function "
210
+                    "that barbican needs to wrap a keys for secret.store() to "
211
+                    "function.")
212
+        return
213
+        u.log.debug('Checking api functionality...')
214
+
215
+        # This handles both keystone v2 and v3.
216
+        # For keystone v2 we need a user:
217
+        #  - 'demo' user
218
+        #  - has a project 'demo'
219
+        #  - in the 'demo' project
220
+        #  - with an 'admin' role
221
+        # For keystone v3 we need a user:
222
+        #  - 'default' domain
223
+        #  - 'demo' user
224
+        #  - 'demo' project
225
+        #  - 'admin' role -- to be able to delete.
226
+
227
+        # barbican requires a user with creator or admin role on the project
228
+        # when creating a secret (which this test does).  Therefore, we create
229
+        # a demo user, demo project, and then get a demo barbican client and do
230
+        # the secret.  ensure that the default domain is created.
231
+
232
+        if self._keystone_version == '2':
233
+            # find or create the 'demo' tenant (project)
234
+            tenant = self._find_or_create(
235
+                items=self.keystone.tenants.list(),
236
+                key=lambda t: t.name == 'demo',
237
+                create=lambda: self.keystone.tenants.create(
238
+                    tenant_name="demo",
239
+                    description="Demo for testing barbican",
240
+                    enabled=True))
241
+            # find or create the demo user
242
+            demo_user = self._find_or_create(
243
+                items=self.keystone.users.list(),
244
+                key=lambda u: u.name == 'demo',
245
+                create=lambda: self.keystone.users.create(
246
+                    name='demo',
247
+                    password='pass',
248
+                    tenant_id=tenant.id))
249
+            # find the admin role
250
+            # already be created - if not, then this will fail later.
251
+            admin_role = self._find_or_create(
252
+                items=self.keystone.roles.list(),
253
+                key=lambda r: r.name.lower() == 'admin',
254
+                create=lambda: None)
255
+            # grant the role if it isn't already created.
256
+            # now grant the creator role to the demo user.
257
+            self._find_or_create(
258
+                items=self.keystone.roles.roles_for_user(
259
+                    demo_user, tenant=tenant),
260
+                key=lambda r: r.name.lower() == admin_role.name.lower(),
261
+                create=lambda: self.keystone.roles.add_user_role(
262
+                    demo_user, admin_role, tenant=tenant))
263
+            # now we can finally get the barbican client and create the secret
264
+            keystone_ep = self.keystone.service_catalog.url_for(
265
+                service_type='identity', endpoint_type='publicURL')
266
+            auth = keystone_identity.v2.Password(
267
+                username=demo_user.name,
268
+                password='pass',
269
+                tenant_name=tenant.name,
270
+                auth_url=keystone_ep)
271
+
272
+        else:
273
+            # find or create the 'default' domain
274
+            domain = self._find_or_create(
275
+                items=self.keystone.domains.list(),
276
+                key=lambda u: u.name == 'default',
277
+                create=lambda: self.keystone.domains.create(
278
+                    "default",
279
+                    description="domain for barbican testing",
280
+                    enabled=True))
281
+            # find or create the 'demo' user
282
+            demo_user = self._find_or_create(
283
+                items=self.keystone.users.list(domain=domain.id),
284
+                key=lambda u: u.name == 'demo',
285
+                create=lambda: self.keystone.users.create(
286
+                    'demo',
287
+                    domain=domain.id,
288
+                    description="Demo user for barbican tests",
289
+                    enabled=True,
290
+                    email="demo@example.com",
291
+                    password="pass"))
292
+            # find or create the 'demo' project
293
+            demo_project = self._find_or_create(
294
+                items=self.keystone.projects.list(domain=domain.id),
295
+                key=lambda x: x.name == 'demo',
296
+                create=lambda: self.keystone.projects.create(
297
+                    'demo',
298
+                    domain=domain.id,
299
+                    description='barbican testing project',
300
+                    enabled=True))
301
+            # create the role for the user - needs to be admin so that the
302
+            # secret can be deleted - note there is only one admin role, and it
303
+            # should already be created - if not, then this will fail later.
304
+            admin_role = self._find_or_create(
305
+                items=self.keystone.roles.list(),
306
+                key=lambda r: r.name.lower() == 'admin',
307
+                create=lambda: None)
308
+            # now grant the creator role to the demo user.
309
+            try:
310
+                self.keystone.roles.check(
311
+                    role=admin_role,
312
+                    user=demo_user,
313
+                    project=demo_project)
314
+            except keystoneclient.exceptions.NotFound:
315
+                # create it if it isn't found
316
+                self.keystone.roles.grant(
317
+                    role=admin_role,
318
+                    user=demo_user,
319
+                    project=demo_project)
320
+            # now we can finally get the barbican client and create the secret
321
+            keystone_ep = self.keystone.service_catalog.url_for(
322
+                service_type='identity', endpoint_type='publicURL')
323
+            auth = keystone_identity.v3.Password(
324
+                user_domain_name=domain.name,
325
+                username=demo_user.name,
326
+                password='pass',
327
+                project_domain_name=domain.name,
328
+                project_name=demo_project.name,
329
+                auth_url=keystone_ep)
330
+
331
+        # Now we carry on with common v2 and v3 code
332
+        sess = keystone_session.Session(auth=auth)
333
+        # Authenticate admin with barbican endpoint
334
+        barbican_ep = self.keystone.service_catalog.url_for(
335
+            service_type='key-manager', endpoint_type='publicURL')
336
+        barbican = barbican_client.Client(session=sess,
337
+                                          endpoint=barbican_ep)
338
+
339
+        # before creating a secret with the HSM we have to ensure that the mkek
340
+        # is generated.  This is done with an action WHICH will fail if the
341
+        # mkek has already been generated.  We look for the duplicate message
342
+        # and if it exists then we assume that it is okay.
343
+        action_id = u.run_action(self.d.sentry['barbican'][0], 'generate-mkek')
344
+        assert u.wait_on_action(action_id), "generate-mkek action failed."
345
+
346
+        # now create the secret.
347
+        my_secret = barbican.secrets.create()
348
+        my_secret.name = u'Random plain text password'
349
+        my_secret.payload = u'password'
350
+        my_secret_ref = my_secret.store()
351
+        assert(my_secret_ref is not None)
352
+        # and now delete the secret
353
+        my_secret.delete()
354
+        u.log.debug('OK')

+ 10
- 0
src/tests/gate-basic-xenial-mitaka-keystone-v2 View File

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

+ 10
- 0
src/tests/gate-basic-xenial-mitaka-keystone-v3 View File

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

+ 17
- 0
src/tests/tests.yaml View File

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

+ 1
- 1
src/tox.ini View File

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

Loading…
Cancel
Save