summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MANIFEST.in6
-rw-r--r--barbican_tempest_plugin/clients.py14
-rw-r--r--barbican_tempest_plugin/tests/scenario/barbican_manager.py184
-rw-r--r--barbican_tempest_plugin/tests/scenario/manager.py555
-rw-r--r--barbican_tempest_plugin/tests/scenario/test_image_signing.py83
-rw-r--r--barbican_tempest_plugin/tests/scenario/test_volume_encryption.py115
-rw-r--r--setup.cfg2
-rw-r--r--test-requirements.txt1
-rwxr-xr-xtools/pre_test_hook.sh9
-rw-r--r--tox.ini3
10 files changed, 960 insertions, 12 deletions
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index c978a52..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,6 +0,0 @@
1include AUTHORS
2include ChangeLog
3exclude .gitignore
4exclude .gitreview
5
6global-exclude *.pyc
diff --git a/barbican_tempest_plugin/clients.py b/barbican_tempest_plugin/clients.py
index 078f383..9c009ba 100644
--- a/barbican_tempest_plugin/clients.py
+++ b/barbican_tempest_plugin/clients.py
@@ -12,14 +12,22 @@
12# License for the specific language governing permissions and limitations under 12# License for the specific language governing permissions and limitations under
13# the License. 13# the License.
14 14
15from tempest import clients
16from tempest.common import credentials_factory as common_creds
15from tempest import config 17from tempest import config
16from tempest.lib.services import clients 18from tempest.lib.services import clients as cli
17
18 19
19CONF = config.CONF 20CONF = config.CONF
20 21
22ADMIN_CREDS = common_creds.get_configured_admin_credentials()
23
24
25class Manager(clients.Manager):
26 def __init__(self, credentials=ADMIN_CREDS):
27 super(Manager, self).__init__(credentials)
28
21 29
22class Clients(clients.ServiceClients): 30class Clients(cli.ServiceClients):
23 """Tempest stable service clients and loaded plugins service clients""" 31 """Tempest stable service clients and loaded plugins service clients"""
24 32
25 def __init__(self, credentials, service=None): 33 def __init__(self, credentials, service=None):
diff --git a/barbican_tempest_plugin/tests/scenario/barbican_manager.py b/barbican_tempest_plugin/tests/scenario/barbican_manager.py
new file mode 100644
index 0000000..a98d2cc
--- /dev/null
+++ b/barbican_tempest_plugin/tests/scenario/barbican_manager.py
@@ -0,0 +1,184 @@
1# Copyright 2017 Johns Hopkins Applied Physics Lab
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16import base64
17from datetime import datetime
18from datetime import timedelta
19import os
20
21from cryptography.hazmat.backends import default_backend
22from cryptography.hazmat.primitives.asymmetric import padding
23from cryptography.hazmat.primitives.asymmetric import rsa
24from cryptography.hazmat.primitives import hashes
25from cryptography.hazmat.primitives import serialization
26from cryptography import x509
27from cryptography.x509.oid import NameOID
28
29from oslo_log import log as logging
30from tempest import config
31
32from barbican_tempest_plugin import clients
33from barbican_tempest_plugin.tests.scenario import manager as mgr
34
35CONF = config.CONF
36LOG = logging.getLogger(__name__)
37
38
39class BarbicanScenarioTest(mgr.ScenarioTest):
40
41 credentials = ('primary', 'admin')
42 manager = clients.Manager()
43
44 def setUp(self):
45 super(BarbicanScenarioTest, self).setUp()
46 self.img_file = os.path.join(CONF.scenario.img_dir,
47 CONF.scenario.img_file)
48 self.private_key = rsa.generate_private_key(public_exponent=3,
49 key_size=1024,
50 backend=default_backend())
51 self.signing_certificate = self._create_self_signed_certificate(
52 self.private_key
53 )
54 self.signing_cert_uuid = self._store_cert(
55 self.signing_certificate
56 )
57
58 @classmethod
59 def skip_checks(cls):
60 super(BarbicanScenarioTest, cls).skip_checks()
61 if not CONF.service_available.barbican:
62 raise cls.skipException('Barbican is not enabled.')
63
64 @classmethod
65 def setup_clients(cls):
66 super(BarbicanScenarioTest, cls).setup_clients()
67
68 os = getattr(cls, 'os_%s' % cls.credentials[0])
69 os_adm = getattr(cls, 'os_%s' % cls.credentials[1])
70 cls.consumer_client = os.secret_v1.ConsumerClient(
71 service='key-manager'
72 )
73 cls.container_client = os.secret_v1.ContainerClient(
74 service='key-manager'
75 )
76 cls.order_client = os.secret_v1.OrderClient(service='key-manager')
77 cls.secret_client = os.secret_v1.SecretClient(service='key-manager')
78 cls.secret_metadata_client = os.secret_v1.SecretMetadataClient(
79 service='key-manager'
80 )
81
82 if CONF.compute_feature_enabled.attach_encrypted_volume:
83 if CONF.volume_feature_enabled.api_v2:
84 cls.admin_volume_types_client =\
85 os_adm.volume_types_v2_client
86 cls.admin_encryption_types_client =\
87 os_adm.encryption_types_v2_client
88 else:
89 cls.admin_volume_types_client =\
90 os_adm.volume_types_client
91 cls.admin_encryption_types_client =\
92 os_adm.encryption_types_client
93
94 def _get_uuid(self, href):
95 return href.split('/')[-1]
96
97 def _create_self_signed_certificate(self, private_key):
98 issuer = x509.Name([
99 x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
100 x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"CA"),
101 x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
102 x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"),
103 x509.NameAttribute(NameOID.COMMON_NAME, u"Test Certificate"),
104 ])
105 cert_builder = x509.CertificateBuilder(
106 issuer_name=issuer, subject_name=issuer,
107 public_key=private_key.public_key(),
108 serial_number=x509.random_serial_number(),
109 not_valid_before=datetime.utcnow(),
110 not_valid_after=datetime.utcnow() + timedelta(days=10)
111 )
112 cert = cert_builder.sign(private_key,
113 hashes.SHA256(),
114 default_backend())
115 return cert
116
117 def _store_cert(self, cert):
118 pem_encoding = cert.public_bytes(encoding=serialization.Encoding.PEM)
119 cert_b64 = base64.b64encode(pem_encoding)
120 expire_time = (datetime.utcnow() + timedelta(days=5))
121 LOG.debug("Uploading certificate to barbican")
122 result = self.secret_client.create_secret(
123 expiration=expire_time.isoformat(), algorithm="rsa",
124 secret_type="certificate",
125 payload_content_type="application/octet-stream",
126 payload_content_encoding="base64",
127 payload=cert_b64
128 )
129 LOG.debug("Certificate uploaded to barbican (%s)", result)
130 return self._get_uuid(result['secret_ref'])
131
132 def _sign_image(self, image_file):
133 LOG.debug("Creating signature for image data")
134 signer = self.private_key.signer(
135 padding.PSS(
136 mgf=padding.MGF1(hashes.SHA256()),
137 salt_length=padding.PSS.MAX_LENGTH
138 ),
139 hashes.SHA256()
140 )
141 chunk_bytes = 8192
142 with open(image_file, 'rb') as f:
143 chunk = f.read(chunk_bytes)
144 while len(chunk) > 0:
145 signer.update(chunk)
146 chunk = f.read(chunk_bytes)
147 signature = signer.finalize()
148 signature_b64 = base64.b64encode(signature)
149 return signature_b64
150
151 def sign_and_upload_image(self):
152 img_signature = self._sign_image(self.img_file)
153
154 img_properties = {
155 'img_signature': img_signature,
156 'img_signature_certificate_uuid': self.signing_cert_uuid,
157 'img_signature_key_type': 'RSA-PSS',
158 'img_signature_hash_method': 'SHA-256',
159 }
160
161 LOG.debug("Uploading image with signature metadata properties")
162 img_uuid = self._image_create(
163 'signed_img',
164 CONF.scenario.img_container_format,
165 self.img_file,
166 disk_format=CONF.scenario.img_disk_format,
167 properties=img_properties
168 )
169 LOG.debug("Uploaded image %s", img_uuid)
170
171 return img_uuid
172
173 def create_encryption_type(self, client=None, type_id=None, provider=None,
174 key_size=None, cipher=None,
175 control_location=None):
176 if not client:
177 client = self.admin_encryption_types_client
178 if not type_id:
179 volume_type = self.create_volume_type()
180 type_id = volume_type['id']
181 LOG.debug("Creating an encryption type for volume type: %s", type_id)
182 client.create_encryption_type(
183 type_id, provider=provider, key_size=key_size, cipher=cipher,
184 control_location=control_location)
diff --git a/barbican_tempest_plugin/tests/scenario/manager.py b/barbican_tempest_plugin/tests/scenario/manager.py
new file mode 100644
index 0000000..698c70d
--- /dev/null
+++ b/barbican_tempest_plugin/tests/scenario/manager.py
@@ -0,0 +1,555 @@
1# Copyright 2012 OpenStack Foundation
2# Copyright 2013 IBM Corp.
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17from oslo_log import log
18
19from tempest.common import compute
20from tempest.common import image as common_image
21from tempest.common.utils.linux import remote_client
22from tempest.common import waiters
23from tempest import config
24from tempest import exceptions
25from tempest.lib.common.utils import data_utils
26from tempest.lib.common.utils import test_utils
27from tempest.lib import exceptions as lib_exc
28import tempest.test
29
30CONF = config.CONF
31
32LOG = log.getLogger(__name__)
33
34
35class ScenarioTest(tempest.test.BaseTestCase):
36 """Base class for scenario tests. Uses tempest own clients. """
37
38 credentials = ['primary']
39
40 @classmethod
41 def setup_clients(cls):
42 super(ScenarioTest, cls).setup_clients()
43 # Clients (in alphabetical order)
44 cls.flavors_client = cls.manager.flavors_client
45 cls.compute_floating_ips_client = (
46 cls.manager.compute_floating_ips_client)
47 if CONF.service_available.glance:
48 # Check if glance v1 is available to determine which client to use.
49 if CONF.image_feature_enabled.api_v1:
50 cls.image_client = cls.manager.image_client
51 elif CONF.image_feature_enabled.api_v2:
52 cls.image_client = cls.manager.image_client_v2
53 else:
54 raise lib_exc.InvalidConfiguration(
55 'Either api_v1 or api_v2 must be True in '
56 '[image-feature-enabled].')
57 # Compute image client
58 cls.compute_images_client = cls.manager.compute_images_client
59 cls.keypairs_client = cls.manager.keypairs_client
60 # Nova security groups client
61 cls.compute_security_groups_client = (
62 cls.manager.compute_security_groups_client)
63 cls.compute_security_group_rules_client = (
64 cls.manager.compute_security_group_rules_client)
65 cls.servers_client = cls.manager.servers_client
66 # Neutron network client
67 cls.networks_client = cls.manager.networks_client
68 cls.ports_client = cls.manager.ports_client
69 cls.routers_client = cls.manager.routers_client
70 cls.subnets_client = cls.manager.subnets_client
71 cls.floating_ips_client = cls.manager.floating_ips_client
72 cls.security_groups_client = cls.manager.security_groups_client
73 cls.security_group_rules_client = (
74 cls.manager.security_group_rules_client)
75
76 if CONF.volume_feature_enabled.api_v2:
77 cls.volumes_client = cls.manager.volumes_v2_client
78 cls.snapshots_client = cls.manager.snapshots_v2_client
79 else:
80 cls.volumes_client = cls.manager.volumes_client
81 cls.snapshots_client = cls.manager.snapshots_client
82
83 # ## Test functions library
84 #
85 # The create_[resource] functions only return body and discard the
86 # resp part which is not used in scenario tests
87
88 def _create_port(self, network_id, client=None, namestart='port-quotatest',
89 **kwargs):
90 if not client:
91 client = self.ports_client
92 name = data_utils.rand_name(namestart)
93 result = client.create_port(
94 name=name,
95 network_id=network_id,
96 **kwargs)
97 self.assertIsNotNone(result, 'Unable to allocate port')
98 port = result['port']
99 self.addCleanup(test_utils.call_and_ignore_notfound_exc,
100 client.delete_port, port['id'])
101 return port
102
103 def create_keypair(self, client=None):
104 if not client:
105 client = self.keypairs_client
106 name = data_utils.rand_name(self.__class__.__name__)
107 # We don't need to create a keypair by pubkey in scenario
108 body = client.create_keypair(name=name)
109 self.addCleanup(client.delete_keypair, name)
110 return body['keypair']
111
112 def create_server(self, name=None, image_id=None, flavor=None,
113 validatable=False, wait_until='ACTIVE',
114 clients=None, **kwargs):
115 """Wrapper utility that returns a test server.
116
117 This wrapper utility calls the common create test server and
118 returns a test server. The purpose of this wrapper is to minimize
119 the impact on the code of the tests already using this
120 function.
121 """
122
123 # NOTE(jlanoux): As a first step, ssh checks in the scenario
124 # tests need to be run regardless of the run_validation and
125 # validatable parameters and thus until the ssh validation job
126 # becomes voting in CI. The test resources management and IP
127 # association are taken care of in the scenario tests.
128 # Therefore, the validatable parameter is set to false in all
129 # those tests. In this way create_server just return a standard
130 # server and the scenario tests always perform ssh checks.
131
132 # Needed for the cross_tenant_traffic test:
133 if clients is None:
134 clients = self.manager
135
136 if name is None:
137 name = data_utils.rand_name(self.__class__.__name__ + "-server")
138
139 vnic_type = CONF.network.port_vnic_type
140
141 # If vnic_type is configured create port for
142 # every network
143 if vnic_type:
144 ports = []
145
146 create_port_body = {'binding:vnic_type': vnic_type,
147 'namestart': 'port-smoke'}
148 if kwargs:
149 # Convert security group names to security group ids
150 # to pass to create_port
151 if 'security_groups' in kwargs:
152 security_groups = \
153 clients.security_groups_client.list_security_groups(
154 ).get('security_groups')
155 sec_dict = dict([(s['name'], s['id'])
156 for s in security_groups])
157
158 sec_groups_names = [s['name'] for s in kwargs.pop(
159 'security_groups')]
160 security_groups_ids = [sec_dict[s]
161 for s in sec_groups_names]
162
163 if security_groups_ids:
164 create_port_body[
165 'security_groups'] = security_groups_ids
166 networks = kwargs.pop('networks', [])
167 else:
168 networks = []
169
170 # If there are no networks passed to us we look up
171 # for the project's private networks and create a port.
172 # The same behaviour as we would expect when passing
173 # the call to the clients with no networks
174 if not networks:
175 networks = clients.networks_client.list_networks(
176 **{'router:external': False, 'fields': 'id'})['networks']
177
178 # It's net['uuid'] if networks come from kwargs
179 # and net['id'] if they come from
180 # clients.networks_client.list_networks
181 for net in networks:
182 net_id = net.get('uuid', net.get('id'))
183 if 'port' not in net:
184 port = self._create_port(network_id=net_id,
185 client=clients.ports_client,
186 **create_port_body)
187 ports.append({'port': port['id']})
188 else:
189 ports.append({'port': net['port']})
190 if ports:
191 kwargs['networks'] = ports
192 self.ports = ports
193
194 tenant_network = self.get_tenant_network()
195
196 body, servers = compute.create_test_server(
197 clients,
198 tenant_network=tenant_network,
199 wait_until=wait_until,
200 name=name, flavor=flavor,
201 image_id=image_id, **kwargs)
202
203 self.addCleanup(waiters.wait_for_server_termination,
204 clients.servers_client, body['id'])
205 self.addCleanup(test_utils.call_and_ignore_notfound_exc,
206 clients.servers_client.delete_server, body['id'])
207 server = clients.servers_client.show_server(body['id'])['server']
208 return server
209
210 def create_volume(self, size=None, name=None, snapshot_id=None,
211 imageRef=None, volume_type=None):
212 if size is None:
213 size = CONF.volume.volume_size
214 if imageRef:
215 image = self.compute_images_client.show_image(imageRef)['image']
216 min_disk = image.get('minDisk')
217 size = max(size, min_disk)
218 if name is None:
219 name = data_utils.rand_name(self.__class__.__name__ + "-volume")
220 kwargs = {'display_name': name,
221 'snapshot_id': snapshot_id,
222 'imageRef': imageRef,
223 'volume_type': volume_type,
224 'size': size}
225 volume = self.volumes_client.create_volume(**kwargs)['volume']
226
227 self.addCleanup(self.volumes_client.wait_for_resource_deletion,
228 volume['id'])
229 self.addCleanup(test_utils.call_and_ignore_notfound_exc,
230 self.volumes_client.delete_volume, volume['id'])
231
232 # NOTE(e0ne): Cinder API v2 uses name instead of display_name
233 if 'display_name' in volume:
234 self.assertEqual(name, volume['display_name'])
235 else:
236 self.assertEqual(name, volume['name'])
237 waiters.wait_for_volume_resource_status(self.volumes_client,
238 volume['id'], 'available')
239 # The volume retrieved on creation has a non-up-to-date status.
240 # Retrieval after it becomes active ensures correct details.
241 volume = self.volumes_client.show_volume(volume['id'])['volume']
242 return volume
243
244 def create_volume_type(self, client=None, name=None, backend_name=None):
245 if not client:
246 client = self.admin_volume_types_client
247 if not name:
248 class_name = self.__class__.__name__
249 name = data_utils.rand_name(class_name + '-volume-type')
250 randomized_name = data_utils.rand_name('scenario-type-' + name)
251
252 LOG.debug("Creating a volume type: %s on backend %s",
253 randomized_name, backend_name)
254 extra_specs = {}
255 if backend_name:
256 extra_specs = {"volume_backend_name": backend_name}
257
258 body = client.create_volume_type(name=randomized_name,
259 extra_specs=extra_specs)
260 volume_type = body['volume_type']
261 self.assertIn('id', volume_type)
262 self.addCleanup(client.delete_volume_type, volume_type['id'])
263 return volume_type
264
265 def _image_create(self, name, fmt, path,
266 disk_format=None, properties=None):
267 if properties is None:
268 properties = {}
269 name = data_utils.rand_name('%s-' % name)
270 params = {
271 'name': name,
272 'container_format': fmt,
273 'disk_format': disk_format or fmt,
274 }
275 if CONF.image_feature_enabled.api_v1:
276 params['is_public'] = 'False'
277 params['properties'] = properties
278 params = {'headers': common_image.image_meta_to_headers(**params)}
279 else:
280 params['visibility'] = 'private'
281 # Additional properties are flattened out in the v2 API.
282 params.update(properties)
283 body = self.image_client.create_image(**params)
284 image = body['image'] if 'image' in body else body
285 self.addCleanup(self.image_client.delete_image, image['id'])
286 self.assertEqual("queued", image['status'])
287 with open(path, 'rb') as image_file:
288 if CONF.image_feature_enabled.api_v1:
289 self.image_client.update_image(image['id'], data=image_file)
290 else:
291 self.image_client.store_image_file(image['id'], image_file)
292 return image['id']
293
294 def rebuild_server(self, server_id, image=None,
295 preserve_ephemeral=False, wait=True,
296 rebuild_kwargs=None):
297 if image is None:
298 image = CONF.compute.image_ref
299
300 rebuild_kwargs = rebuild_kwargs or {}
301
302 LOG.debug("Rebuilding server (id: %s, image: %s, preserve eph: %s)",
303 server_id, image, preserve_ephemeral)
304 self.servers_client.rebuild_server(
305 server_id=server_id, image_ref=image,
306 preserve_ephemeral=preserve_ephemeral,
307 **rebuild_kwargs)
308 if wait:
309 waiters.wait_for_server_status(self.servers_client,
310 server_id, 'ACTIVE')
311
312 def create_floating_ip(self, thing, pool_name=None):
313 """Create a floating IP and associates to a server on Nova"""
314
315 if not pool_name:
316 pool_name = CONF.network.floating_network_name
317 floating_ip = (self.compute_floating_ips_client.
318 create_floating_ip(pool=pool_name)['floating_ip'])
319 self.addCleanup(test_utils.call_and_ignore_notfound_exc,
320 self.compute_floating_ips_client.delete_floating_ip,
321 floating_ip['id'])
322 self.compute_floating_ips_client.associate_floating_ip_to_server(
323 floating_ip['ip'], thing['id'])
324 return floating_ip
325
326 def nova_volume_attach(self, server, volume_to_attach):
327 volume = self.servers_client.attach_volume(
328 server['id'], volumeId=volume_to_attach['id'], device='/dev/%s'
329 % CONF.compute.volume_device_name)['volumeAttachment']
330 self.assertEqual(volume_to_attach['id'], volume['id'])
331 waiters.wait_for_volume_resource_status(self.volumes_client,
332 volume['id'], 'in-use')
333
334 # Return the updated volume after the attachment
335 return self.volumes_client.show_volume(volume['id'])['volume']
336
337 def nova_volume_detach(self, server, volume):
338 self.servers_client.detach_volume(server['id'], volume['id'])
339 waiters.wait_for_volume_resource_status(self.volumes_client,
340 volume['id'], 'available')
341
342 volume = self.volumes_client.show_volume(volume['id'])['volume']
343 self.assertEqual('available', volume['status'])
344
345 def create_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
346 private_key=None):
347 ssh_client = self.get_remote_client(ip_address,
348 private_key=private_key)
349 if dev_name is not None:
350 ssh_client.make_fs(dev_name)
351 ssh_client.exec_command('sudo mount /dev/%s %s' % (dev_name,
352 mount_path))
353 cmd_timestamp = 'sudo sh -c "date > %s/timestamp; sync"' % mount_path
354 ssh_client.exec_command(cmd_timestamp)
355 timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
356 % mount_path)
357 if dev_name is not None:
358 ssh_client.exec_command('sudo umount %s' % mount_path)
359 return timestamp
360
361 def get_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
362 private_key=None):
363 ssh_client = self.get_remote_client(ip_address,
364 private_key=private_key)
365 if dev_name is not None:
366 ssh_client.mount(dev_name, mount_path)
367 timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
368 % mount_path)
369 if dev_name is not None:
370 ssh_client.exec_command('sudo umount %s' % mount_path)
371 return timestamp
372
373 def get_server_ip(self, server):
374 """Get the server fixed or floating IP.
375
376 Based on the configuration we're in, return a correct ip
377 address for validating that a guest is up.
378 """
379 if CONF.validation.connect_method == 'floating':
380 # The tests calling this method don't have a floating IP
381 # and can't make use of the validation resources. So the
382 # method is creating the floating IP there.
383 return self.create_floating_ip(server)['ip']
384 elif CONF.validation.connect_method == 'fixed':
385 # Determine the network name to look for based on config or creds
386 # provider network resources.
387 if CONF.validation.network_for_ssh:
388 addresses = server['addresses'][
389 CONF.validation.network_for_ssh]
390 else:
391 creds_provider = self._get_credentials_provider()
392 net_creds = creds_provider.get_primary_creds()
393 network = getattr(net_creds, 'network', None)
394 addresses = (server['addresses'][network['name']]
395 if network else [])
396 for address in addresses:
397 if (address['version'] == CONF.validation.ip_version_for_ssh
398 and address['OS-EXT-IPS:type'] == 'fixed'):
399 return address['addr']
400 raise exceptions.ServerUnreachable(server_id=server['id'])
401 else:
402 raise lib_exc.InvalidConfiguration()
403
404 def get_remote_client(self, ip_address, username=None, private_key=None):
405 """Get a SSH client to a remote server
406
407 @param ip_address the server floating or fixed IP address to use
408 for ssh validation
409 @param username name of the Linux account on the remote server
410 @param private_key the SSH private key to use
411 @return a RemoteClient object
412 """
413
414 if username is None:
415 username = CONF.validation.image_ssh_user
416 # Set this with 'keypair' or others to log in with keypair or
417 # username/password.
418 if CONF.validation.auth_method == 'keypair':
419 password = None
420 if private_key is None:
421 private_key = self.keypair['private_key']
422 else:
423 password = CONF.validation.image_ssh_password
424 private_key = None
425 linux_client = remote_client.RemoteClient(ip_address, username,
426 pkey=private_key,
427 password=password)
428 try:
429 linux_client.validate_authentication()
430 except Exception as e:
431 message = ('Initializing SSH connection to %(ip)s failed. '
432 'Error: %(error)s' % {'ip': ip_address,
433 'error': e})
434 caller = test_utils.find_test_caller()
435 if caller:
436 message = '(%s) %s' % (caller, message)
437 LOG.exception(message)
438 self._log_console_output()
439 raise
440
441 return linux_client
442
443 def _default_security_group(self, client=None, tenant_id=None):
444 """Get default secgroup for given tenant_id.
445
446 :returns: default secgroup for given tenant
447 """
448 if client is None:
449 client = self.security_groups_client
450 if not tenant_id:
451 tenant_id = client.tenant_id
452 sgs = [
453 sg for sg in list(client.list_security_groups().values())[0]
454 if sg['tenant_id'] == tenant_id and sg['name'] == 'default'
455 ]
456 msg = "No default security group for tenant %s." % (tenant_id)
457 self.assertGreater(len(sgs), 0, msg)
458 return sgs[0]
459
460 def _create_security_group(self):
461 # Create security group
462 sg_name = data_utils.rand_name(self.__class__.__name__)
463 sg_desc = sg_name + " description"
464 secgroup = self.compute_security_groups_client.create_security_group(
465 name=sg_name, description=sg_desc)['security_group']
466 self.assertEqual(secgroup['name'], sg_name)
467 self.assertEqual(secgroup['description'], sg_desc)
468 self.addCleanup(
469 test_utils.call_and_ignore_notfound_exc,
470 self.compute_security_groups_client.delete_security_group,
471 secgroup['id'])
472
473 # Add rules to the security group
474 self._create_loginable_secgroup_rule(secgroup['id'])
475
476 return secgroup
477
478 def _create_loginable_secgroup_rule(self, secgroup_id=None):
479 _client = self.compute_security_groups_client
480 _client_rules = self.compute_security_group_rules_client
481 if secgroup_id is None:
482 sgs = _client.list_security_groups()['security_groups']
483 for sg in sgs:
484 if sg['name'] == 'default':
485 secgroup_id = sg['id']
486
487 # These rules are intended to permit inbound ssh and icmp
488 # traffic from all sources, so no group_id is provided.
489 # Setting a group_id would only permit traffic from ports
490 # belonging to the same security group.
491 rulesets = [
492 {
493 # ssh
494 'ip_protocol': 'tcp',
495 'from_port': 22,
496 'to_port': 22,
497 'cidr': '0.0.0.0/0',
498 },
499 {
500 # ping
501 'ip_protocol': 'icmp',
502 'from_port': -1,
503 'to_port': -1,
504 'cidr': '0.0.0.0/0',
505 }
506 ]
507 rules = list()
508 for ruleset in rulesets:
509 sg_rule = _client_rules.create_security_group_rule(
510 parent_group_id=secgroup_id, **ruleset)['security_group_rule']
511 rules.append(sg_rule)
512 return rules
513
514 def _create_security_group_rule(self, secgroup=None,
515 sec_group_rules_client=None,
516 tenant_id=None,
517 security_groups_client=None, **kwargs):
518 """Create a rule from a dictionary of rule parameters.
519
520 Create a rule in a secgroup. if secgroup not defined will search for
521 default secgroup in tenant_id.
522
523 :param secgroup: the security group.
524 :param tenant_id: if secgroup not passed -- the tenant in which to
525 search for default secgroup
526 :param kwargs: a dictionary containing rule parameters:
527 for example, to allow incoming ssh:
528 rule = {
529 direction: 'ingress'
530 protocol:'tcp',
531 port_range_min: 22,
532 port_range_max: 22
533 }
534 """
535 if sec_group_rules_client is None:
536 sec_group_rules_client = self.security_group_rules_client
537 if security_groups_client is None:
538 security_groups_client = self.security_groups_client
539 if not tenant_id:
540 tenant_id = security_groups_client.tenant_id
541 if secgroup is None:
542 secgroup = self._default_security_group(
543 client=security_groups_client, tenant_id=tenant_id)
544
545 ruleset = dict(security_group_id=secgroup['id'],
546 tenant_id=secgroup['tenant_id'])
547 ruleset.update(kwargs)
548
549 sg_rule = sec_group_rules_client.create_security_group_rule(**ruleset)
550 sg_rule = sg_rule['security_group_rule']
551
552 self.assertEqual(secgroup['tenant_id'], sg_rule['tenant_id'])
553 self.assertEqual(secgroup['id'], sg_rule['security_group_id'])
554
555 return sg_rule
diff --git a/barbican_tempest_plugin/tests/scenario/test_image_signing.py b/barbican_tempest_plugin/tests/scenario/test_image_signing.py
new file mode 100644
index 0000000..9070578
--- /dev/null
+++ b/barbican_tempest_plugin/tests/scenario/test_image_signing.py
@@ -0,0 +1,83 @@
1# Copyright (c) 2017 Johns Hopkins University Applied Physics Laboratory
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_log import log as logging
16from tempest import config
17from tempest import exceptions
18from tempest.lib import decorators
19from tempest import test
20
21from barbican_tempest_plugin.tests.scenario import barbican_manager
22
23CONF = config.CONF
24LOG = logging.getLogger(__name__)
25
26
27class ImageSigningTest(barbican_manager.BarbicanScenarioTest):
28
29 @decorators.idempotent_id('4343df3c-5553-40ea-8705-0cce73b297a9')
30 @test.services('compute', 'image')
31 def test_signed_image_upload_and_boot(self):
32 """Test that Nova boots a signed image.
33
34 The test follows these steps:
35 * Create an asymmetric keypair
36 * Sign an image file with the private key
37 * Create a certificate with the public key
38 * Store the certificate in Barbican
39 * Store the signed image in Glance
40 * Boot the signed image
41 * Confirm the instance changes state to Active
42 """
43 img_uuid = self.sign_and_upload_image()
44
45 LOG.debug("Booting server with signed image %s", img_uuid)
46 instance = self.create_server(name='signed_img_server',
47 image_id=img_uuid,
48 wait_until='ACTIVE')
49 self.servers_client.delete_server(instance['id'])
50
51 @decorators.idempotent_id('74f022d6-a6ef-4458-96b7-541deadacf99')
52 @test.services('compute', 'image')
53 def test_signed_image_upload_boot_failure(self):
54 """Test that Nova refuses to boot an incorrectly signed image.
55
56 If the create_server call succeeds instead of throwing an
57 exception, it is likely that signature verification is not
58 turned on. To turn on signature verification, set
59 verify_glance_signatures=True in the nova configuration
60 file under the [glance] section.
61
62 The test follows these steps:
63 * Create an asymmetric keypair
64 * Sign an image file with the private key
65 * Create a certificate with the public key
66 * Store the certificate in Barbican
67 * Store the signed image in Glance
68 * Modify the signature to be incorrect
69 * Attempt to boot the incorrectly signed image
70 * Confirm an exception is thrown
71 """
72 img_uuid = self.sign_and_upload_image()
73
74 LOG.debug("Modifying image signature to be incorrect")
75 metadata = {'img_signature': 'fake_signature'}
76 self.compute_images_client.update_image_metadata(
77 img_uuid, metadata
78 )
79
80 self.assertRaisesRegex(exceptions.BuildErrorException,
81 "Signature verification for the image failed",
82 self.create_server,
83 image_id=img_uuid)
diff --git a/barbican_tempest_plugin/tests/scenario/test_volume_encryption.py b/barbican_tempest_plugin/tests/scenario/test_volume_encryption.py
new file mode 100644
index 0000000..9c42694
--- /dev/null
+++ b/barbican_tempest_plugin/tests/scenario/test_volume_encryption.py
@@ -0,0 +1,115 @@
1# Copyright (c) 2017 Johns Hopkins University Applied Physics Laboratory
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_log import log as logging
16from tempest import config
17from tempest.lib import decorators
18from tempest import test
19
20from barbican_tempest_plugin.tests.scenario import barbican_manager
21
22CONF = config.CONF
23LOG = logging.getLogger(__name__)
24
25
26class VolumeEncryptionTest(barbican_manager.BarbicanScenarioTest):
27
28 """The test suite for encrypted cinder volumes
29
30 This test is for verifying the functionality of encrypted cinder volumes.
31 For both LUKS and cryptsetup encryption types, this test performs
32 the following:
33 * Creates an image in Glance
34 * Boots an instance from the image
35 * Creates an encryption type (as admin)
36 * Creates a volume of that encryption type (as a regular user)
37 * Attaches and detaches the encrypted volume to the instance
38 NOTE (dane-fichter): These tests use a key stored in Barbican, unlike
39 the original volume encryption scenario in Tempest.
40 """
41
42 @classmethod
43 def skip_checks(cls):
44 super(VolumeEncryptionTest, cls).skip_checks()
45 if not CONF.compute_feature_enabled.attach_encrypted_volume:
46 raise cls.skipException('Encrypted volume attach is not supported')
47
48 def create_encrypted_volume(self, encryption_provider, volume_type):
49 volume_type = self.create_volume_type(name=volume_type)
50 self.create_encryption_type(type_id=volume_type['id'],
51 provider=encryption_provider,
52 key_size=256,
53 cipher='aes-xts-plain64',
54 control_location='front-end')
55 return self.create_volume(volume_type=volume_type['name'])
56
57 def attach_detach_volume(self, server, volume, keypair):
58 # Attach volume
59 attached_volume = self.nova_volume_attach(server, volume)
60
61 # Write a timestamp to volume
62 server_ip = self.get_server_ip(server)
63 timestamp = self.create_timestamp(
64 server_ip,
65 dev_name=CONF.compute.volume_device_name,
66 private_key=keypair['private_key']
67 )
68 timestamp2 = self.get_timestamp(
69 server_ip,
70 dev_name=CONF.compute.volume_device_name,
71 private_key=keypair['private_key']
72 )
73 self.assertEqual(timestamp, timestamp2)
74
75 # Detach volume
76 self.nova_volume_detach(server, attached_volume)
77
78 @decorators.idempotent_id('89165fb4-5534-4b9d-8429-97ccffb8f86f')
79 @test.services('compute', 'volume', 'image')
80 def test_encrypted_cinder_volumes_luks(self):
81 img_uuid = self.sign_and_upload_image()
82 LOG.info("Creating keypair and security group")
83 keypair = self.create_keypair()
84 security_group = self._create_security_group()
85 server = self.create_server(
86 name='signed_img_server',
87 image_id=img_uuid,
88 key_name=keypair['name'],
89 security_groups=[{'name': security_group['name']}],
90 wait_until='ACTIVE'
91 )
92 volume = self.create_encrypted_volume('nova.volume.encryptors.'
93 'luks.LuksEncryptor',
94 volume_type='luks')
95 self.attach_detach_volume(server, volume, keypair)
96
97 @decorators.idempotent_id('cbc752ed-b716-4727-910f-956ccf965723')
98 @test.services('compute', 'volume', 'image')
99 def test_encrypted_cinder_volumes_cryptsetup(self):
100 img_uuid = self.sign_and_upload_image()
101 LOG.info("Creating keypair and security group")
102 keypair = self.create_keypair()
103 security_group = self._create_security_group()
104
105 server = self.create_server(
106 name='signed_img_server',
107 image_id=img_uuid,
108 key_name=keypair['name'],
109 security_groups=[{'name': security_group['name']}],
110 wait_until='ACTIVE'
111 )
112 volume = self.create_encrypted_volume('nova.volume.encryptors.'
113 'cryptsetup.CryptsetupEncryptor',
114 volume_type='cryptsetup')
115 self.attach_detach_volume(server, volume, keypair)
diff --git a/setup.cfg b/setup.cfg
index c91bdca..15565c1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -17,7 +17,7 @@ classifier =
17 Programming Language :: Python :: 2.7 17 Programming Language :: Python :: 2.7
18 Programming Language :: Python :: 3 18 Programming Language :: Python :: 3
19 Programming Language :: Python :: 3.3 19 Programming Language :: Python :: 3.3
20 Programming Language :: Python :: 3.4 20 Programming Language :: Python :: 3.5
21 21
22[files] 22[files]
23packages = 23packages =
diff --git a/test-requirements.txt b/test-requirements.txt
index 2bf9f2e..74109e4 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -7,6 +7,7 @@ hacking>=0.12.0,<0.13 # Apache-2.0
7python-subunit>=0.0.18 # Apache-2.0/BSD 7python-subunit>=0.0.18 # Apache-2.0/BSD
8sphinx>=1.2.1,!=1.3b1,<1.4 # BSD 8sphinx>=1.2.1,!=1.3b1,<1.4 # BSD
9oslosphinx>=4.7.0 # Apache-2.0 9oslosphinx>=4.7.0 # Apache-2.0
10oslotest>=1.10.0 # Apache-2.0
10testrepository>=0.0.18 # Apache-2.0/BSD 11testrepository>=0.0.18 # Apache-2.0/BSD
11testscenarios>=0.4 # Apache-2.0/BSD 12testscenarios>=0.4 # Apache-2.0/BSD
12testtools>=1.4.0 # MIT 13testtools>=1.4.0 # MIT
diff --git a/tools/pre_test_hook.sh b/tools/pre_test_hook.sh
index 806ca40..2640433 100755
--- a/tools/pre_test_hook.sh
+++ b/tools/pre_test_hook.sh
@@ -11,7 +11,16 @@ export LOCALCONF_PATH=$DEVSTACK_DIR/local.conf
11# Here we can set some configurations for local.conf 11# Here we can set some configurations for local.conf
12# for example, to pass some config options directly to .conf files 12# for example, to pass some config options directly to .conf files
13 13
14# For image signature verification tests
14echo -e '[[post-config|$NOVA_CONF]]' >> $LOCALCONF_PATH 15echo -e '[[post-config|$NOVA_CONF]]' >> $LOCALCONF_PATH
15echo -e '[glance]' >> $LOCALCONF_PATH 16echo -e '[glance]' >> $LOCALCONF_PATH
16echo -e 'verify_glance_signatures = True' >> $LOCALCONF_PATH 17echo -e 'verify_glance_signatures = True' >> $LOCALCONF_PATH
17 18
19# Allow dynamically created tempest users to create secrets
20# in barbican
21echo -e '[[test-config|$TEMPEST_CONFIG]]' >> $LOCALCONF_PATH
22echo -e '[auth]' >> $LOCALCONF_PATH
23echo -e 'tempest_roles=creator' >> $LOCALCONF_PATH
24# Glance v1 doesn't do signature verification on image upload
25echo -e '[image-feature-enabled]' >> $LOCALCONF_PATH
26echo -e 'api_v1=False' >> $LOCALCONF_PATH
diff --git a/tox.ini b/tox.ini
index f100b39..011fc96 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
1[tox] 1[tox]
2minversion = 2.0 2minversion = 2.0
3envlist = py34,py27,pypy,pep8 3envlist = py35,py27,pypy,pep8
4skipsdist = True 4skipsdist = True
5 5
6[testenv] 6[testenv]
@@ -15,7 +15,6 @@ commands = python setup.py test --slowest --testr-args='{posargs}'
15[testenv:pep8] 15[testenv:pep8]
16commands = 16commands =
17 flake8 {posargs} 17 flake8 {posargs}
18 check-uuid --package barbican_tempest_plugin
19 18
20[testenv:venv] 19[testenv:venv]
21commands = {posargs} 20commands = {posargs}