Barbican implementation for Certificates

A Barbican implementation of CertManager and a placeholder
implementation of CertGenerator (not supported yet).

Change-Id: Icdbf883a733101c84b9a7bb933782ef166b929f7
Partially-implements: blueprint tls-data-security
This commit is contained in:
Adam Harwell 2014-10-21 18:25:29 -05:00
parent ac4fe48813
commit 0f7e269821
10 changed files with 782 additions and 3 deletions

View File

@ -0,0 +1,102 @@
# Copyright (c) 2014 Rackspace US, Inc
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Common classes for Barbican certificate handling
"""
from barbicanclient import client as barbican_client
from keystoneclient.auth.identity import v3 as keystone_client
from keystoneclient import session
from oslo.config import cfg
from octavia.certificates.common import cert
from octavia.openstack.common import excutils
from octavia.openstack.common import gettextutils
from octavia.openstack.common import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_group('keystone_authtoken', 'octavia.common.config')
class BarbicanCert(cert.Cert):
"""Representation of a Cert based on the Barbican CertificateContainer."""
def __init__(self, cert_container):
if not isinstance(cert_container,
barbican_client.containers.CertificateContainer):
raise TypeError(gettextutils._LE(
"Retrieved Barbican Container is not of the correct type "
"(certificate)."))
self._cert_container = cert_container
def get_certificate(self):
return self._cert_container.certificate.payload
def get_intermediates(self):
return self._cert_container.intermediates.payload
def get_private_key(self):
return self._cert_container.private_key.payload
def get_private_key_passphrase(self):
return self._cert_container.private_key_passphrase.payload
class BarbicanKeystoneAuth(object):
_keystone_session = None
_barbican_client = None
@classmethod
def _get_keystone_session(cls):
"""Initializes a Keystone session.
:return: a Keystone Session object
:raises Exception: if the session cannot be established
"""
if not cls._keystone_session:
try:
kc = keystone_client.Password(
auth_url=CONF.keystone_authtoken.auth_uri,
username=CONF.keystone_authtoken.admin_user,
password=CONF.keystone_authtoken.admin_password,
project_id=CONF.keystone_authtoken.admin_project_id
)
cls._keystone_session = session.Session(auth=kc)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(gettextutils._LE(
"Error creating Keystone session: %s"), e)
return cls._keystone_session
@classmethod
def get_barbican_client(cls):
"""Creates a Barbican client object.
:return: a Barbican Client object
:raises Exception: if the client cannot be created
"""
if not cls._barbican_client:
try:
cls._barbican_client = barbican_client.Client(
session=cls._get_keystone_session()
)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(gettextutils._LE(
"Error creating Barbican client: %s"), e)
return cls._barbican_client

View File

@ -29,4 +29,4 @@ CONF.register_opts(certgen_opts, group='certificates')
def API():
cls = importutils.import_class(CONF.certgen.cert_generator_class)
return cls()
return cls()

View File

@ -0,0 +1,39 @@
# Copyright (c) 2014 Rackspace US, Inc
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Cert generator implementation for Barbican
"""
from octavia.certificates.generator import cert_gen
from octavia.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class BarbicanCertGenerator(cert_gen.CertGenerator):
"""Certificate Generator that wraps the Barbican client API."""
@staticmethod
def sign_cert(csr, validity):
"""Signs a certificate using our private CA based on the specified CSR.
:param csr: A Certificate Signing Request
:param validity: Valid for <validity> seconds from the current time
:return: Signed certificate
:raises Exception: if certificate signing fails
"""
raise NotImplementedError("Barbican does not yet support signing.")

View File

@ -29,4 +29,4 @@ CONF.register_opts(certmgr_opts, group='certificates')
def API():
cls = importutils.import_class(CONF.certmgr.cert_manager_class)
return cls()
return cls()

View File

@ -0,0 +1,205 @@
# Copyright (c) 2014 Rackspace US, Inc
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Cert manager implementation for Barbican
"""
from oslo.config import cfg
from octavia.certificates.common import barbican as barbican_common
from octavia.certificates.manager import cert_mgr
from octavia.openstack.common import excutils
from octavia.openstack.common import gettextutils
from octavia.openstack.common import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class BarbicanCertManager(cert_mgr.CertManager):
"""Certificate Manager that wraps the Barbican client API."""
@staticmethod
def store_cert(certificate, private_key, intermediates=None,
private_key_passphrase=None, expiration=None,
name='Octavia TLS Cert', **kwargs):
"""Stores a certificate in the certificate manager.
:param certificate: PEM encoded TLS certificate
:param private_key: private key for the supplied certificate
:param intermediates: ordered and concatenated intermediate certs
:param private_key_passphrase: optional passphrase for the supplied key
:param expiration: the expiration time of the cert in ISO 8601 format
:param name: a friendly name for the cert
:returns: the container_ref of the stored cert
:raises Exception: if certificate storage fails
"""
connection = barbican_common.BarbicanKeystoneAuth.get_barbican_client()
LOG.info(gettextutils._LI(
"Storing certificate container '{0}' in Barbican."
).format(name))
certificate_secret = None
private_key_secret = None
intermediates_secret = None
pkp_secret = None
try:
certificate_secret = connection.secrets.create(
payload=certificate,
expiration=expiration,
name="Certificate"
)
private_key_secret = connection.secrets.create(
payload=private_key,
expiration=expiration,
name="Private Key"
)
certificate_container = connection.containers.create_certificate(
name=name,
certificate=certificate_secret,
private_key=private_key_secret
)
if intermediates:
intermediates_secret = connection.secrets.create(
payload=intermediates,
expiration=expiration,
name="Intermediates"
)
certificate_container.intermediates = intermediates_secret
if private_key_passphrase:
pkp_secret = connection.secrets.create(
payload=private_key_passphrase,
expiration=expiration,
name="Private Key Passphrase"
)
certificate_container.private_key_passphrase = pkp_secret
certificate_container.store()
return certificate_container.container_ref
except Exception as e:
for i in [certificate_secret, private_key_secret,
intermediates_secret, pkp_secret]:
if i and i.secret_ref:
old_ref = i.secret_ref
try:
i.delete()
LOG.info(gettextutils._LI(
"Deleted secret {0} ({1}) during rollback."
).format(i.name, old_ref))
except Exception:
LOG.warning(gettextutils._LW(
"Failed to delete {0} ({1}) during rollback. This "
"might not be a problem."
).format(i.name, old_ref))
with excutils.save_and_reraise_exception():
LOG.error(gettextutils._LE(
"Error storing certificate data: {0}"
).format(str(e)))
@staticmethod
def get_cert(cert_ref, service_name='Octavia', resource_ref=None,
check_only=False, **kwargs):
"""Retrieves the specified cert and registers as a consumer.
:param cert_ref: the UUID of the cert to retrieve
:param service_name: Friendly name for the consuming service
:param resource_ref: Full HATEOAS reference to the consuming resource
:param check_only: Read Certificate data without registering
:return: octavia.certificates.common.Cert representation of the
certificate data
:raises Exception: if certificate retrieval fails
"""
connection = barbican_common.BarbicanKeystoneAuth.get_barbican_client()
LOG.info(gettextutils._LI(
"Loading certificate container {0} from Barbican."
).format(cert_ref))
try:
if check_only:
cert_container = connection.containers.get(
container_ref=cert_ref
)
else:
cert_container = connection.containers.register_consumer(
container_ref=cert_ref,
name=service_name,
url=resource_ref
)
return barbican_common.BarbicanCert(cert_container)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(gettextutils._LE(
"Error getting {0}: {1}"
).format(cert_ref, str(e)))
@staticmethod
def delete_cert(cert_ref, service_name='Octavia', resource_ref=None,
**kwargs):
"""Deregister as a consumer for the specified cert.
:param cert_ref: the UUID of the cert to retrieve
:param service_name: Friendly name for the consuming service
:param resource_ref: Full HATEOAS reference to the consuming resource
:raises Exception: if deregistration fails
"""
connection = barbican_common.BarbicanKeystoneAuth.get_barbican_client()
LOG.info(gettextutils._LI(
"Deregistering as a consumer of {0} in Barbican."
).format(cert_ref))
try:
connection.containers.remove_consumer(
container_ref=cert_ref,
name=service_name,
url=resource_ref
)
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(gettextutils._LE(
"Error deregistering as a consumer of {0}: {1}"
).format(cert_ref, str(e)))
@staticmethod
def _actually_delete_cert(cert_ref):
"""Deletes the specified cert. Very dangerous. Do not recommend.
:param cert_ref: the UUID of the cert to delete
:raises Exception: if certificate deletion fails
"""
connection = barbican_common.BarbicanKeystoneAuth.get_barbican_client()
LOG.info(gettextutils._LI(
"Recursively deleting certificate container {0} from Barbican."
).format(cert_ref))
try:
certificate_container = connection.containers.get(cert_ref)
certificate_container.certificate.delete()
if certificate_container.intermediates:
certificate_container.intermediates.delete()
if certificate_container.private_key_passphrase:
certificate_container.private_key_passphrase.delete()
certificate_container.private_key.delete()
certificate_container.delete()
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(gettextutils._LE(
"Error recursively deleting container {0}: {1}"
).format(cert_ref, str(e)))

View File

@ -41,9 +41,10 @@ class CertManager(object):
pass
@abc.abstractmethod
def get_cert(self, cert_ref, **kwargs):
def get_cert(self, cert_ref, check_only=False, **kwargs):
"""Retrieves the specified cert.
If check_only is True, don't perform any sort of registration.
If the specified cert does not exist, a CertificateStorageException
should be raised.
"""

View File

@ -0,0 +1,127 @@
# Copyright 2014 Rackspace
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from barbicanclient import client as barbican_client
from keystoneclient import session
import mock
import octavia.certificates.common.barbican as barbican_common
import octavia.tests.unit.base as base
class TestBarbicanAuth(base.TestCase):
def setUp(self):
# Reset the session and client
barbican_common.BarbicanKeystoneAuth._keystone_session = None
barbican_common.BarbicanKeystoneAuth._barbican_client = None
super(TestBarbicanAuth, self).setUp()
def test_get_keystone_client(self):
# There should be no existing session
self.assertIsNone(
barbican_common.BarbicanKeystoneAuth._keystone_session
)
# Get us a session
ks1 = barbican_common.BarbicanKeystoneAuth._get_keystone_session()
# Our returned session should also be the saved session
self.assertIsInstance(
barbican_common.BarbicanKeystoneAuth._keystone_session,
session.Session
)
self.assertIs(
barbican_common.BarbicanKeystoneAuth._keystone_session,
ks1
)
# Getting the session again should return the same object
ks2 = barbican_common.BarbicanKeystoneAuth._get_keystone_session()
self.assertIs(ks1, ks2)
def test_get_barbican_client(self):
# There should be no existing client
self.assertIsNone(
barbican_common.BarbicanKeystoneAuth._barbican_client
)
# Mock out the keystone session and get the client
barbican_common.BarbicanKeystoneAuth._keystone_session = (
mock.MagicMock()
)
bc1 = barbican_common.BarbicanKeystoneAuth.get_barbican_client()
# Our returned client should also be the saved client
self.assertIsInstance(
barbican_common.BarbicanKeystoneAuth._barbican_client,
barbican_client.Client
)
self.assertIs(
barbican_common.BarbicanKeystoneAuth._barbican_client,
bc1
)
# Getting the session again should return the same object
bc2 = barbican_common.BarbicanKeystoneAuth.get_barbican_client()
self.assertIs(bc1, bc2)
class TestBarbicanCert(base.TestCase):
def setUp(self):
# Certificate data
self.certificate = "My Certificate"
self.intermediates = "My Intermediates"
self.private_key = "My Private Key"
self.private_key_passphrase = "My Private Key Passphrase"
self.certificate_secret = barbican_client.secrets.Secret(
api=mock.MagicMock(),
payload=self.certificate
)
self.intermediates_secret = barbican_client.secrets.Secret(
api=mock.MagicMock(),
payload=self.intermediates
)
self.private_key_secret = barbican_client.secrets.Secret(
api=mock.MagicMock(),
payload=self.private_key
)
self.private_key_passphrase_secret = barbican_client.secrets.Secret(
api=mock.MagicMock(),
payload=self.private_key_passphrase
)
super(TestBarbicanCert, self).setUp()
def test_barbican_cert(self):
container = barbican_client.containers.CertificateContainer(
api=mock.MagicMock(),
certificate=self.certificate_secret,
intermediates=self.intermediates_secret,
private_key=self.private_key_secret,
private_key_passphrase=self.private_key_passphrase_secret
)
# Create a cert
cert = barbican_common.BarbicanCert(
cert_container=container
)
# Validate the cert functions
self.assertEqual(cert.get_certificate(), self.certificate)
self.assertEqual(cert.get_intermediates(), self.intermediates)
self.assertEqual(cert.get_private_key(), self.private_key)
self.assertEqual(cert.get_private_key_passphrase(),
self.private_key_passphrase)

View File

@ -0,0 +1,66 @@
# Copyright 2014 Rackspace
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import uuid
import mock
from OpenSSL import crypto
import octavia.certificates.generator.barbican as barbican_cert_gen
import octavia.tests.unit.base as base
class TestBarbicanGenerator(base.TestCase):
def setUp(self):
# Make a fake Order and contents
self.barbican_endpoint = 'http://localhost:9311/v1'
self.container_uuid = uuid.uuid4()
# TODO(rm_work): fill this section, right now it is placeholder data
self.order_uuid = uuid.uuid4()
self.order_ref = '{0}/orders/{1}'.format(
self.barbican_endpoint, self.container_uuid
)
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 1024)
req = crypto.X509Req()
req.set_pubkey(key)
self.certificate_signing_request = crypto.dump_certificate_request(
crypto.FILETYPE_PEM, req
)
order = mock.Mock()
self.order = order
super(TestBarbicanGenerator, self).setUp()
def test_sign_cert(self):
# TODO(rm_work): Update this test when Barbican supports this, right
# now this is all guesswork
self.skipTest("Barbican does not yet support signing.")
# Mock out the client
bc = mock.MagicMock()
bc.orders.create.return_value = self.order
barbican_cert_gen.BarbicanCertGenerator._barbican_client = bc
# Attempt to order a cert signing
barbican_cert_gen.BarbicanCertGenerator.sign_cert(
csr=self.certificate_signing_request
)
# create order should be called once
# should get back a valid order
bc.orders.create.assert_called_once_with()

View File

@ -0,0 +1,238 @@
# Copyright 2014 Rackspace
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import uuid
from barbicanclient import containers
from barbicanclient import secrets
import mock
import octavia.certificates.common.barbican as barbican_common
import octavia.certificates.common.cert as cert
import octavia.certificates.manager.barbican as barbican_cert_mgr
import octavia.tests.unit.base as base
class TestBarbicanManager(base.TestCase):
def setUp(self):
# Make a fake Container and contents
self.barbican_endpoint = 'http://localhost:9311/v1'
self.container_uuid = uuid.uuid4()
self.container_ref = '{0}/containers/{1}'.format(
self.barbican_endpoint, self.container_uuid
)
self.name = 'My Fancy Cert'
self.private_key = mock.Mock(spec=secrets.Secret)
self.certificate = mock.Mock(spec=secrets.Secret)
self.intermediates = mock.Mock(spec=secrets.Secret)
self.private_key_passphrase = mock.Mock(spec=secrets.Secret)
container = mock.Mock(spec=containers.CertificateContainer)
container.container_ref = self.container_ref
container.name = self.name
container.private_key = self.private_key
container.certificate = self.certificate
container.intermediates = self.intermediates
container.private_key_passphrase = self.private_key_passphrase
self.container = container
self.empty_container = mock.Mock(spec=containers.CertificateContainer)
self.secret1 = mock.Mock(spec=secrets.Secret)
self.secret2 = mock.Mock(spec=secrets.Secret)
self.secret3 = mock.Mock(spec=secrets.Secret)
self.secret4 = mock.Mock(spec=secrets.Secret)
super(TestBarbicanManager, self).setUp()
def test_store_cert(self):
# Mock out the client
bc = mock.MagicMock()
bc.containers.create_certificate.return_value = self.empty_container
barbican_common.BarbicanKeystoneAuth._barbican_client = bc
# Attempt to store a cert
barbican_cert_mgr.BarbicanCertManager.store_cert(
certificate=self.certificate,
private_key=self.private_key,
intermediates=self.intermediates,
private_key_passphrase=self.private_key_passphrase,
name=self.name
)
# create_secret should be called four times with our data
calls = [
mock.call(payload=self.certificate, expiration=None,
name=mock.ANY),
mock.call(payload=self.private_key, expiration=None,
name=mock.ANY),
mock.call(payload=self.intermediates, expiration=None,
name=mock.ANY),
mock.call(payload=self.private_key_passphrase, expiration=None,
name=mock.ANY)
]
bc.secrets.create.assert_has_calls(calls, any_order=True)
# create_certificate should be called once
self.assertEqual(bc.containers.create_certificate.call_count, 1)
# Container should be stored once
self.empty_container.store.assert_called_once_with()
def test_store_cert_failure(self):
# Mock out the client
bc = mock.MagicMock()
bc.containers.create_certificate.return_value = self.empty_container
test_secrets = [
self.secret1,
self.secret2,
self.secret3,
self.secret4
]
bc.secrets.create.side_effect = test_secrets
self.empty_container.store.side_effect = ValueError()
barbican_common.BarbicanKeystoneAuth._barbican_client = bc
# Attempt to store a cert
self.assertRaises(
ValueError,
barbican_cert_mgr.BarbicanCertManager.store_cert,
certificate=self.certificate,
private_key=self.private_key,
intermediates=self.intermediates,
private_key_passphrase=self.private_key_passphrase,
name=self.name
)
# create_secret should be called four times with our data
calls = [
mock.call(payload=self.certificate, expiration=None,
name=mock.ANY),
mock.call(payload=self.private_key, expiration=None,
name=mock.ANY),
mock.call(payload=self.intermediates, expiration=None,
name=mock.ANY),
mock.call(payload=self.private_key_passphrase, expiration=None,
name=mock.ANY)
]
bc.secrets.create.assert_has_calls(calls, any_order=True)
# create_certificate should be called once
self.assertEqual(bc.containers.create_certificate.call_count, 1)
# Container should be stored once
self.empty_container.store.assert_called_once_with()
# All secrets should be deleted (or at least an attempt made)
for s in test_secrets:
s.delete.assert_called_once_with()
def test_get_cert(self):
# Mock out the client
bc = mock.MagicMock()
bc.containers.register_consumer.return_value = self.container
barbican_common.BarbicanKeystoneAuth._barbican_client = bc
# Get the container data
data = barbican_cert_mgr.BarbicanCertManager.get_cert(
cert_ref=self.container_ref,
resource_ref=self.container_ref,
service_name='Octavia'
)
# 'register_consumer' should be called once with the container_ref
bc.containers.register_consumer.assert_called_once_with(
container_ref=self.container_ref,
url=self.container_ref,
name='Octavia'
)
# The returned data should be a Cert object with the correct values
self.assertIsInstance(data, cert.Cert)
self.assertEqual(data.get_private_key(),
self.private_key.payload)
self.assertEqual(data.get_certificate(),
self.certificate.payload)
self.assertEqual(data.get_intermediates(),
self.intermediates.payload)
self.assertEqual(data.get_private_key_passphrase(),
self.private_key_passphrase.payload)
def test_get_cert_no_registration(self):
# Mock out the client
bc = mock.MagicMock()
bc.containers.get.return_value = self.container
barbican_common.BarbicanKeystoneAuth._barbican_client = bc
# Get the container data
data = barbican_cert_mgr.BarbicanCertManager.get_cert(
cert_ref=self.container_ref, check_only=True
)
# 'get' should be called once with the container_ref
bc.containers.get.assert_called_once_with(
container_ref=self.container_ref
)
# The returned data should be a Cert object with the correct values
self.assertIsInstance(data, cert.Cert)
self.assertEqual(data.get_private_key(),
self.private_key.payload)
self.assertEqual(data.get_certificate(),
self.certificate.payload)
self.assertEqual(data.get_intermediates(),
self.intermediates.payload)
self.assertEqual(data.get_private_key_passphrase(),
self.private_key_passphrase.payload)
def test_delete_cert(self):
# Mock out the client
bc = mock.MagicMock()
barbican_common.BarbicanKeystoneAuth._barbican_client = bc
# Attempt to deregister as a consumer
barbican_cert_mgr.BarbicanCertManager.delete_cert(
cert_ref=self.container_ref,
resource_ref=self.container_ref,
service_name='Octavia'
)
# remove_consumer should be called once with the container_ref
bc.containers.remove_consumer.assert_called_once_with(
container_ref=self.container_ref,
url=self.container_ref,
name='Octavia'
)
def test_actually_delete_cert(self):
# Mock out the client
bc = mock.MagicMock()
bc.containers.get.return_value = self.container
barbican_common.BarbicanKeystoneAuth._barbican_client = bc
# Attempt to store a cert
barbican_cert_mgr.BarbicanCertManager._actually_delete_cert(
cert_ref=self.container_ref
)
# All secrets should be deleted
self.container.certificate.delete.assert_called_once_with()
self.container.private_key.delete.assert_called_once_with()
self.container.intermediates.delete.assert_called_once_with()
self.container.private_key_passphrase.delete.assert_called_once_with()
# Container should be deleted once
self.container.delete.assert_called_once_with()

View File

@ -27,6 +27,7 @@ oslo.config>=1.4.0.0a3
oslo.db>=0.4.0 # Apache-2.0
oslo.messaging>=1.4.0.0a3
oslo.rootwrap>=1.3.0.0a1
python-barbicanclient>=3.0
python-keystoneclient>=0.11.1
python-novaclient>=2.17.0
posix_ipc