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:
43
unit_tests/__init__.py
Normal file
43
unit_tests/__init__.py
Normal 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
132
unit_tests/test_handlers.py
Normal 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')
|
||||
227
unit_tests/test_lib_barbican_softhsm.py
Normal file
227
unit_tests/test_lib_barbican_softhsm.py
Normal 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'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user