diff --git a/diskimage_builder/block_device/blockdevice.py b/diskimage_builder/block_device/blockdevice.py index 5dd0c996d..c112d671b 100644 --- a/diskimage_builder/block_device/blockdevice.py +++ b/diskimage_builder/block_device/blockdevice.py @@ -16,18 +16,87 @@ import codecs import json import logging import os +import pprint import shutil import sys import yaml from diskimage_builder.block_device.config import config_tree_to_graph from diskimage_builder.block_device.config import create_graph +from diskimage_builder.block_device.exception import \ + BlockDeviceSetupException from diskimage_builder.block_device.utils import exec_sudo logger = logging.getLogger(__name__) +def _load_json(file_name): + """Load file from .json file on disk, return None if not existing""" + if os.path.exists(file_name): + with codecs.open(file_name, encoding="utf-8", mode="r") as fd: + return json.load(fd) + return None + + +class BlockDeviceState(object): + """The global state singleton + + An reference to an instance of this object is passed between nodes + as a global repository. It contains a single dictionary "state" + and a range of helper functions. + + This is used in two contexts: + + - The state is built by the :func:`NodeBase.create` commands as + called during :func:`BlockDevice.cmd_create`. It is then + persisted to disk by :func:`save_state` + + - Later calls (cleanup, umount, etc) load the state dictionary + from disk and are thus passed the full state. + """ + # XXX: + # - might it make sense to make state implement MutableMapping, + # so that callers treat it as a dictionary? + # - we could implement getters/setters such that if loaded from + # disk, the state is read-only? or make it append-only + # (i.e. you can't overwrite existing keys) + def __init__(self, filename=None): + """Initialise state + + :param filename: if :param:`filename` is passed and exists, it + will be loaded as the state. If it does not exist an + exception is raised. If :param:`filename` is not + passed, state will be initalised to a blank dictionary. + """ + if filename: + if not os.path.exists(filename): + raise BlockDeviceSetupException("State dump not found") + else: + self.state = _load_json(filename) + assert self.state is not None + else: + self.state = {} + + def save_state(self, filename): + """Persist the state to disk + + :param filename: The file to persist state to + """ + logger.debug("Writing state to: %s", filename) + self.debug_dump() + with open(filename, "w") as fd: + json.dump(self.state, fd) + + def debug_dump(self): + """Log state to debug""" + # This is pretty good for human consumption, but maybe a bit + # verbose. + nice_output = pprint.pformat(self.state, width=40) + for line in nice_output.split('\n'): + logger.debug(" " + line) + + class BlockDevice(object): """Handles block devices. @@ -116,13 +185,6 @@ class BlockDevice(object): else: v['label'] = "cloudimg-rootfs" - @staticmethod - def _load_json(file_name): - if os.path.exists(file_name): - with codecs.open(file_name, encoding="utf-8", mode="r") as fd: - return json.load(fd) - return None - def __init__(self, params): """Create BlockDevice object @@ -142,9 +204,7 @@ class BlockDevice(object): self.config_json_file_name \ = os.path.join(self.state_dir, "config.json") - self.config = self._load_json(self.config_json_file_name) - self.state = self._load_json(self.state_json_file_name) - logger.debug("Using state [%s]", self.state) + self.config = _load_json(self.config_json_file_name) # This needs to exists for the state and config files try: @@ -152,16 +212,6 @@ class BlockDevice(object): except OSError: pass - def write_state(self, state): - logger.debug("Write state [%s]", self.state_json_file_name) - with open(self.state_json_file_name, "w") as fd: - json.dump(state, fd) - - def create(self, result, rollback): - dg, call_order = create_graph(self.config, self.params) - for node in call_order: - node.create(result, rollback) - def cmd_init(self): """Initialize block device setup @@ -234,16 +284,23 @@ class BlockDevice(object): # mountpoints list print("%s" % "|".join(mount_points)) return 0 + + # the following symbols all come from the global state + # dictionary. They can only be accessed after the state has + # been dumped; i.e. after cmd_create() called. + state = BlockDeviceState(self.state_json_file_name) + state = state.state + if symbol == 'image-block-partition': # If there is no partition needed, pass back directly the # image. - if 'root' in self.state['blockdev']: - print("%s" % self.state['blockdev']['root']['device']) + if 'root' in state['blockdev']: + print("%s" % state['blockdev']['root']['device']) else: - print("%s" % self.state['blockdev']['image0']['device']) + print("%s" % state['blockdev']['image0']['device']) return 0 if symbol == 'image-path': - print("%s" % self.state['blockdev']['image0']['image']) + print("%s" % state['blockdev']['image0']['image']) return 0 logger.error("Invalid symbol [%s] for getval", symbol) @@ -253,28 +310,33 @@ class BlockDevice(object): """Creates the fstab""" logger.info("Creating fstab") + # State should have been created by prior calls; we only need + # the dict + state = BlockDeviceState(self.state_json_file_name) + state = state.state + tmp_fstab = os.path.join(self.state_dir, "fstab") with open(tmp_fstab, "wt") as fstab_fd: # This gives the order in which this must be mounted - for mp in self.state['mount_order']: + for mp in state['mount_order']: logger.debug("Writing fstab entry for [%s]", mp) - fs_base = self.state['mount'][mp]['base'] - fs_name = self.state['mount'][mp]['name'] - fs_val = self.state['filesys'][fs_base] + fs_base = state['mount'][mp]['base'] + fs_name = state['mount'][mp]['name'] + fs_val = state['filesys'][fs_base] if 'label' in fs_val: diskid = "LABEL=%s" % fs_val['label'] else: diskid = "UUID=%s" % fs_val['uuid'] # If there is no fstab entry - do not write anything - if 'fstab' not in self.state: + if 'fstab' not in state: continue - if fs_name not in self.state['fstab']: + if fs_name not in state['fstab']: continue - options = self.state['fstab'][fs_name]['options'] - dump_freq = self.state['fstab'][fs_name]['dump-freq'] - fsck_passno = self.state['fstab'][fs_name]['fsck-passno'] + options = state['fstab'][fs_name]['options'] + dump_freq = state['fstab'][fs_name]['dump-freq'] + fsck_passno = state['fstab'][fs_name]['fsck-passno'] fstab_fd.write("%s %s %s %s %s %s\n" % (diskid, mp, fs_val['fstype'], @@ -292,25 +354,34 @@ class BlockDevice(object): logger.info("create() called") logger.debug("Using config [%s]", self.config) - self.state = {} rollback = [] - + # Create a new, empty state + state = BlockDeviceState() try: - self.create(self.state, rollback) + dg, call_order = create_graph(self.config, self.params) + for node in call_order: + node.create(state.state, rollback) except Exception: logger.exception("Create failed; rollback initiated") for rollback_cb in reversed(rollback): rollback_cb() sys.exit(1) - self.write_state(self.state) + state.save_state(self.state_json_file_name) logger.info("create() finished") return 0 def cmd_umount(self): """Unmounts the blockdevice and cleanup resources""" - if self.state is None: + + # State should have been created by prior calls; we only need + # the dict. If it is not here, it has been cleaned up already + # (? more details?) + try: + state = BlockDeviceState(self.state_json_file_name) + state = state.state + except BlockDeviceSetupException: logger.info("State already cleaned - no way to do anything here") return 0 @@ -321,36 +392,44 @@ class BlockDevice(object): if dg is None: return 0 for node in reverse_order: - node.umount(self.state) + node.umount(state) return 0 def cmd_cleanup(self): """Cleanup all remaining relicts - in good case""" + # State should have been created by prior calls; we only need + # the dict + state = BlockDeviceState(self.state_json_file_name) + state = state.state # Deleting must be done in reverse order dg, call_order = create_graph(self.config, self.params) reverse_order = reversed(call_order) for node in reverse_order: - node.cleanup(self.state) + node.cleanup(state) - logger.info("Removing temporary dir [%s]", self.state_dir) + logger.info("Removing temporary state dir [%s]", self.state_dir) shutil.rmtree(self.state_dir) return 0 def cmd_delete(self): """Cleanup all remaining relicts - in case of an error""" + # State should have been created by prior calls; we only need + # the dict + state = BlockDeviceState(self.state_json_file_name) + state = state.state # Deleting must be done in reverse order dg, call_order = create_graph(self.config, self.params) reverse_order = reversed(call_order) for node in reverse_order: - node.delete(self.state) + node.delete(state) - logger.info("Removing temporary dir [%s]", self.state_dir) + logger.info("Removing temporary state dir [%s]", self.state_dir) shutil.rmtree(self.state_dir) return 0 diff --git a/diskimage_builder/block_device/level0/localloop.py b/diskimage_builder/block_device/level0/localloop.py index 6edda78fa..bfdd71060 100644 --- a/diskimage_builder/block_device/level0/localloop.py +++ b/diskimage_builder/block_device/level0/localloop.py @@ -100,7 +100,7 @@ class LocalLoopNode(NodeBase): logger.debug("Gave up trying to detach [%s]", loopdev) return rval - def create(self, result, rollback): + def create(self, state, rollback): logger.debug("[%s] Creating loop on [%s] with size [%d]", self.name, self.filename, self.size) @@ -110,11 +110,11 @@ class LocalLoopNode(NodeBase): block_device = self._loopdev_attach(self.filename) rollback.append(lambda: self._loopdev_detach(block_device)) - if 'blockdev' not in result: - result['blockdev'] = {} + if 'blockdev' not in state: + state['blockdev'] = {} - result['blockdev'][self.name] = {"device": block_device, - "image": self.filename} + state['blockdev'][self.name] = {"device": block_device, + "image": self.filename} logger.debug("Created loop name [%s] device [%s] image [%s]", self.name, block_device, self.filename) return diff --git a/diskimage_builder/block_device/level1/partition.py b/diskimage_builder/block_device/level1/partition.py index b77b730de..c8d386a91 100644 --- a/diskimage_builder/block_device/level1/partition.py +++ b/diskimage_builder/block_device/level1/partition.py @@ -65,5 +65,5 @@ class PartitionNode(NodeBase): edge_from.append(self.prev_partition.name) return (edge_from, edge_to) - def create(self, result, rollback): - self.partitioning.create(result, rollback) + def create(self, state, rollback): + self.partitioning.create(state, rollback) diff --git a/diskimage_builder/block_device/level1/partitioning.py b/diskimage_builder/block_device/level1/partitioning.py index 522255930..b4fa9592f 100644 --- a/diskimage_builder/block_device/level1/partitioning.py +++ b/diskimage_builder/block_device/level1/partitioning.py @@ -127,14 +127,13 @@ class Partitioning(PluginBase): exec_sudo(["kpartx", "-avs", device_path]) - def create(self, result, rollback): + def create(self, state, rollback): # not this is NOT a node and this is not called directly! The # create() calls in the partition nodes this plugin has # created are calling back into this. - image_path = result['blockdev'][self.base]['image'] - device_path = result['blockdev'][self.base]['device'] - logger.info("Creating partition on [%s] [%s]", - self.base, image_path) + image_path = state['blockdev'][self.base]['image'] + device_path = state['blockdev'][self.base]['device'] + logger.info("Creating partition on [%s] [%s]", self.base, image_path) # This is a bit of a hack. Each of the partitions is actually # in the graph, so for every partition we get a create() call @@ -167,7 +166,7 @@ class Partitioning(PluginBase): logger.debug("Create partition [%s] [%d]", part_name, part_no) partition_device_name = device_path + "p%d" % part_no - result['blockdev'][part_name] \ + state['blockdev'][part_name] \ = {'device': partition_device_name} partition_devices.add(partition_device_name) diff --git a/diskimage_builder/block_device/level2/mkfs.py b/diskimage_builder/block_device/level2/mkfs.py index 99c08b590..854cb46e3 100644 --- a/diskimage_builder/block_device/level2/mkfs.py +++ b/diskimage_builder/block_device/level2/mkfs.py @@ -102,9 +102,7 @@ class FilesystemNode(NodeBase): edge_to = [] return (edge_from, edge_to) - def create(self, result, rollback): - logger.info("create called; result [%s]", result) - + def create(self, state, rollback): cmd = ["mkfs"] cmd.extend(['-t', self.type]) @@ -123,17 +121,17 @@ class FilesystemNode(NodeBase): if self.type in ('ext2', 'ext3', 'ext4', 'xfs'): cmd.append('-q') - if 'blockdev' not in result: - result['blockdev'] = {} - device = result['blockdev'][self.base]['device'] + if 'blockdev' not in state: + state['blockdev'] = {} + device = state['blockdev'][self.base]['device'] cmd.append(device) logger.debug("Creating fs command [%s]", cmd) exec_sudo(cmd) - if 'filesys' not in result: - result['filesys'] = {} - result['filesys'][self.name] \ + if 'filesys' not in state: + state['filesys'] = {} + state['filesys'][self.name] \ = {'uuid': self.uuid, 'label': self.label, 'fstype': self.type, 'opts': self.opts, 'device': device} diff --git a/diskimage_builder/block_device/level3/mount.py b/diskimage_builder/block_device/level3/mount.py index ed940f8d7..3df34e1d6 100644 --- a/diskimage_builder/block_device/level3/mount.py +++ b/diskimage_builder/block_device/level3/mount.py @@ -91,9 +91,8 @@ class MountPointNode(NodeBase): edge_from.append(self.base) return (edge_from, edge_to) - def create(self, result, rollback): + def create(self, state, rollback): logger.debug("mount called [%s]", self.mount_point) - logger.debug("result [%s]", result) rel_mp = self.mount_point if self.mount_point[0] != '/' \ else self.mount_point[1:] mount_point = os.path.join(self.mount_base, rel_mp) @@ -102,17 +101,17 @@ class MountPointNode(NodeBase): # file system tree. exec_sudo(['mkdir', '-p', mount_point]) logger.info("Mounting [%s] to [%s]", self.name, mount_point) - exec_sudo(["mount", result['filesys'][self.base]['device'], + exec_sudo(["mount", state['filesys'][self.base]['device'], mount_point]) - if 'mount' not in result: - result['mount'] = {} - result['mount'][self.mount_point] \ + if 'mount' not in state: + state['mount'] = {} + state['mount'][self.mount_point] \ = {'name': self.name, 'base': self.base, 'path': mount_point} - if 'mount_order' not in result: - result['mount_order'] = [] - result['mount_order'].append(self.mount_point) + if 'mount_order' not in state: + state['mount_order'] = [] + state['mount_order'].append(self.mount_point) def umount(self, state): logger.info("Called for [%s]", self.name) diff --git a/diskimage_builder/block_device/level4/fstab.py b/diskimage_builder/block_device/level4/fstab.py index 75bd586d8..8c5ca267c 100644 --- a/diskimage_builder/block_device/level4/fstab.py +++ b/diskimage_builder/block_device/level4/fstab.py @@ -34,14 +34,13 @@ class FstabNode(NodeBase): edge_to = [] return (edge_from, edge_to) - def create(self, result, rollback): + def create(self, state, rollback): logger.debug("fstab create called [%s]", self.name) - logger.debug("result [%s]", result) - if 'fstab' not in result: - result['fstab'] = {} + if 'fstab' not in state: + state['fstab'] = {} - result['fstab'][self.base] = { + state['fstab'][self.base] = { 'name': self.name, 'base': self.base, 'options': self.options, diff --git a/diskimage_builder/block_device/plugin.py b/diskimage_builder/block_device/plugin.py index 7a0ffa52a..145b18876 100644 --- a/diskimage_builder/block_device/plugin.py +++ b/diskimage_builder/block_device/plugin.py @@ -74,7 +74,7 @@ class NodeBase(object): return @abc.abstractmethod - def create(self, results, rollback): + def create(self, state, rollback): """Main creation driver This is the main driver function. After the graph is @@ -82,10 +82,11 @@ class NodeBase(object): Arguments: - :param results: A shared dictionary of prior results. This + :param state: A shared dictionary of prior results. This dictionary is passed by reference to each call, meaning any entries inserted will be available to subsequent :func:`create` - calls of following nodes. + calls of following nodes. The ``state`` dictionary will be + saved and available to other calls. :param rollback: A shared list of functions to be called in the failure case. Nodes should only append to this list. @@ -105,7 +106,7 @@ class NodeBase(object): Actions to taken when ``dib-block-device umount`` is called :param state: the current state dictionary. This is the - `results` dictionary from :func:`create` before this call is + `state` dictionary from :func:`create` before this call is made. :return: None """ @@ -119,7 +120,7 @@ class NodeBase(object): called in the reverse order to :func:`create` :param state: the current state dictionary. This is the - `results` dictionary from :func:`create` before this call is + `state` dictionary from :func:`create` before this call is made. :return: None """ @@ -134,7 +135,7 @@ class NodeBase(object): :func:`create` :param state: the current state dictionary. This is the - `results` dictionary from :func:`create` before this call is + `state` dictionary from :func:`create` before this call is made. :return: None """ diff --git a/diskimage_builder/block_device/tests/config/cmd_create.yaml b/diskimage_builder/block_device/tests/config/cmd_create.yaml new file mode 100644 index 000000000..6f6feeed4 --- /dev/null +++ b/diskimage_builder/block_device/tests/config/cmd_create.yaml @@ -0,0 +1,6 @@ +- test_a: + name: test_node_a + +- test_b: + name: test_node_b + base: test_node_a \ No newline at end of file diff --git a/diskimage_builder/block_device/tests/plugin/__init__.py b/diskimage_builder/block_device/tests/plugin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/diskimage_builder/block_device/tests/plugin/test_a.py b/diskimage_builder/block_device/tests/plugin/test_a.py new file mode 100644 index 000000000..cb8ba523c --- /dev/null +++ b/diskimage_builder/block_device/tests/plugin/test_a.py @@ -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. + +# plugin test case + +import logging + +from diskimage_builder.block_device.plugin import NodeBase +from diskimage_builder.block_device.plugin import PluginBase + + +logger = logging.getLogger(__name__) + + +class TestANode(NodeBase): + def __init__(self, name): + logger.debug("Create test 1") + super(TestANode, self).__init__(name) + + def get_edges(self): + # this is like the loop node; it's a root and doesn't have a + # base + return ([], []) + + def create(self, state, rollback): + # put some fake entries into state + state['test_a'] = {} + state['test_a']['value'] = 'foo' + state['test_a']['value2'] = 'bar' + return + + +class TestA(PluginBase): + + def __init__(self, config, defaults): + super(PluginBase, self).__init__() + self.node = TestANode(config['name']) + + def get_nodes(self): + return [self.node] diff --git a/diskimage_builder/block_device/tests/plugin/test_b.py b/diskimage_builder/block_device/tests/plugin/test_b.py new file mode 100644 index 000000000..aeed742e6 --- /dev/null +++ b/diskimage_builder/block_device/tests/plugin/test_b.py @@ -0,0 +1,47 @@ +# 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. + +# plugin test case + +import logging + +from diskimage_builder.block_device.plugin import NodeBase +from diskimage_builder.block_device.plugin import PluginBase + + +logger = logging.getLogger(__name__) + + +class TestBNode(NodeBase): + def __init__(self, name, base): + logger.debug("Create test 1") + super(TestBNode, self).__init__(name) + self.base = base + + def get_edges(self): + return ([self.base], []) + + def create(self, state, rollback): + state['test_b'] = {} + state['test_b']['value'] = 'baz' + return + + +class TestB(PluginBase): + + def __init__(self, config, defaults): + super(PluginBase, self).__init__() + self.node = TestBNode(config['name'], + config['base']) + + def get_nodes(self): + return [self.node] diff --git a/diskimage_builder/block_device/tests/test_base.py b/diskimage_builder/block_device/tests/test_base.py new file mode 100644 index 000000000..1875ba2f1 --- /dev/null +++ b/diskimage_builder/block_device/tests/test_base.py @@ -0,0 +1,41 @@ +# 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 fixtures +import logging +import os +import testtools +import yaml + + +logger = logging.getLogger(__name__) + + +class TestBase(testtools.TestCase): + """Base for all test cases""" + def setUp(self): + super(TestBase, self).setUp() + + fs = '%(asctime)s %(levelname)s [%(name)s] %(message)s' + self.log_fixture = self.useFixture( + fixtures.FakeLogger(level=logging.DEBUG, format=fs)) + + def get_config_file(self, f): + """Get the full path to sample config file f """ + logger.debug(os.path.dirname(__file__)) + return os.path.join(os.path.dirname(__file__), 'config', f) + + def load_config_file(self, f): + """Load f and return it after yaml parsing""" + path = self.get_config_file(f) + with open(path, 'r') as config: + return yaml.safe_load(config) diff --git a/diskimage_builder/block_device/tests/test_config.py b/diskimage_builder/block_device/tests/test_config.py index c2bd0ba98..87e64eeef 100644 --- a/diskimage_builder/block_device/tests/test_config.py +++ b/diskimage_builder/block_device/tests/test_config.py @@ -10,30 +10,23 @@ # License for the specific language governing permissions and limitations # under the License. -import fixtures import logging -import os -import testtools -import yaml from diskimage_builder.block_device.config import config_tree_to_graph from diskimage_builder.block_device.config import create_graph from diskimage_builder.block_device.exception import \ BlockDeviceSetupException +from diskimage_builder.block_device.tests.test_base import TestBase logger = logging.getLogger(__name__) -class TestConfig(testtools.TestCase): +class TestConfig(TestBase): """Helper for setting up and reading a config""" def setUp(self): super(TestConfig, self).setUp() - fs = '%(asctime)s %(levelname)s [%(name)s] %(message)s' - self.log_fixture = self.useFixture( - fixtures.FakeLogger(level=logging.DEBUG, format=fs)) - # reset all globals for each test. # XXX: remove globals :/ import diskimage_builder.block_device.level2.mkfs @@ -42,12 +35,6 @@ class TestConfig(testtools.TestCase): diskimage_builder.block_device.level3.mount.mount_points = {} diskimage_builder.block_device.level3.mount.sorted_mount_points = None - def load_config_file(self, f): - path = os.path.join(os.path.dirname(__file__), - 'config', f) - with open(path, 'r') as config: - return yaml.safe_load(config) - class TestGraphGeneration(TestConfig): """Extra helper class for testing graph generation""" diff --git a/diskimage_builder/block_device/tests/test_mount_order.py b/diskimage_builder/block_device/tests/test_mount_order.py index 863153689..9bcb438c7 100644 --- a/diskimage_builder/block_device/tests/test_mount_order.py +++ b/diskimage_builder/block_device/tests/test_mount_order.py @@ -30,14 +30,14 @@ class TestMountOrder(tc.TestGraphGeneration): graph, call_order = create_graph(config, self.fake_default_config) - result = {} - result['filesys'] = {} - result['filesys']['mkfs_root'] = {} - result['filesys']['mkfs_root']['device'] = 'fake' - result['filesys']['mkfs_var'] = {} - result['filesys']['mkfs_var']['device'] = 'fake' - result['filesys']['mkfs_var_log'] = {} - result['filesys']['mkfs_var_log']['device'] = 'fake' + state = {} + state['filesys'] = {} + state['filesys']['mkfs_root'] = {} + state['filesys']['mkfs_root']['device'] = 'fake' + state['filesys']['mkfs_var'] = {} + state['filesys']['mkfs_var']['device'] = 'fake' + state['filesys']['mkfs_var_log'] = {} + state['filesys']['mkfs_var_log']['device'] = 'fake' rollback = [] @@ -46,7 +46,7 @@ class TestMountOrder(tc.TestGraphGeneration): # XXX: do we even need to create? We could test the # sudo arguments from the mock in the below asserts # too - node.create(result, rollback) + node.create(state, rollback) # ensure that partitions are mounted in order root->var->var/log - self.assertListEqual(result['mount_order'], ['/', '/var', '/var/log']) + self.assertListEqual(state['mount_order'], ['/', '/var', '/var/log']) diff --git a/diskimage_builder/block_device/tests/test_state.py b/diskimage_builder/block_device/tests/test_state.py new file mode 100644 index 000000000..ece4a4218 --- /dev/null +++ b/diskimage_builder/block_device/tests/test_state.py @@ -0,0 +1,107 @@ +# 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 codecs +import fixtures +import json +import logging +import os + +from stevedore import extension +from testtools.matchers import FileExists + +import diskimage_builder.block_device.blockdevice as bd +import diskimage_builder.block_device.tests.test_base as tb + +from diskimage_builder.block_device.exception import \ + BlockDeviceSetupException + +logger = logging.getLogger(__name__) + + +class TestStateBase(tb.TestBase): + + def setUp(self): + super(TestStateBase, self).setUp() + + # override the extensions to the test extensions + test_extensions = extension.ExtensionManager( + namespace='diskimage_builder.block_device.plugin_test', + invoke_on_load=False) + extensions_fixture = fixtures.MonkeyPatch( + 'diskimage_builder.block_device.config._extensions', + test_extensions) + self.useFixture(extensions_fixture) + + # status and other bits saved here + self.build_dir = fixtures.TempDir() + self.useFixture(self.build_dir) + + +class TestState(TestStateBase): + + # The the state generation & saving methods + def test_state_create(self): + params = { + 'build-dir': self.build_dir.path, + 'config': self.get_config_file('cmd_create.yaml') + } + + bd_obj = bd.BlockDevice(params) + + bd_obj.cmd_init() + bd_obj.cmd_create() + + # cmd_create should have persisted this to disk + state_file = os.path.join(self.build_dir.path, + 'states', 'block-device', + 'state.json') + self.assertThat(state_file, FileExists()) + + # ensure we see the values put in by the test extensions + # persisted + with codecs.open(state_file, encoding='utf-8', mode='r') as fd: + state = json.load(fd) + self.assertDictEqual(state, + {'test_a': {'value': 'foo', + 'value2': 'bar'}, + 'test_b': {'value': 'baz'}}) + + # Test state going missing between phases + def test_missing_state(self): + params = { + 'build-dir': self.build_dir.path, + 'config': self.get_config_file('cmd_create.yaml') + } + + bd_obj = bd.BlockDevice(params) + bd_obj.cmd_init() + bd_obj.cmd_create() + + # cmd_create should have persisted this to disk + state_file = os.path.join(self.build_dir.path, + 'states', 'block-device', + 'state.json') + self.assertThat(state_file, FileExists()) + + # simulate the state somehow going missing, and ensure that + # later calls notice + os.unlink(state_file) + self.assertRaisesRegexp(BlockDeviceSetupException, + "State dump not found", + bd_obj.cmd_cleanup) + self.assertRaisesRegexp(BlockDeviceSetupException, + "State dump not found", + bd_obj.cmd_writefstab) + self.assertRaisesRegexp(BlockDeviceSetupException, + "State dump not found", + bd_obj.cmd_delete) diff --git a/setup.cfg b/setup.cfg index b48bb47de..e8b3b3f53 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,3 +71,8 @@ diskimage_builder.block_device.plugin = mkfs = diskimage_builder.block_device.level2.mkfs:Mkfs mount = diskimage_builder.block_device.level3.mount:Mount fstab = diskimage_builder.block_device.level4.fstab:Fstab + +# unit test extensions +diskimage_builder.block_device.plugin_test = + test_a = diskimage_builder.block_device.tests.plugin.test_a:TestA + test_b = diskimage_builder.block_device.tests.plugin.test_b:TestB