Merge "Make keystone_pki less keystone specific"

This commit is contained in:
Jenkins 2014-10-03 08:04:42 +00:00 committed by Gerrit Code Review
commit 2acfb45d4a
6 changed files with 229 additions and 75 deletions

View File

@ -15,7 +15,7 @@ import argparse
import textwrap import textwrap
from os_cloud_config.cmd.utils import environment from os_cloud_config.cmd.utils import environment
from os_cloud_config import keystone_pki from os_cloud_config import ssl_pki
def parse_args(): def parse_args():
@ -60,6 +60,6 @@ def main():
environment._configure_logging(args) environment._configure_logging(args)
if args.heatenv: if args.heatenv:
keystone_pki.generate_certs_into_json(args.heatenv, args.seed) ssl_pki.generate_cert_into_json(args.heatenv, "keystone")
else: else:
keystone_pki.create_and_write_ca_and_signing_pairs(args.directory) ssl_pki.create_and_write_ca_and_signing_pairs(args.directory)

View File

@ -0,0 +1,49 @@
# 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 argparse
import textwrap
from os_cloud_config import ssl_pki
def parse_args():
description = textwrap.dedent("""
Generate and sign certificate with CA
This script generates a certificate and signes certificate using the CA
in the heat environment. If no CA is in the heat environment then a new
CA will be generated. The resulting certificate and CA (if one is made)
are added to the heat environment.
""")
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
'heat_env',
metavar='<heat_env>',
help='path to JSON heat environment file'
)
parser.add_argument(
'name',
metavar='<name>',
help='name for key/certificate pair',
)
return parser.parse_args()
def main():
args = parse_args()
ssl_pki.generate_cert_into_json(args.heat_env, args.name, args.overwrite)

View File

@ -23,14 +23,14 @@ from os_cloud_config.tests import base
class GenerateKeystonePKITest(base.TestCase): class GenerateKeystonePKITest(base.TestCase):
@mock.patch('os_cloud_config.keystone_pki.generate_certs_into_json') @mock.patch('os_cloud_config.ssl_pki.generate_cert_into_json')
@mock.patch.object(sys, 'argv', ['generate-keystone-pki', '-j', @mock.patch.object(sys, 'argv', ['generate-keystone-pki', '-j',
'foo.json', '-s']) 'foo.json', '-s'])
def test_with_heatenv(self, generate_mock): def test_with_heatenv(self, generate_mock):
generate_keystone_pki.main() generate_keystone_pki.main()
generate_mock.assert_called_once_with('foo.json', True) generate_mock.assert_called_once_with('foo.json', 'keystone')
@mock.patch('os_cloud_config.keystone_pki.create_and_write_ca_' @mock.patch('os_cloud_config.ssl_pki.create_and_write_ca_'
'and_signing_pairs') 'and_signing_pairs')
@mock.patch.object(sys, 'argv', ['generate-keystone-pki', '-d', 'bar']) @mock.patch.object(sys, 'argv', ['generate-keystone-pki', '-d', 'bar'])
def test_without_heatenv(self, create_mock): def test_without_heatenv(self, create_mock):

View File

@ -54,7 +54,7 @@ def create_ca_pair(cert_serial=1):
subject.ST = 'Unset' subject.ST = 'Unset'
subject.L = 'Unset' subject.L = 'Unset'
subject.O = 'Unset' subject.O = 'Unset'
subject.CN = 'Keystone CA' subject.CN = 'os-cloud-config CA'
ca_cert.gmtime_adj_notBefore(0) ca_cert.gmtime_adj_notBefore(0)
ca_cert.gmtime_adj_notAfter(60 * 60 * 24 * CA_CERT_DAYS) ca_cert.gmtime_adj_notAfter(60 * 60 * 24 * CA_CERT_DAYS)
ca_cert.set_issuer(subject) ca_cert.set_issuer(subject)
@ -103,7 +103,7 @@ def create_signing_pair(ca_key_pem, ca_cert_pem, cert_serial=2):
subject.ST = 'Unset' subject.ST = 'Unset'
subject.L = 'Unset' subject.L = 'Unset'
subject.O = 'Unset' subject.O = 'Unset'
subject.CN = 'Keystone Signing' subject.CN = 'os-cloud-config Signing'
signing_cert.gmtime_adj_notBefore(0) signing_cert.gmtime_adj_notBefore(0)
signing_cert.gmtime_adj_notAfter(60 * 60 * 24 * SIGNING_CERT_DAYS) signing_cert.gmtime_adj_notAfter(60 * 60 * 24 * SIGNING_CERT_DAYS)
signing_cert.set_issuer(ca_cert.get_subject()) signing_cert.set_issuer(ca_cert.get_subject())
@ -137,18 +137,54 @@ def create_and_write_ca_and_signing_pairs(directory):
_write_pki_file(path.join(directory, 'signing_cert.pem'), signing_cert_pem) _write_pki_file(path.join(directory, 'signing_cert.pem'), signing_cert_pem)
def generate_certs_into_json(jsonfile, seed): def generate_cert_into_json(jsonfile, name, auto_gen_ca=True,
"""Create and write out CA certificate and signing certificate/key. overwrite=False):
"""Create and write out an SSL certificate.
Generate CA certificate, signing certificate and signing key and Create an ssl certificate, sign it with a CA, and output the certificate
add them into a JSON file. If key/certs already exist in JSON file, no to <jsonfile> which is a heat JSON environment. If the parameters
change is done. property exists in destination, then the following properties are added:
{
"parameters": {
"<name>SslCertificate": PEM_DATA,
"<name>SslCertificateKey": PEM_DATA
}
}
:param jsonfile: JSON file where certs and key will be written The CA certificate and key lives in the parameters "CaSslCertificate" and
:type jsonfile: string "CaSslCertificateKey". If these parameters are not defined then a new CA
:param seed: JSON file for seed machine has different structure. Different is created and these properties are added.
key/certs names and different parent node are used
:type seed: boolean If no "parameters" property exists then the following properties are
added:
{
"<name>": {
"ssl" : {
"certificate": PEM DATA,
"certificate_key: PEM_DATA
}
}
}
The CA certificate and key in this case are:
{
"ssl": {
"ca_certificate": PEM_DATA,
"ca_certificate_key": PEM_DATA
}
}
:param jsonfile: Destination to write certificate and possible CA to.
:type jsonfile: string
:param cert_serial: Serial for the certificate. If None then this is
automatically determined.
:type cert_serial: integer
:param name: name for the certificate.
:type name: string
:param overwrite: overwrite certificate if it already exists
:type overwrite: boolean
""" """
if os.path.isfile(jsonfile): if os.path.isfile(jsonfile):
with open(jsonfile) as json_fd: with open(jsonfile) as json_fd:
@ -156,30 +192,60 @@ def generate_certs_into_json(jsonfile, seed):
else: else:
all_data = {} all_data = {}
if seed: parent = all_data.get("parameters")
parent = 'keystone' ca_parent = None
ca_cert_name = 'ca_certificate' if parent is not None:
signing_key_name = 'signing_key' cert_dest = "%sSslCertificate" % name
signing_cert_name = 'signing_certificate' cert_key_dest = "%sSslCertificateKey" % name
ca_dest = "CaSslCertificate"
ca_key_dest = "CaSslCertificateKey"
cert_count_dest = "SslCertificatCount"
ca_parent = parent
else: else:
parent = 'parameters' cert_dest = "certificate"
ca_cert_name = 'KeystoneCACertificate' cert_key_dest = "certificate_key"
signing_key_name = 'KeystoneSigningKey' ca_dest = "ca_certificate"
signing_cert_name = 'KeystoneSigningCertificate' ca_key_dest = "ca_certificate_key"
cert_count_dest = "certificate_count"
if parent not in all_data: # Make parent be a node in all_data
all_data[parent] = {} svc = all_data.get(name, {})
parent_node = all_data[parent] parent = all_data.get("ssl", {})
svc["ssl"] = parent
ca_parent = all_data.get("ssl", {})
if not (ca_cert_name in parent_node and all_data[name] = svc
signing_key_name in parent_node and all_data["ssl"] = ca_parent
signing_cert_name in parent_node):
ca_key_pem, ca_cert_pem = create_ca_pair() # If we only have one of cert or key this is an error if not overwriting
if (cert_dest not in parent and cert_key_dest in parent or
cert_dest in parent and cert_key_dest not in parent) and \
not overwrite:
raise ValueError("Only one of certificate or key defined.")
if cert_dest not in parent or overwrite:
# Check that we have both a CA cert and key or neither
if (ca_dest not in parent and ca_key_dest in parent) or \
(ca_dest in parent and ca_key_dest not in parent):
raise ValueError("Only one of CA certificate or key defined.")
# Gen CA
if ca_dest not in parent:
ca_key_pem, ca_cert_pem = create_ca_pair()
ca_parent[ca_key_dest] = ca_key_pem
ca_parent[ca_dest] = ca_cert_pem
# Gen cert
cert_serial = ca_parent.get(cert_count_dest, 0)
cert_serial += 1
signing_key_pem, signing_cert_pem = create_signing_pair(ca_key_pem, signing_key_pem, signing_cert_pem = create_signing_pair(ca_key_pem,
ca_cert_pem) ca_cert_pem,
parent_node.update({ca_cert_name: ca_cert_pem, cert_serial)
signing_key_name: signing_key_pem, ca_parent[cert_count_dest] = cert_serial
signing_cert_name: signing_cert_pem}) parent[cert_key_dest] = signing_key_pem
parent[cert_dest] = signing_cert_pem
# Write out env
with open(jsonfile, 'w') as json_fd: with open(jsonfile, 'w') as json_fd:
json.dump(all_data, json_fd, sort_keys=True) json.dump(all_data, json_fd, sort_keys=True)
LOG.debug("Wrote key/certs into '%s'.", path.abspath(jsonfile)) LOG.debug("Wrote key/certs into '%s'.", path.abspath(jsonfile))

View File

@ -17,18 +17,18 @@ import stat
import mock import mock
from OpenSSL import crypto from OpenSSL import crypto
from os_cloud_config import keystone_pki from os_cloud_config import ssl_pki
from os_cloud_config.tests import base from os_cloud_config.tests import base
class KeystonePKITest(base.TestCase): class SslPKITest(base.TestCase):
def test_create_ca_and_signing_pairs(self): def test_create_ca_and_signing_pairs(self):
# use one common test to avoid generating CA pair twice # use one common test to avoid generating CA pair twice
# do not mock out pyOpenSSL, test generated keys/certs # do not mock out pyOpenSSL, test generated keys/certs
# create CA pair # create CA pair
ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair() ca_key_pem, ca_cert_pem = ssl_pki.create_ca_pair()
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_pem) ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_pem)
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert_pem) ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert_pem)
@ -38,11 +38,11 @@ class KeystonePKITest(base.TestCase):
# check CA cert properties # check CA cert properties
self.assertFalse(ca_cert.has_expired()) self.assertFalse(ca_cert.has_expired())
self.assertEqual('Keystone CA', ca_cert.get_issuer().CN) self.assertEqual('os-cloud-config CA', ca_cert.get_issuer().CN)
self.assertEqual('Keystone CA', ca_cert.get_subject().CN) self.assertEqual('os-cloud-config CA', ca_cert.get_subject().CN)
# create signing pair # create signing pair
signing_key_pem, signing_cert_pem = keystone_pki.create_signing_pair( signing_key_pem, signing_cert_pem = ssl_pki.create_signing_pair(
ca_key_pem, ca_cert_pem) ca_key_pem, ca_cert_pem)
signing_key = crypto.load_privatekey(crypto.FILETYPE_PEM, signing_key = crypto.load_privatekey(crypto.FILETYPE_PEM,
signing_key_pem) signing_key_pem)
@ -55,22 +55,23 @@ class KeystonePKITest(base.TestCase):
# check signing cert properties # check signing cert properties
self.assertFalse(signing_cert.has_expired()) self.assertFalse(signing_cert.has_expired())
self.assertEqual('Keystone CA', signing_cert.get_issuer().CN) self.assertEqual('os-cloud-config CA', signing_cert.get_issuer().CN)
self.assertEqual('Keystone Signing', signing_cert.get_subject().CN) self.assertEqual('os-cloud-config Signing',
signing_cert.get_subject().CN)
# pyOpenSSL currenty cannot verify a cert against a CA cert # pyOpenSSL currenty cannot verify a cert against a CA cert
@mock.patch('os_cloud_config.keystone_pki.os.chmod', create=True) @mock.patch('os_cloud_config.ssl_pki.os.chmod', create=True)
@mock.patch('os_cloud_config.keystone_pki.os.mkdir', create=True) @mock.patch('os_cloud_config.ssl_pki.os.mkdir', create=True)
@mock.patch('os_cloud_config.keystone_pki.path.isdir', create=True) @mock.patch('os_cloud_config.ssl_pki.path.isdir', create=True)
@mock.patch('os_cloud_config.keystone_pki.create_ca_pair') @mock.patch('os_cloud_config.ssl_pki.create_ca_pair')
@mock.patch('os_cloud_config.keystone_pki.create_signing_pair') @mock.patch('os_cloud_config.ssl_pki.create_signing_pair')
@mock.patch('os_cloud_config.keystone_pki.open', create=True) @mock.patch('os_cloud_config.ssl_pki.open', create=True)
def test_create_and_write_ca_and_signing_pairs( def test_create_and_write_ca_and_signing_pairs(
self, open_, create_signing, create_ca, isdir, mkdir, chmod): self, open_, create_signing, create_ca, isdir, mkdir, chmod):
create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') create_ca.return_value = ('mock_ca_key', 'mock_ca_cert')
create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') create_signing.return_value = ('mock_signing_key', 'mock_signing_cert')
isdir.return_value = False isdir.return_value = False
keystone_pki.create_and_write_ca_and_signing_pairs('/fake_dir') ssl_pki.create_and_write_ca_and_signing_pairs('/fake_dir')
mkdir.assert_called_with('/fake_dir') mkdir.assert_called_with('/fake_dir')
chmod.assert_has_calls([ chmod.assert_has_calls([
@ -99,31 +100,66 @@ class KeystonePKITest(base.TestCase):
mock.call('mock_signing_cert'), mock.call('mock_signing_cert'),
]) ])
@mock.patch('os_cloud_config.keystone_pki.path.isfile', create=True) @mock.patch('os_cloud_config.ssl_pki.path.isfile', create=True)
@mock.patch('os_cloud_config.keystone_pki.create_ca_pair') @mock.patch('os_cloud_config.ssl_pki.create_ca_pair')
@mock.patch('os_cloud_config.keystone_pki.create_signing_pair') @mock.patch('os_cloud_config.ssl_pki.create_signing_pair')
@mock.patch('os_cloud_config.keystone_pki.open', create=True) @mock.patch('os_cloud_config.ssl_pki.open', create=True)
@mock.patch('os_cloud_config.keystone_pki.json.dump') @mock.patch('os_cloud_config.ssl_pki.json.load')
@mock.patch('os_cloud_config.ssl_pki.json.dump')
def test_generate_certs_into_json( def test_generate_certs_into_json(
self, mock_json, open_, create_signing, create_ca, isfile): self, mock_json_dump, mock_json_load, open_, create_signing,
create_ca, isfile):
create_ca.return_value = ('mock_ca_key', 'mock_ca_cert') create_ca.return_value = ('mock_ca_key', 'mock_ca_cert')
create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') create_signing.return_value = ('mock_signing_key', 'mock_signing_cert')
isfile.return_value = False isfile.return_value = True
mock_json_load.return_value = {
"Keystone": {
"mock_property": "mock_value"
}
}
keystone_pki.generate_certs_into_json('/jsonfile', False) ssl_pki.generate_cert_into_json('/jsonfile', "Keystone")
params = mock_json.call_args[0][0]['parameters'] ssl = mock_json_dump.call_args[0][0]["ssl"]
self.assertEqual(params['KeystoneCACertificate'], 'mock_ca_cert') keystone = mock_json_dump.call_args[0][0]["Keystone"]
self.assertEqual(params['KeystoneSigningKey'], 'mock_signing_key') self.assertEqual(keystone["mock_property"], "mock_value")
self.assertEqual(params['KeystoneSigningCertificate'], self.assertEqual(keystone["ssl"]["certificate"], "mock_signing_cert")
self.assertEqual(keystone["ssl"]["certificate_key"],
"mock_signing_key")
self.assertEqual(ssl["ca_certificate"], "mock_ca_cert")
self.assertEqual(ssl["ca_certificate_key"], "mock_ca_key")
@mock.patch('os_cloud_config.ssl_pki.path.isfile', create=True)
@mock.patch('os_cloud_config.ssl_pki.create_ca_pair')
@mock.patch('os_cloud_config.ssl_pki.create_signing_pair')
@mock.patch('os_cloud_config.ssl_pki.open', create=True)
@mock.patch('os_cloud_config.ssl_pki.json.load')
@mock.patch('os_cloud_config.ssl_pki.json.dump')
def test_generate_cert_into_json_notseed(
self, mock_json_dump, mock_json_load, open_, create_signing,
create_ca, isfile):
create_ca.return_value = ('mock_ca_key', 'mock_ca_cert')
create_signing.return_value = ('mock_signing_key', 'mock_signing_cert')
isfile.return_value = True
mock_json_load.return_value = {
"parameters": {}
}
ssl_pki.generate_cert_into_json('/jsonfile', "Keystone")
params = mock_json_dump.call_args[0][0]['parameters']
self.assertEqual(params['CaSslCertificate'], 'mock_ca_cert')
self.assertEqual(params['KeystoneSslCertificateKey'],
'mock_signing_key')
self.assertEqual(params['KeystoneSslCertificate'],
'mock_signing_cert') 'mock_signing_cert')
@mock.patch('os_cloud_config.keystone_pki.path.isfile', create=True) @mock.patch('os_cloud_config.ssl_pki.path.isfile', create=True)
@mock.patch('os_cloud_config.keystone_pki.create_ca_pair') @mock.patch('os_cloud_config.ssl_pki.create_ca_pair')
@mock.patch('os_cloud_config.keystone_pki.create_signing_pair') @mock.patch('os_cloud_config.ssl_pki.create_signing_pair')
@mock.patch('os_cloud_config.keystone_pki.open', create=True) @mock.patch('os_cloud_config.ssl_pki.open', create=True)
@mock.patch('os_cloud_config.keystone_pki.json.load') @mock.patch('os_cloud_config.ssl_pki.json.load')
@mock.patch('os_cloud_config.keystone_pki.json.dump') @mock.patch('os_cloud_config.ssl_pki.json.dump')
def test_generate_certs_into_json_with_existing_certs( def test_generate_certs_into_json_with_existing_certs(
self, mock_json_dump, mock_json_load, open_, create_signing, self, mock_json_dump, mock_json_load, open_, create_signing,
create_ca, isfile): create_ca, isfile):
@ -131,10 +167,12 @@ class KeystonePKITest(base.TestCase):
create_signing.return_value = ('mock_signing_key', 'mock_signing_cert') create_signing.return_value = ('mock_signing_key', 'mock_signing_cert')
isfile.return_value = True isfile.return_value = True
mock_json_load.return_value = { mock_json_load.return_value = {
'KeystoneCACertificate': 'mock_ca_cert', "parameters": {
'KeystoneSigningKey': 'mock_signing_key', 'KeystoneCACertificate': 'mock_ca_cert',
'KeystoneSigningCertificate': 'mock_signing_cert' 'KeystoneSigningKey': 'mock_signing_key',
'KeystoneSigningCertificate': 'mock_signing_cert'
}
} }
keystone_pki.generate_certs_into_json('/jsonfile', False) ssl_pki.generate_cert_into_json('/jsonfile', "keystone")
mock_json_dump.assert_not_called() mock_json_dump.assert_not_called()

View File

@ -26,6 +26,7 @@ packages =
[entry_points] [entry_points]
console_scripts = console_scripts =
generate-keystone-pki = os_cloud_config.cmd.generate_keystone_pki:main generate-keystone-pki = os_cloud_config.cmd.generate_keystone_pki:main
generate-ssl-cert = os_cloud_config.cmd.generate_ssl_cert:main
init-keystone = os_cloud_config.cmd.init_keystone:main init-keystone = os_cloud_config.cmd.init_keystone:main
register-nodes = os_cloud_config.cmd.register_nodes:main register-nodes = os_cloud_config.cmd.register_nodes:main
setup-endpoints = os_cloud_config.cmd.setup_endpoints:main setup-endpoints = os_cloud_config.cmd.setup_endpoints:main