Charm with unit tests

Working charm with unit tests. Various features:

Change the install ownership for token store

It turns out that the 'user' that creates the token store, is the only one that
can access it, apart from root.  As Barbican uses a 'barbican' user for the
barbican-worker process, we need to create the token store with 1777 perms (see
https://github.com/opendnssec/SoftHSMv2/issues/185) and also create the initial
token using the barbican user.

Add an initial README.md: This describes the charm, where to get help
and how to use it.
This commit is contained in:
Alex Kavanagh
2016-07-01 17:22:13 +00:00
parent cbe7232aac
commit 45e370b142
11 changed files with 923 additions and 38 deletions

43
unit_tests/__init__.py Normal file
View File

@@ -0,0 +1,43 @@
# Copyright 2016 Canonical Ltd
#
# 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 sys
import mock
sys.path.append('src')
sys.path.append('src/lib')
# Mock out charmhelpers so that we can test without it.
# also stops sideeffects from occuring.
charmhelpers = mock.MagicMock()
sys.modules['charmhelpers'] = charmhelpers
sys.modules['charmhelpers.core'] = charmhelpers.core
sys.modules['charmhelpers.core.hookenv'] = charmhelpers.core.hookenv
sys.modules['charmhelpers.core.host'] = charmhelpers.core.host
sys.modules['charmhelpers.core.unitdata'] = charmhelpers.core.unitdata
sys.modules['charmhelpers.core.templating'] = charmhelpers.core.templating
sys.modules['charmhelpers.contrib'] = charmhelpers.contrib
sys.modules['charmhelpers.contrib.openstack'] = charmhelpers.contrib.openstack
sys.modules['charmhelpers.contrib.openstack.utils'] = (
charmhelpers.contrib.openstack.utils)
sys.modules['charmhelpers.contrib.openstack.templating'] = (
charmhelpers.contrib.openstack.templating)
sys.modules['charmhelpers.contrib.network'] = charmhelpers.contrib.network
sys.modules['charmhelpers.contrib.network.ip'] = (
charmhelpers.contrib.network.ip)
sys.modules['charmhelpers.fetch'] = charmhelpers.fetch
sys.modules['charmhelpers.cli'] = charmhelpers.cli
sys.modules['charmhelpers.contrib.hahelpers'] = charmhelpers.contrib.hahelpers
sys.modules['charmhelpers.contrib.hahelpers.cluster'] = (
charmhelpers.contrib.hahelpers.cluster)

132
unit_tests/test_handlers.py Normal file
View File

@@ -0,0 +1,132 @@
# Copyright 2016 Canonical Ltd
#
# 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 __future__ import absolute_import
from __future__ import print_function
import unittest
import mock
import reactive.handlers as handlers
_when_args = {}
_when_not_args = {}
def mock_hook_factory(d):
def mock_hook(*args, **kwargs):
def inner(f):
# remember what we were passed. Note that we can't actually
# determine the class we're attached to, as the decorator only gets
# the function.
try:
d[f.__name__].append(dict(args=args, kwargs=kwargs))
except KeyError:
d[f.__name__] = [dict(args=args, kwargs=kwargs)]
return f
return inner
return mock_hook
class TestBarbicanHandlers(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._patched_when = mock.patch('charms.reactive.when',
mock_hook_factory(_when_args))
cls._patched_when_started = cls._patched_when.start()
cls._patched_when_not = mock.patch('charms.reactive.when_not',
mock_hook_factory(_when_not_args))
cls._patched_when_not_started = cls._patched_when_not.start()
# force requires to rerun the mock_hook decorator:
# try except is Python2/Python3 compatibility as Python3 has moved
# reload to importlib.
try:
reload(handlers)
except NameError:
import importlib
importlib.reload(handlers)
@classmethod
def tearDownClass(cls):
cls._patched_when.stop()
cls._patched_when_started = None
cls._patched_when = None
cls._patched_when_not.stop()
cls._patched_when_not_started = None
cls._patched_when_not = None
# and fix any breakage we did to the module
try:
reload(handlers)
except NameError:
import importlib
importlib.reload(handlers)
def setUp(self):
self._patches = {}
self._patches_start = {}
def tearDown(self):
for k, v in self._patches.items():
v.stop()
setattr(self, k, None)
self._patches = None
self._patches_start = None
def patch(self, obj, attr, return_value=None):
mocked = mock.patch.object(obj, attr)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
self._patches_start[attr] = started
setattr(self, attr, started)
def test_registered_hooks(self):
# test that the hooks actually registered the relation expressions that
# are meaningful for this interface: this is to handle regressions.
# The keys are the function names that the hook attaches to.
when_patterns = {
'hsm_connected': ('hsm.connected', ),
}
when_not_patterns = {
'install_packages': ('charm.installed', ),
}
# check the when hooks are attached to the expected functions
for t, p in [(_when_args, when_patterns),
(_when_not_args, when_not_patterns)]:
for f, args in t.items():
# check that function is in patterns
# print("f: {}, args: {}".format(f, args))
self.assertTrue(f in p.keys())
# check that the lists are equal
l = [a['args'][0] for a in args]
self.assertEqual(l, sorted(p[f]))
def test_install_packages(self):
self.patch(handlers.softhsm_plugin, 'install')
self.patch(handlers.reactive, 'set_state')
handlers.install_packages()
self.install.assert_called_once_with()
self.set_state.assert_called_once_with('charm.installed')
def test_hsm_connected(self):
self.patch(handlers.softhsm_plugin, 'on_hsm_connected')
self.patch(handlers.reactive, 'set_state')
handlers.hsm_connected('hsm-thing')
self.on_hsm_connected.assert_called_once_with('hsm-thing')
self.set_state.assert_called_once_with('hsm.available')

View File

@@ -0,0 +1,227 @@
# Copyright 2016 Canonical Ltd
#
# 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 __future__ import absolute_import
from __future__ import print_function
import textwrap
import unittest
import mock
import charm.openstack.softhsm_plugin as softhsm_plugin
class Helper(unittest.TestCase):
def setUp(self):
self._patches = {}
self._patches_start = {}
# patch out the select_release to always return 'mitaka'
# self.patch(softhsm_plugin.unitdata, 'kv')
# _getter = mock.MagicMock()
# _getter.get.return_value = softhsm_plugin.BarbicanSoftHSMCharm.release
# self.kv.return_value = _getter
def tearDown(self):
for k, v in self._patches.items():
v.stop()
setattr(self, k, None)
self._patches = None
self._patches_start = None
def patch(self, obj, attr, return_value=None, **kwargs):
mocked = mock.patch.object(obj, attr, **kwargs)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
self._patches_start[attr] = started
setattr(self, attr, started)
class TestSoftHSM(Helper):
def test_install(self):
self.patch(softhsm_plugin.BarbicanSoftHSMCharm.singleton, 'install')
softhsm_plugin.install()
self.install.assert_called_once_with()
def test_on_hsm_connected(self):
self.patch(softhsm_plugin.BarbicanSoftHSMCharm.singleton,
'on_hsm_connected')
softhsm_plugin.on_hsm_connected('hsm-thing')
self.on_hsm_connected.assert_called_once_with('hsm-thing')
def test_read_pins_from_store(self):
# test with no file (patch open so that it raises an error)
mock_open = mock.MagicMock(return_value=mock.sentinel.file_handle)
with mock.patch('builtins.open', mock_open):
def raise_exception():
raise Exception("Supposed to break")
mock_open.side_effect = raise_exception
pin, so_pin = softhsm_plugin.read_pins_from_store()
self.assertEqual(pin, None)
self.assertEqual(so_pin, None)
# now provide the pin and so pin as a json object
d = '{"pin": "1234", "so_pin": "5678"}'
with mock.patch('builtins.open',
mock.mock_open(read_data=d),
create=True):
pin, so_pin = softhsm_plugin.read_pins_from_store()
self.assertEqual(pin, '1234')
self.assertEqual(so_pin, '5678')
def test_write_pins_to_store(self):
f = mock.MagicMock()
self.patch(softhsm_plugin.os, 'fdopen', return_value=f)
self.patch(softhsm_plugin.os, 'open', return_value='opener')
self.patch(softhsm_plugin.json, 'dump')
softhsm_plugin.write_pins_to_store('1234', '5678')
self.open.assert_called_once_with(
softhsm_plugin.STORED_PINS_FILE,
softhsm_plugin.os.O_WRONLY | softhsm_plugin.os.O_CREAT,
0o600)
self.fdopen.assert_called_once_with('opener', 'w')
self.dump.assert_called_once_with(
{'pin': '1234', 'so_pin': '5678'}, f.__enter__())
def test_read_slot_id(self):
result = textwrap.dedent("""
Slot 5
Slot info:
Description: SoftHSM slot 0
Manufacturer ID: SoftHSM project
Hardware version: 2.0
Firmware version: 2.0
Token present: yes
Token info:
Manufacturer ID: SoftHSM project
Model: SoftHSM v2
Hardware version: 2.0
Firmware version: 2.0
Serial number: 02ae3171143498e7
Initialized: yes
User PIN init.: yes
Label: barbican_token
""")
self.patch(softhsm_plugin.subprocess, 'check_output',
return_value=result.encode())
self.assertEqual(softhsm_plugin.read_slot_id('barbican_token'), '5')
self.check_output.assert_called_once_with(
[softhsm_plugin.SOFTHSM2_UTIL_CMD, '--show-slots'])
self.assertEqual(softhsm_plugin.read_slot_id('not_found'), None)
class TestBarbicanSoftHSMCharm(Helper):
def test_install(self):
self.patch(softhsm_plugin.charms_openstack.charm.OpenStackCharm,
'install')
self.patch(softhsm_plugin.ch_core_host, 'add_user_to_group')
c = softhsm_plugin.BarbicanSoftHSMCharm()
self.patch(c, 'setup_token_store')
self.patch(softhsm_plugin.hookenv, 'status_set')
c.install()
self.install.assert_called_once_with()
self.add_user_to_group.assert_called_once_with('barbican', 'softhsm')
self.setup_token_store.assert_called_once_with()
self.status_set.assert_called_once_with(
'waiting', 'Charm installed and token store configured')
def test_setup_token_store(self):
self.patch(softhsm_plugin, 'read_pins_from_store')
self.patch(softhsm_plugin.os.path, 'exists')
self.patch(softhsm_plugin.os.path, 'isdir')
self.patch(softhsm_plugin.shutil, 'rmtree')
self.patch(softhsm_plugin.os, 'remove')
self.patch(softhsm_plugin.os, 'makedirs')
self.patch(softhsm_plugin.os, 'chmod')
self.patch(softhsm_plugin.ch_core_host, 'pwgen')
self.patch(softhsm_plugin, 'write_pins_to_store')
self.patch(softhsm_plugin.subprocess, 'check_call')
self.patch(softhsm_plugin.hookenv, 'log')
# first, pretend that the token store is already setup.
self.read_pins_from_store.return_value = ('1234', '5678', )
c = softhsm_plugin.BarbicanSoftHSMCharm()
c.setup_token_store()
self.assertEqual(self.log.call_count, 0)
# now pretend the token store isn't set up
self.read_pins_from_store.return_value = None, None
# assume that the token store exists and is a dir first:
self.exists.return_value = True
self.isdir.return_value = True
# return two values, for each of the two pwgen calls.
self.pwgen.side_effect = ['abcd', 'efgh']
c.setup_token_store()
# now validate it did everything we expected.
self.exists.assert_called_once_with(softhsm_plugin.TOKEN_STORE)
self.isdir.assert_called_once_with(softhsm_plugin.TOKEN_STORE)
self.rmtree.assert_called_once_with(softhsm_plugin.TOKEN_STORE)
self.makedirs.assert_called_once_with(softhsm_plugin.TOKEN_STORE)
self.chmod.assert_called_once_with(softhsm_plugin.TOKEN_STORE, 0o1777)
self.assertEqual(self.pwgen.call_count, 2)
self.write_pins_to_store.assert_called_once_with('abcd', 'efgh')
self.check_call.called_once_with([
'sudo', '-u', 'barbican',
softhsm_plugin.SOFTHSM2_UTIL_CMD,
'--init-token', '--free',
'--label', softhsm_plugin.BARBICAN_TOKEN_LABEL,
'--pin', 'abcd',
'--so-pin', 'efgh'])
self.log.assert_called_once_with("Initialised token store.")
def test_on_hsm_connected(self):
hsm = mock.MagicMock()
self.patch(softhsm_plugin, 'read_pins_from_store')
self.patch(softhsm_plugin, 'read_slot_id')
self.patch(softhsm_plugin.hookenv, 'status_set')
self.patch(softhsm_plugin.hookenv, 'log')
c = softhsm_plugin.BarbicanSoftHSMCharm()
self.patch(c, 'setup_token_store')
# simulate not being able to set up the token store
self.read_pins_from_store.return_value = None, None
with self.assertRaises(RuntimeError):
c.on_hsm_connected(hsm)
self.status_set.assert_called_once_with(
'error', "Couldn't set up the token store?")
self.setup_token_store.assert_called_once_with()
self.log.assert_called_once_with(
"Setting plugin name to softhsm2",
level=softhsm_plugin.hookenv.DEBUG)
# now assume that the pins can be read, but no slot is set up.
self.read_pins_from_store.return_value = '1234', '5678'
self.read_slot_id.return_value = None
with self.assertRaises(RuntimeError):
c.on_hsm_connected(hsm)
# now assume that the slot is also set up.
self.read_slot_id.return_value = '10'
c.on_hsm_connected(hsm)
hsm.set_plugin_data.assert_called_once_with({
"library_path": softhsm_plugin.SOFTHSM2_LIB_PATH,
"login": '1234',
"slot_id": '10'
})
# finally test corner case where token store isn't set up already
self.read_pins_from_store.side_effect = [
[None, None], ['abcd', 'efgh']]
hsm.reset_mock()
self.setup_token_store.reset_mock()
c.on_hsm_connected(hsm)
self.setup_token_store.assert_called_once_with()
hsm.set_plugin_data.assert_called_once_with({
"library_path": softhsm_plugin.SOFTHSM2_LIB_PATH,
"login": 'abcd',
"slot_id": '10'
})