diff --git a/magnumclient/tests/v1/test_certificates.py b/magnumclient/tests/v1/test_certificates.py new file mode 100644 index 00000000..79df241f --- /dev/null +++ b/magnumclient/tests/v1/test_certificates.py @@ -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) diff --git a/magnumclient/tests/v1/test_shell.py b/magnumclient/tests/v1/test_shell.py index c8f802d3..008f3255 100644 --- a/magnumclient/tests/v1/test_shell.py +++ b/magnumclient/tests/v1/test_shell.py @@ -13,6 +13,7 @@ # under the License. import mock +from mock import mock_open from magnumclient.tests import base from magnumclient.v1 import shell @@ -157,6 +158,119 @@ class ShellTest(base.TestCase): shell.do_bay_update(client_mock, args) 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): client_mock = mock.MagicMock() args = mock.MagicMock() diff --git a/magnumclient/v1/certificates.py b/magnumclient/v1/certificates.py new file mode 100644 index 00000000..cd9eafbb --- /dev/null +++ b/magnumclient/v1/certificates.py @@ -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 "" % 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) diff --git a/magnumclient/v1/client.py b/magnumclient/v1/client.py index 672865ce..fe50f277 100644 --- a/magnumclient/v1/client.py +++ b/magnumclient/v1/client.py @@ -19,6 +19,7 @@ from keystoneclient.v3 import client as keystone_client_v3 from magnumclient.common import httpclient from magnumclient.v1 import baymodels from magnumclient.v1 import bays +from magnumclient.v1 import certificates from magnumclient.v1 import containers from magnumclient.v1 import nodes from magnumclient.v1 import pods @@ -72,6 +73,7 @@ class Client(object): } self.http_client = httpclient.HTTPClient(magnum_url, **http_cli_kwargs) self.bays = bays.BayManager(self.http_client) + self.certificates = certificates.CertificateManager(self.http_client) self.baymodels = baymodels.BayModelManager(self.http_client) self.containers = containers.ContainerManager(self.http_client) self.nodes = nodes.NodeManager(self.http_client) diff --git a/magnumclient/v1/shell.py b/magnumclient/v1/shell.py index 6c775bc8..59ad7455 100644 --- a/magnumclient/v1/shell.py +++ b/magnumclient/v1/shell.py @@ -33,6 +33,10 @@ def _show_bay(bay): utils.print_dict(bay._info) +def _show_cert(certificate): + print(certificate.pem) + + def _show_baymodel(baymodel): del baymodel._info['links'] utils.print_dict(baymodel._info) @@ -249,6 +253,56 @@ def do_baymodel_list(cs, args): {'versions': _print_list_field('versions')}) +@utils.arg('--bay', + required=True, + metavar='', + 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='', + help='File path of the csr file to send to Magnum to get signed.') +@utils.arg('--bay', + required=True, + metavar='', + 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): """Print a list of configured nodes.""" nodes = cs.nodes.list()