Add certificates operations

This commit adds two operations to the magnum client:

magnum ca-show --bay bay_uuid
magnum ca-sign --bay bay_uuid --csr /path/to/csr.pem

ca-show retrieves the CA certificate for the provided bay.

ca-sign, sends the provided CSR to Magnum, and prints the signed
certificate returned by Magnum. The certificate is signed with the
CA for the given Bay.

Change-Id: I784a1b3dc77e72dfb9e7f8d25cbbc37a0b5ffce0
Partial-Implements: blueprint magnum-as-a-ca
This commit is contained in:
Andrew Melton 2015-09-16 14:06:01 -07:00
parent 38b3eb8409
commit fd794c18f9
5 changed files with 308 additions and 0 deletions

View File

@ -0,0 +1,90 @@
# Copyright 2015 IBM Corp.
#
# 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 copy
import testtools
from magnumclient import exceptions
from magnumclient.tests import utils
from magnumclient.v1 import certificates
CERT1 = {
'bay_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a53',
'pem': 'fake-pem'
}
CERT2 = {
'bay_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a53',
'pem': 'fake-pem',
'csr': 'fake-csr',
}
CREATE_CERT = {'bay_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a53',
'csr': 'fake-csr'}
UPDATED_POD = copy.deepcopy(CERT1)
NEW_DESCR = 'new-description'
UPDATED_POD['description'] = NEW_DESCR
fake_responses = {
'/v1/certificates':
{
'POST': (
{},
CERT2,
)
},
'/v1/certificates/%s' % CERT1['bay_uuid']:
{
'GET': (
{},
CERT1
)
}
}
class CertificateManagerTest(testtools.TestCase):
def setUp(self):
super(CertificateManagerTest, self).setUp()
self.api = utils.FakeAPI(fake_responses)
self.mgr = certificates.CertificateManager(self.api)
def test_cert_show_by_id(self):
cert = self.mgr.get(CERT1['bay_uuid'])
expect = [
('GET', '/v1/certificates/%s' % CERT1['bay_uuid'], {}, None)
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(CERT1['bay_uuid'], cert.bay_uuid)
self.assertEqual(CERT1['pem'], cert.pem)
def test_cert_create(self):
cert = self.mgr.create(**CREATE_CERT)
expect = [
('POST', '/v1/certificates', {}, CREATE_CERT),
]
self.assertEqual(expect, self.api.calls)
self.assertEqual(CERT2['bay_uuid'], cert.bay_uuid)
self.assertEqual(CERT2['pem'], cert.pem)
self.assertEqual(CERT2['csr'], cert.csr)
def test_pod_create_fail(self):
create_cert_fail = copy.deepcopy(CREATE_CERT)
create_cert_fail["wrong_key"] = "wrong"
self.assertRaisesRegexp(exceptions.InvalidAttribute,
("Key must be in %s" %
','.join(certificates.CREATION_ATTRIBUTES)),
self.mgr.create, **create_cert_fail)
self.assertEqual([], self.api.calls)

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
import mock import mock
from mock import mock_open
from magnumclient.tests import base from magnumclient.tests import base
from magnumclient.v1 import shell from magnumclient.v1 import shell
@ -157,6 +158,119 @@ class ShellTest(base.TestCase):
shell.do_bay_update(client_mock, args) shell.do_bay_update(client_mock, args)
client_mock.bays.update.assert_called_once_with(bay_id, patch) client_mock.bays.update.assert_called_once_with(bay_id, patch)
@mock.patch('os.path.isfile')
def test_do_ca_show(self, mock_isfile):
mock_isfile.return_value = True
client_mock = mock.MagicMock()
bay = mock.MagicMock()
bay.uuid = 'uuid'
bay.status = 'CREATE_COMPLETE'
client_mock.bays.get.return_value = bay
args = mock.MagicMock()
bay_id_or_name = "xxx"
args.bay = bay_id_or_name
shell.do_ca_show(client_mock, args)
client_mock.certificates.get.assert_called_once_with(
bay_uuid=bay.uuid)
@mock.patch('os.path.isfile')
def test_do_ca_show_wrong_status(self, mock_isfile):
mock_isfile.return_value = True
client_mock = mock.MagicMock()
bay = mock.MagicMock()
bay.uuid = 'uuid'
bay.status = 'XXX'
client_mock.bays.get.return_value = bay
args = mock.MagicMock()
bay_id_or_name = "xxx"
args.bay = bay_id_or_name
shell.do_ca_show(client_mock, args)
self.assertFalse(client_mock.certificates.get.called)
@mock.patch('os.path.isfile')
def test_do_ca_sign(self, mock_isfile):
mock_isfile.return_value = True
client_mock = mock.MagicMock()
bay = mock.MagicMock()
bay.uuid = 'uuid'
bay.status = 'CREATE_COMPLETE'
client_mock.bays.get.return_value = bay
args = mock.MagicMock()
bay_id_or_name = "xxx"
args.bay = bay_id_or_name
csr = "test_csr"
args.csr = csr
fake_csr = 'fake-csr'
mock_o = mock_open(read_data=fake_csr)
with mock.patch.object(shell, 'open', mock_o):
shell.do_ca_sign(client_mock, args)
mock_isfile.assert_called_once_with(csr)
mock_o.assert_called_once_with(csr, 'r')
client_mock.certificates.create.assert_called_once_with(
csr=fake_csr, bay_uuid=bay.uuid)
@mock.patch('os.path.isfile')
def test_do_ca_sign_wrong_status(self, mock_isfile):
mock_isfile.return_value = True
client_mock = mock.MagicMock()
bay = mock.MagicMock()
bay.uuid = 'uuid'
bay.status = 'XXX'
client_mock.bays.get.return_value = bay
args = mock.MagicMock()
bay_id_or_name = "xxx"
args.bay = bay_id_or_name
csr = "test_csr"
args.csr = csr
fake_csr = 'fake-csr'
mock_o = mock_open(read_data=fake_csr)
with mock.patch.object(shell, 'open', mock_o):
shell.do_ca_sign(client_mock, args)
self.assertFalse(mock_isfile.called)
self.assertFalse(mock_o.called)
self.assertFalse(client_mock.certificates.create.called)
@mock.patch('os.path.isfile')
def test_do_ca_sign_not_file(self, mock_isfile):
mock_isfile.return_value = False
client_mock = mock.MagicMock()
bay = mock.MagicMock()
bay.uuid = 'uuid'
bay.status = 'CREATE_COMPLETE'
client_mock.bays.get.return_value = bay
args = mock.MagicMock()
bay_id_or_name = "xxx"
args.bay = bay_id_or_name
csr = "test_csr"
args.csr = csr
fake_csr = 'fake-csr'
mock_o = mock_open(read_data=fake_csr)
with mock.patch.object(shell, 'open', mock_o):
shell.do_ca_sign(client_mock, args)
mock_isfile.assert_called_once_with(csr)
self.assertFalse(mock_o.called)
self.assertFalse(client_mock.certificates.create.called)
def test_do_baymodel_create(self): def test_do_baymodel_create(self):
client_mock = mock.MagicMock() client_mock = mock.MagicMock()
args = mock.MagicMock() args = mock.MagicMock()

View File

@ -0,0 +1,48 @@
# Copyright 2015 Rackspace, 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.
from magnumclient.common import base
from magnumclient import exceptions
CREATION_ATTRIBUTES = ['bay_uuid', 'csr']
class Certificate(base.Resource):
def __repr__(self):
return "<Certificate %s>" % self._info
class CertificateManager(base.Manager):
resource_class = Certificate
@staticmethod
def _path(id=None):
return '/v1/certificates/%s' % id if id else '/v1/certificates'
def get(self, bay_uuid):
try:
return self._list(self._path(bay_uuid))[0]
except IndexError:
return None
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exceptions.InvalidAttribute(
"Key must be in %s" % ",".join(CREATION_ATTRIBUTES))
return self._create(self._path(), new)

View File

@ -19,6 +19,7 @@ from keystoneclient.v3 import client as keystone_client_v3
from magnumclient.common import httpclient from magnumclient.common import httpclient
from magnumclient.v1 import baymodels from magnumclient.v1 import baymodels
from magnumclient.v1 import bays from magnumclient.v1 import bays
from magnumclient.v1 import certificates
from magnumclient.v1 import containers from magnumclient.v1 import containers
from magnumclient.v1 import nodes from magnumclient.v1 import nodes
from magnumclient.v1 import pods from magnumclient.v1 import pods
@ -72,6 +73,7 @@ class Client(object):
} }
self.http_client = httpclient.HTTPClient(magnum_url, **http_cli_kwargs) self.http_client = httpclient.HTTPClient(magnum_url, **http_cli_kwargs)
self.bays = bays.BayManager(self.http_client) self.bays = bays.BayManager(self.http_client)
self.certificates = certificates.CertificateManager(self.http_client)
self.baymodels = baymodels.BayModelManager(self.http_client) self.baymodels = baymodels.BayModelManager(self.http_client)
self.containers = containers.ContainerManager(self.http_client) self.containers = containers.ContainerManager(self.http_client)
self.nodes = nodes.NodeManager(self.http_client) self.nodes = nodes.NodeManager(self.http_client)

View File

@ -33,6 +33,10 @@ def _show_bay(bay):
utils.print_dict(bay._info) utils.print_dict(bay._info)
def _show_cert(certificate):
print(certificate.pem)
def _show_baymodel(baymodel): def _show_baymodel(baymodel):
del baymodel._info['links'] del baymodel._info['links']
utils.print_dict(baymodel._info) utils.print_dict(baymodel._info)
@ -249,6 +253,56 @@ def do_baymodel_list(cs, args):
{'versions': _print_list_field('versions')}) {'versions': _print_list_field('versions')})
@utils.arg('--bay',
required=True,
metavar='<bay>',
help='ID or name of the bay.')
def do_ca_show(cs, args):
bay = cs.bays.get(args.bay)
if bay.status not in ['CREATE_COMPLETE', 'UPDATE_COMPLETE']:
print('Bay status for %s is: %s. We can not create a %s there'
' until the status is CREATE_COMPLETE or UPDATE_COMPLETE.' %
(bay.uuid, bay.status, 'certificate'))
return
opts = {
'bay_uuid': bay.uuid
}
cert = cs.certificates.get(**opts)
_show_cert(cert)
@utils.arg('--csr',
metavar='<csr>',
help='File path of the csr file to send to Magnum to get signed.')
@utils.arg('--bay',
required=True,
metavar='<bay>',
help='ID or name of the bay.')
def do_ca_sign(cs, args):
bay = cs.bays.get(args.bay)
if bay.status not in ['CREATE_COMPLETE', 'UPDATE_COMPLETE']:
print('Bay status for %s is: %s. We can not create a %s there'
' until the status is CREATE_COMPLETE or UPDATE_COMPLETE.' %
(bay.uuid, bay.status, 'certificate'))
return
opts = {
'bay_uuid': bay.uuid
}
if args.csr is None or not os.path.isfile(args.csr):
print('A CSR must be provided.')
return
with open(args.csr, 'r') as f:
opts['csr'] = f.read()
cert = cs.certificates.create(**opts)
_show_cert(cert)
def do_node_list(cs, args): def do_node_list(cs, args):
"""Print a list of configured nodes.""" """Print a list of configured nodes."""
nodes = cs.nodes.list() nodes = cs.nodes.list()