Merge "Create and use plugin/node abstract classes"
This commit is contained in:
commit
795ec082ed
@ -28,6 +28,8 @@ from diskimage_builder.block_device.config import \
|
||||
config_tree_to_graph
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
from diskimage_builder.block_device.utils import exec_sudo
|
||||
|
||||
|
||||
@ -189,20 +191,24 @@ class BlockDevice(object):
|
||||
|
||||
# Instantiate a "plugin" object, passing it the
|
||||
# configuration entry
|
||||
# XXX would a "factory" pattern for plugins, where we make
|
||||
# a method call on an object stevedore has instantiated be
|
||||
# better here?
|
||||
if cfg_obj_name not in self.plugin_manager:
|
||||
raise BlockDeviceSetupException(
|
||||
("Config element [%s] is not implemented" % cfg_obj_name))
|
||||
cfg_obj = self.plugin_manager[cfg_obj_name].plugin(
|
||||
cfg_obj_val, default_config)
|
||||
plugin = self.plugin_manager[cfg_obj_name].plugin
|
||||
assert issubclass(plugin, PluginBase)
|
||||
cfg_obj = plugin(cfg_obj_val, default_config)
|
||||
|
||||
# Ask the plugin for the nodes it would like to insert
|
||||
# into the graph. Some plugins, such as partitioning,
|
||||
# return multiple nodes from one config entry.
|
||||
nodes = cfg_obj.get_nodes()
|
||||
assert isinstance(nodes, list)
|
||||
for node in nodes:
|
||||
# would only be missing if a plugin was way out of
|
||||
# line and didn't put it in...
|
||||
assert node.name
|
||||
# plugins should return nodes...
|
||||
assert isinstance(node, NodeBase)
|
||||
# ensure node names are unique. networkx by default
|
||||
# just appends the attribute to the node dict for
|
||||
# existing nodes, which is not what we want.
|
||||
|
@ -1,17 +0,0 @@
|
||||
# Copyright 2016 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 diskimage_builder.block_device.level0.localloop import LocalLoop
|
||||
|
||||
__all__ = [LocalLoop]
|
@ -18,14 +18,15 @@ import subprocess
|
||||
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
from diskimage_builder.block_device.utils import parse_abs_size_spec
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalLoop(Digraph.Node):
|
||||
class LocalLoopNode(NodeBase):
|
||||
"""Level0: Local loop image device handling.
|
||||
|
||||
This class handles local loop devices that can be used
|
||||
@ -34,6 +35,7 @@ class LocalLoop(Digraph.Node):
|
||||
def __init__(self, config, default_config):
|
||||
logger.debug("Creating LocalLoop object; config [%s] "
|
||||
"default_config [%s]" % (config, default_config))
|
||||
super(LocalLoopNode, self).__init__(config['name'])
|
||||
if 'size' in config:
|
||||
self.size = parse_abs_size_spec(config['size'])
|
||||
logger.debug("Image size [%s]" % self.size)
|
||||
@ -44,18 +46,12 @@ class LocalLoop(Digraph.Node):
|
||||
self.image_dir = config['directory']
|
||||
else:
|
||||
self.image_dir = default_config['image-dir']
|
||||
self.name = config['name']
|
||||
Digraph.Node.__init__(self, self.name)
|
||||
self.filename = os.path.join(self.image_dir, self.name + ".raw")
|
||||
|
||||
def get_edges(self):
|
||||
"""Because this is created without base, there are no edges."""
|
||||
return ([], [])
|
||||
|
||||
def get_nodes(self):
|
||||
"""Returns nodes for adding to the graph"""
|
||||
return [self]
|
||||
|
||||
@staticmethod
|
||||
def image_create(filename, size):
|
||||
logger.info("Create image file [%s]" % filename)
|
||||
@ -131,3 +127,13 @@ class LocalLoop(Digraph.Node):
|
||||
|
||||
def delete(self, state):
|
||||
self._image_delete(state['blockdev'][self.name]['image'])
|
||||
|
||||
|
||||
class LocalLoop(PluginBase):
|
||||
|
||||
def __init__(self, config, defaults):
|
||||
super(PluginBase, self).__init__()
|
||||
self.node = LocalLoopNode(config, defaults)
|
||||
|
||||
def get_nodes(self):
|
||||
return [self.node]
|
||||
|
@ -1,18 +0,0 @@
|
||||
# Copyright
|
||||
# 2016-2017 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 diskimage_builder.block_device.level1.partitioning import Partitioning
|
||||
|
||||
__all__ = [Partitioning]
|
@ -14,26 +14,20 @@ import logging
|
||||
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Partition(Digraph.Node):
|
||||
|
||||
type_string = "partitions"
|
||||
class PartitionNode(NodeBase):
|
||||
|
||||
flag_boot = 1
|
||||
flag_primary = 2
|
||||
|
||||
def __init__(self, config, parent, prev_partition):
|
||||
if 'name' not in config:
|
||||
raise BlockDeviceSetupException(
|
||||
"Missing 'name' in partition config: %s" % config)
|
||||
self.name = config['name']
|
||||
|
||||
Digraph.Node.__init__(self, self.name)
|
||||
super(PartitionNode, self).__init__(config['name'])
|
||||
|
||||
self.base = config['base']
|
||||
self.partitioning = parent
|
||||
@ -64,9 +58,6 @@ class Partition(Digraph.Node):
|
||||
def get_type(self):
|
||||
return self.ptype
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def get_edges(self):
|
||||
edge_from = [self.base]
|
||||
edge_to = []
|
||||
@ -76,15 +67,3 @@ class Partition(Digraph.Node):
|
||||
|
||||
def create(self, result, rollback):
|
||||
self.partitioning.create(result, rollback)
|
||||
|
||||
def umount(self, state):
|
||||
"""Partition does not need any umount task."""
|
||||
pass
|
||||
|
||||
def cleanup(self, state):
|
||||
"""Partition does not need any cleanup."""
|
||||
pass
|
||||
|
||||
def delete(self, state):
|
||||
"""Partition does not need any cleanup."""
|
||||
pass
|
||||
|
@ -20,21 +20,22 @@ from subprocess import CalledProcessError
|
||||
from diskimage_builder.block_device.exception import \
|
||||
BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.level1.mbr import MBR
|
||||
from diskimage_builder.block_device.level1.partition import \
|
||||
Partition
|
||||
from diskimage_builder.block_device.level1.partition import PartitionNode
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
from diskimage_builder.block_device.utils import exec_sudo
|
||||
from diskimage_builder.block_device.utils import parse_abs_size_spec
|
||||
from diskimage_builder.block_device.utils import parse_rel_size_spec
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Partitioning(Digraph.Node):
|
||||
class Partitioning(PluginBase):
|
||||
|
||||
def __init__(self, config, default_config):
|
||||
logger.debug("Creating Partitioning object; config [%s]" % config)
|
||||
super(Partitioning, self).__init__()
|
||||
|
||||
# Because using multiple partitions of one base is done
|
||||
# within one object, there is the need to store a flag if the
|
||||
# creation of the partitions was already done.
|
||||
@ -75,19 +76,19 @@ class Partitioning(Digraph.Node):
|
||||
prev_partition = None
|
||||
|
||||
for part_cfg in config['partitions']:
|
||||
np = Partition(part_cfg, self, prev_partition)
|
||||
np = PartitionNode(part_cfg, self, prev_partition)
|
||||
self.partitions.append(np)
|
||||
prev_partition = np
|
||||
|
||||
def get_nodes(self):
|
||||
# return the list of partitions
|
||||
return self.partitions
|
||||
|
||||
def _size_of_block_dev(self, dev):
|
||||
with open(dev, "r") as fd:
|
||||
fd.seek(0, 2)
|
||||
return fd.tell()
|
||||
|
||||
def get_nodes(self):
|
||||
# We just add partitions
|
||||
return self.partitions
|
||||
|
||||
def _all_part_devices_exist(self, expected_part_devices):
|
||||
for part_device in expected_part_devices:
|
||||
logger.debug("Checking if partition device [%s] exists" %
|
||||
@ -127,11 +128,18 @@ class Partitioning(Digraph.Node):
|
||||
exec_sudo(["kpartx", "-avs", device_path])
|
||||
|
||||
def create(self, result, 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))
|
||||
|
||||
# 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
|
||||
# as the walk happens. But we only need to create the
|
||||
# partition table once...
|
||||
if self.already_created:
|
||||
logger.info("Not creating the partitions a second time.")
|
||||
return
|
||||
@ -143,9 +151,9 @@ class Partitioning(Digraph.Node):
|
||||
with MBR(image_path, disk_size, self.align) as part_impl:
|
||||
for part_cfg in self.partitions:
|
||||
part_name = part_cfg.get_name()
|
||||
part_bootflag = Partition.flag_boot \
|
||||
part_bootflag = PartitionNode.flag_boot \
|
||||
in part_cfg.get_flags()
|
||||
part_primary = Partition.flag_primary \
|
||||
part_primary = PartitionNode.flag_primary \
|
||||
in part_cfg.get_flags()
|
||||
part_size = part_cfg.get_size()
|
||||
part_free = part_impl.free()
|
||||
|
@ -1,17 +0,0 @@
|
||||
# Copyright 2017 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 diskimage_builder.block_device.level2.mkfs import Mkfs
|
||||
|
||||
__all__ = [Mkfs]
|
@ -17,8 +17,9 @@ import uuid
|
||||
|
||||
from diskimage_builder.block_device.exception \
|
||||
import BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
from diskimage_builder.block_device.utils import exec_sudo
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -40,12 +41,14 @@ file_system_max_label_length = {
|
||||
}
|
||||
|
||||
|
||||
class Filesystem(Digraph.Node):
|
||||
class FilesystemNode(NodeBase):
|
||||
|
||||
def __init__(self, config):
|
||||
logger.debug("Create filesystem object; config [%s]" % config)
|
||||
super(FilesystemNode, self).__init__(config['name'])
|
||||
|
||||
# Parameter check (mandatory)
|
||||
for pname in ['base', 'name', 'type']:
|
||||
for pname in ['base', 'type']:
|
||||
if pname not in config:
|
||||
raise BlockDeviceSetupException(
|
||||
"Mkfs config needs [%s]" % pname)
|
||||
@ -92,8 +95,6 @@ class Filesystem(Digraph.Node):
|
||||
if self.uuid is None:
|
||||
self.uuid = str(uuid.uuid4())
|
||||
|
||||
Digraph.Node.__init__(self, self.name)
|
||||
|
||||
logger.debug("Filesystem created [%s]" % self)
|
||||
|
||||
def get_edges(self):
|
||||
@ -137,36 +138,18 @@ class Filesystem(Digraph.Node):
|
||||
'fstype': self.type, 'opts': self.opts,
|
||||
'device': device}
|
||||
|
||||
def umount(self, state):
|
||||
"""Mkfs does not need any umount."""
|
||||
pass
|
||||
|
||||
def cleanup(self, state):
|
||||
"""Mkfs does not need any cleanup."""
|
||||
pass
|
||||
|
||||
def delete(self, state):
|
||||
"""Mkfs does not need any delete."""
|
||||
pass
|
||||
|
||||
|
||||
class Mkfs(object):
|
||||
"""Module for creating file systems
|
||||
class Mkfs(PluginBase):
|
||||
"""Create a file system
|
||||
|
||||
This block device module handles creating different file
|
||||
systems.
|
||||
"""
|
||||
|
||||
type_string = "mkfs"
|
||||
|
||||
def __init__(self, config, default_config):
|
||||
logger.debug("Create Mkfs object; config [%s]" % config)
|
||||
logger.debug("default_config [%s]" % default_config)
|
||||
self.config = config
|
||||
self.default_config = default_config
|
||||
def __init__(self, config, defaults):
|
||||
super(Mkfs, self).__init__()
|
||||
self.filesystems = {}
|
||||
|
||||
fs = Filesystem(self.config)
|
||||
fs = FilesystemNode(config)
|
||||
self.filesystems[fs.get_name()] = fs
|
||||
|
||||
def get_nodes(self):
|
||||
|
@ -1,17 +0,0 @@
|
||||
# Copyright 2017 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 diskimage_builder.block_device.level3.mount import Mount
|
||||
|
||||
__all__ = [Mount]
|
@ -17,9 +17,10 @@ import os
|
||||
|
||||
from diskimage_builder.block_device.exception \
|
||||
import BlockDeviceSetupException
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
from diskimage_builder.block_device.utils import exec_sudo
|
||||
from diskimage_builder.block_device.utils import sort_mount_points
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -32,17 +33,18 @@ mount_points = {}
|
||||
sorted_mount_points = None
|
||||
|
||||
|
||||
class MountPoint(Digraph.Node):
|
||||
class MountPointNode(NodeBase):
|
||||
|
||||
def __init__(self, mount_base, config):
|
||||
super(MountPointNode, self).__init__(config['name'])
|
||||
|
||||
# Parameter check
|
||||
self.mount_base = mount_base
|
||||
for pname in ['base', 'name', 'mount_point']:
|
||||
for pname in ['base', 'mount_point']:
|
||||
if pname not in config:
|
||||
raise BlockDeviceSetupException(
|
||||
"MountPoint config needs [%s]" % pname)
|
||||
setattr(self, pname, config[pname])
|
||||
Digraph.Node.__init__(self, self.name)
|
||||
logger.debug("MountPoint created [%s]" % self)
|
||||
|
||||
def get_node(self):
|
||||
@ -116,30 +118,22 @@ class MountPoint(Digraph.Node):
|
||||
logger.info("Called for [%s]" % self.name)
|
||||
exec_sudo(["umount", state['mount'][self.mount_point]['path']])
|
||||
|
||||
def cleanup(self, state):
|
||||
"""Mount does not need any cleanup."""
|
||||
pass
|
||||
|
||||
def delete(self, state):
|
||||
self.umount(state)
|
||||
|
||||
|
||||
class Mount(object):
|
||||
|
||||
type_string = "mount"
|
||||
|
||||
def __init__(self, config, params):
|
||||
logger.debug("Mounting object; config [%s]" % config)
|
||||
self.config = config
|
||||
self.params = params
|
||||
|
||||
if 'mount-base' not in self.params:
|
||||
raise BlockDeviceSetupException(
|
||||
"Mount default config needs 'mount-base'")
|
||||
self.mount_base = self.params['mount-base']
|
||||
class Mount(PluginBase):
|
||||
def __init__(self, config, defaults):
|
||||
super(Mount, self).__init__()
|
||||
|
||||
self.mount_points = {}
|
||||
mp = MountPoint(self.mount_base, self.config)
|
||||
|
||||
if 'mount-base' not in defaults:
|
||||
raise BlockDeviceSetupException(
|
||||
"Mount default config needs 'mount-base'")
|
||||
self.mount_base = defaults['mount-base']
|
||||
|
||||
mp = MountPointNode(self.mount_base, config)
|
||||
self.mount_points[mp.get_name()] = mp
|
||||
|
||||
def get_nodes(self):
|
||||
|
@ -1,17 +0,0 @@
|
||||
# Copyright 2017 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 diskimage_builder.block_device.level4.fstab import Fstab
|
||||
|
||||
__all__ = [Fstab]
|
@ -14,30 +14,20 @@
|
||||
|
||||
import logging
|
||||
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
from diskimage_builder.block_device.plugin import NodeBase
|
||||
from diskimage_builder.block_device.plugin import PluginBase
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Fstab(Digraph.Node):
|
||||
|
||||
type_string = "fstab"
|
||||
|
||||
class FstabNode(NodeBase):
|
||||
def __init__(self, config, params):
|
||||
logger.debug("Fstab object; config [%s]" % config)
|
||||
self.config = config
|
||||
self.params = params
|
||||
self.name = self.config['name']
|
||||
self.base = self.config['base']
|
||||
Digraph.Node.__init__(self, self.name)
|
||||
|
||||
self.options = self.config.get('options', 'defaults')
|
||||
self.dump_freq = self.config.get('dump-freq', 0)
|
||||
self.fsck_passno = self.config.get('fsck-passno', 2)
|
||||
|
||||
def get_nodes(self):
|
||||
return [self]
|
||||
super(FstabNode, self).__init__(config['name'])
|
||||
self.base = config['base']
|
||||
self.options = config.get('options', 'defaults')
|
||||
self.dump_freq = config.get('dump-freq', 0)
|
||||
self.fsck_passno = config.get('fsck-passno', 2)
|
||||
|
||||
def get_edges(self):
|
||||
edge_from = [self.base]
|
||||
@ -59,14 +49,12 @@ class Fstab(Digraph.Node):
|
||||
'fsck-passno': self.fsck_passno
|
||||
}
|
||||
|
||||
def umount(self, state):
|
||||
"""Fstab does not need any umount task."""
|
||||
pass
|
||||
|
||||
def cleanup(self, state):
|
||||
"""Fstab does not need any cleanup."""
|
||||
pass
|
||||
class Fstab(PluginBase):
|
||||
def __init__(self, config, defaults):
|
||||
super(Fstab, self).__init__()
|
||||
|
||||
def delete(self, state):
|
||||
"""Fstab does not need any cleanup."""
|
||||
pass
|
||||
self.node = FstabNode(config, defaults)
|
||||
|
||||
def get_nodes(self):
|
||||
return [self.node]
|
||||
|
205
diskimage_builder/block_device/plugin.py
Normal file
205
diskimage_builder/block_device/plugin.py
Normal file
@ -0,0 +1,205 @@
|
||||
# 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 abc
|
||||
import six
|
||||
|
||||
#
|
||||
# Plugins convert configuration entries into graph nodes ready for
|
||||
# processing. This defines the abstract classes for both.
|
||||
#
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class NodeBase(object):
|
||||
"""A configuration node entry
|
||||
|
||||
This is the main driver class for dib-block-device operation.
|
||||
|
||||
The final operations graph is composed of instantiations of this
|
||||
class. The graph undergoes a topological sort (i.e. is linearised
|
||||
in dependency order) and each node has :func:`create` called in
|
||||
order to perform its operations.
|
||||
|
||||
Every node has a unique string ``name``. This is its key in the
|
||||
graph and used for edge relationships. Implementations must
|
||||
ensure they initalize it; e.g.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class FooNode(NodeBase):
|
||||
def __init__(name, arg1, ...):
|
||||
super(FooNode, self).__init__(name)
|
||||
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_edges(self):
|
||||
"""Return the dependencies/edges for this node
|
||||
|
||||
This function will be called after all nodes are created (this
|
||||
is because some plugins need to know the global state of all
|
||||
nodes to decide their dependencies).
|
||||
|
||||
This function returns a tuple with two lists
|
||||
|
||||
* ``edges_from`` : a list of node names that point to us
|
||||
* ``edges_to`` : a list of node names we point to
|
||||
|
||||
In most cases, node creation will have saved a single parent
|
||||
that was given in the ``base`` parameter of the configuration.
|
||||
A usual return might look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def get_edges(self):
|
||||
return ( [self.base], [] )
|
||||
|
||||
Some nodes (``level0``) don't have a base, however
|
||||
"""
|
||||
return
|
||||
|
||||
@abc.abstractmethod
|
||||
def create(self, results, rollback):
|
||||
"""Main creation driver
|
||||
|
||||
This is the main driver function. After the graph is
|
||||
linearised, each node has it's :func:`create` function called.
|
||||
|
||||
Arguments:
|
||||
|
||||
:param results: 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.
|
||||
|
||||
:param rollback: A shared list of functions to be called in
|
||||
the failure case. Nodes should only append to this list.
|
||||
On failure, the callbacks will be processed in reverse
|
||||
order.
|
||||
|
||||
:raises Exception: A failure should raise an exception. This
|
||||
will initiate the rollback
|
||||
|
||||
:return: None
|
||||
"""
|
||||
return
|
||||
|
||||
def umount(self, state):
|
||||
"""Umount actions
|
||||
|
||||
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
|
||||
made.
|
||||
:return: None
|
||||
"""
|
||||
return
|
||||
|
||||
def cleanup(self, state):
|
||||
"""Cleanup actions
|
||||
|
||||
Actions to taken when ``dib-block-device cleanup`` is called.
|
||||
This is the cleanup path in the *success* case. The nodes are
|
||||
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
|
||||
made.
|
||||
:return: None
|
||||
"""
|
||||
return
|
||||
|
||||
def delete(self, state):
|
||||
"""Cleanup actions
|
||||
|
||||
Actions to taken when ``dib-block-device delete`` is called.
|
||||
This is the cleanup path in case of a reported external
|
||||
*failure*. The nodes are 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
|
||||
made.
|
||||
:return: None
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class PluginBase(object):
|
||||
"""The base plugin object
|
||||
|
||||
This is the base plugin object. Plugins are an instantiation of
|
||||
this class. There should be an entry-point (see setup.cfg)
|
||||
defined under ``diskimage_builder.block_device.plugin`` for each
|
||||
plugin, e.g.
|
||||
|
||||
foo = diskimage_builder.block_device.levelX.foo:Foo
|
||||
|
||||
A configuration entry in the graph config that matches this entry
|
||||
point will create an instance of this class, e.g.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
foo:
|
||||
name: foo_node
|
||||
base: parent_node
|
||||
argument_a: bar
|
||||
argument_b: baz
|
||||
|
||||
The ``__init__`` function will be passed two arguments:
|
||||
|
||||
``config``
|
||||
The full configuration dictionary for the entry.
|
||||
A unique ``name`` entry can be assumed. In most cases
|
||||
a ``base`` entry will be present giving the parent node
|
||||
(see :func:`NodeBase.get_edges`).
|
||||
``defaults``
|
||||
The global defaults dictionary (see ``--params``)
|
||||
|
||||
``get_nodes()`` should return the node object(s) created by the
|
||||
config for insertion into the final configuration graph. In the
|
||||
simplest case, this is probably a single node created during
|
||||
instantiation. e.g.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Foo(PluginBase):
|
||||
|
||||
def __init__(self, config, defaults):
|
||||
super(Foo, self).__init__()
|
||||
self.node = FooNode(config.name, ...)
|
||||
|
||||
def get_nodes(self):
|
||||
return [self.node]
|
||||
|
||||
|
||||
Some plugins require more, however.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_nodes(self):
|
||||
"""Return nodes created by the plugin
|
||||
|
||||
:returns: a list of :class:`.NodeBase` objects for insertion
|
||||
into the graph
|
||||
"""
|
||||
return
|
@ -15,7 +15,7 @@ import mock
|
||||
|
||||
import diskimage_builder.block_device.tests.test_config as tc
|
||||
|
||||
from diskimage_builder.block_device.level3.mount import MountPoint
|
||||
from diskimage_builder.block_device.level3.mount import MountPointNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -42,7 +42,7 @@ class TestMountOrder(tc.TestGraphGeneration):
|
||||
rollback = []
|
||||
|
||||
for node in call_order:
|
||||
if isinstance(node, MountPoint):
|
||||
if isinstance(node, MountPointNode):
|
||||
# XXX: do we even need to create? We could test the
|
||||
# sudo arguments from the mock in the below asserts
|
||||
# too
|
||||
|
@ -1,228 +0,0 @@
|
||||
# Copyright 2016 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 bisect
|
||||
|
||||
|
||||
class Digraph(object):
|
||||
"""Implements a directed graph.
|
||||
|
||||
Each node of the digraph must have a unique name.
|
||||
"""
|
||||
|
||||
class Edge(object):
|
||||
"""Directed graph edge.
|
||||
|
||||
The digraph has weighted edges. This class holds the weight and
|
||||
a reference to the node.
|
||||
"""
|
||||
|
||||
def __init__(self, node, weight):
|
||||
self.__node = node
|
||||
self.__weight = weight
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__weight == other.get_weight() \
|
||||
and self.__node == other.get_node()
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.__weight < other.get_weight()
|
||||
|
||||
def get_node(self):
|
||||
"""Return the (pointed to) node"""
|
||||
return self.__node
|
||||
|
||||
def get_weight(self):
|
||||
"""Return the edge's weight"""
|
||||
return self.__weight
|
||||
|
||||
class Node(object):
|
||||
"""Directed graph node.
|
||||
|
||||
This holds the incoming and outgoing edges as well as the
|
||||
nodes' name.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Initializes a node.
|
||||
|
||||
Incoming and outgoing are lists of nodes. Typically one
|
||||
direction is provided and the other can be automatically
|
||||
computed.
|
||||
"""
|
||||
self.__name = name
|
||||
self.__incoming = []
|
||||
self.__outgoing = []
|
||||
|
||||
def __repr__(self):
|
||||
return "<Node [%s]>" % self.__name
|
||||
|
||||
def get_name(self):
|
||||
"""Returns the name of the node."""
|
||||
return self.__name
|
||||
|
||||
def add_incoming(self, node, weight):
|
||||
"""Add node to the incoming list."""
|
||||
bisect.insort(self.__incoming, Digraph.Edge(node, weight))
|
||||
|
||||
def add_outgoing(self, node, weight):
|
||||
"""Add node to the outgoing list."""
|
||||
bisect.insort(self.__outgoing, Digraph.Edge(node, weight))
|
||||
|
||||
def get_iter_outgoing(self):
|
||||
"""Return an iterator over the outgoing nodes."""
|
||||
|
||||
return iter([x.get_node() for x in self.__outgoing])
|
||||
|
||||
def has_incoming(self):
|
||||
"""Returns True if the node has incoming edges"""
|
||||
return self.__incoming
|
||||
|
||||
@staticmethod
|
||||
def __as_named_list(inlist):
|
||||
"""Return given list as list of names."""
|
||||
|
||||
return [x.get_node().get_name() for x in inlist]
|
||||
|
||||
def get_outgoing_as_named_list(self):
|
||||
"""Return the names of all outgoing nodes as a list."""
|
||||
|
||||
return self.__as_named_list(self.__outgoing)
|
||||
|
||||
def __init__(self):
|
||||
"""Create a empty digraph."""
|
||||
self._named_nodes = {}
|
||||
|
||||
def create_from_dict(self, init_dgraph, node_gen_func=Node):
|
||||
"""Creates a new digraph based on the given information."""
|
||||
|
||||
# First run: create all nodes
|
||||
for node_name in init_dgraph:
|
||||
# Create the node and put it into the object list of all
|
||||
# nodes and into the local dictionary of named nodes.
|
||||
named_node = node_gen_func(node_name)
|
||||
self.add_node(named_node)
|
||||
|
||||
# Second run: run through all nodes and create the edges.
|
||||
for node_name, outs in init_dgraph.items():
|
||||
node_from = self.find(node_name)
|
||||
for onode in outs:
|
||||
node_to = self.find(onode)
|
||||
if node_to is None:
|
||||
raise RuntimeError("Node '%s' is referenced "
|
||||
"but not specified" % onode)
|
||||
self.create_edge(node_from, node_to)
|
||||
|
||||
def add_node(self, anode):
|
||||
"""Adds a new node to the graph.
|
||||
|
||||
Checks if the node with the same name already exists.
|
||||
"""
|
||||
assert issubclass(anode.__class__, Digraph.Node)
|
||||
|
||||
for node in self._named_nodes.values():
|
||||
if node.get_name() == anode.get_name():
|
||||
raise RuntimeError("Node with name [%s] already "
|
||||
"exists" % node.get_name())
|
||||
self._named_nodes[anode.get_name()] = anode
|
||||
|
||||
def create_edge(self, anode, bnode, weight=0):
|
||||
"""Creates an edge from a to b - both must be nodes."""
|
||||
|
||||
assert issubclass(anode.__class__, Digraph.Node)
|
||||
assert issubclass(bnode.__class__, Digraph.Node)
|
||||
assert anode.get_name() in self._named_nodes.keys()
|
||||
assert anode == self._named_nodes[anode.get_name()]
|
||||
assert bnode.get_name() in self._named_nodes.keys()
|
||||
assert bnode == self._named_nodes[bnode.get_name()]
|
||||
anode.add_outgoing(bnode, weight)
|
||||
bnode.add_incoming(anode, weight)
|
||||
|
||||
def get_iter_nodes_values(self):
|
||||
"""Returns the nodes dict to the values.
|
||||
|
||||
Note: it is not possible to change things with the help of the
|
||||
result of this function.
|
||||
"""
|
||||
return iter(self._named_nodes.values())
|
||||
|
||||
def find(self, name):
|
||||
"""Get the node with the given name.
|
||||
|
||||
Return None if not available.
|
||||
"""
|
||||
if name not in self._named_nodes:
|
||||
return None
|
||||
|
||||
return self._named_nodes[name]
|
||||
|
||||
def as_dict(self):
|
||||
"""Outputs this digraph and create a dictionary."""
|
||||
|
||||
# Start with an empty dictionary
|
||||
rval = {}
|
||||
for node in self._named_nodes.values():
|
||||
rval[node.get_name()] = node.get_outgoing_as_named_list()
|
||||
return rval
|
||||
|
||||
def topological_sort(self):
|
||||
"""Digraph topological search.
|
||||
|
||||
This algorithm is based upon a depth first search with
|
||||
'making' some special nodes.
|
||||
The result is the topological sorted list of nodes.
|
||||
"""
|
||||
|
||||
# List of topological sorted nodes
|
||||
tsort = []
|
||||
# List of nodes already visited.
|
||||
# (This is held here - local to the algorithm - to not modify the
|
||||
# nodes themselves.)
|
||||
visited = []
|
||||
|
||||
def visit(node):
|
||||
"""Recursive deep first search function."""
|
||||
|
||||
if node not in visited:
|
||||
visited.append(node)
|
||||
for onode in node.get_iter_outgoing():
|
||||
visit(onode)
|
||||
tsort.insert(0, node)
|
||||
|
||||
# The 'main' function of the topological sort
|
||||
for node in self.get_iter_nodes_values():
|
||||
if node.has_incoming():
|
||||
continue
|
||||
visit(node)
|
||||
|
||||
return tsort
|
||||
|
||||
|
||||
# Utility functions
|
||||
|
||||
def digraph_create_from_dict(init_dgraph, node_gen_func=Digraph.Node):
|
||||
"""Creates a new digraph based on the given information."""
|
||||
|
||||
digraph = Digraph()
|
||||
digraph.create_from_dict(init_dgraph, node_gen_func)
|
||||
return digraph
|
||||
|
||||
|
||||
def node_list_to_node_name_list(node_list):
|
||||
"""Converts a node list into a list of the corresponding node names."""
|
||||
|
||||
node_name_list = []
|
||||
for node in node_list:
|
||||
node_name_list.append(node.get_name())
|
||||
return node_name_list
|
@ -19,7 +19,8 @@ import subprocess
|
||||
import tempfile
|
||||
import testtools
|
||||
|
||||
from diskimage_builder.block_device.level0.localloop import LocalLoop
|
||||
from diskimage_builder.block_device.level0.localloop \
|
||||
import LocalLoopNode as LocalLoop
|
||||
from diskimage_builder.block_device.level1.mbr import MBR
|
||||
|
||||
|
||||
|
@ -1,143 +0,0 @@
|
||||
# Copyright 2016 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 diskimage_builder.graph.digraph import Digraph
|
||||
from diskimage_builder.graph.digraph import digraph_create_from_dict
|
||||
import testtools
|
||||
|
||||
|
||||
class TestDigraph(testtools.TestCase):
|
||||
|
||||
def test_constructor_001(self):
|
||||
"""Test conversion from dictionary to graph and back (two nodes)"""
|
||||
|
||||
d = {"A": ["B"], "B": []}
|
||||
dg = digraph_create_from_dict(d)
|
||||
e = dg.as_dict()
|
||||
self.assertEqual(d["A"], list(e["A"]))
|
||||
|
||||
def test_constructor_002(self):
|
||||
"""Test conversion from dictionary to graph and back (zero nodes)"""
|
||||
|
||||
d = {}
|
||||
dg = digraph_create_from_dict(d)
|
||||
e = dg.as_dict()
|
||||
self.assertEqual(d, e)
|
||||
|
||||
def test_constructor_003(self):
|
||||
"""Test conversion from dictionary to graph and back (one node)"""
|
||||
|
||||
d = {"A": []}
|
||||
dg = digraph_create_from_dict(d)
|
||||
e = dg.as_dict()
|
||||
self.assertEqual(d["A"], list(e["A"]))
|
||||
|
||||
def test_constructor_004(self):
|
||||
"""Test conversion from dictionary to graph and back (one node)"""
|
||||
|
||||
d = {"A": ["A"]}
|
||||
dg = digraph_create_from_dict(d)
|
||||
e = dg.as_dict()
|
||||
self.assertEqual(d["A"], list(e["A"]))
|
||||
|
||||
def test_constructor_005(self):
|
||||
"""Test conversion: error: pointed node does not exists"""
|
||||
|
||||
d = {"A": ["B"]}
|
||||
try:
|
||||
d = digraph_create_from_dict(d)
|
||||
self.assertTrue(False)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def test_constructor_006(self):
|
||||
"""Test conversion from dictionary: two node circle"""
|
||||
|
||||
d = {"A": ["B"], "B": ["A"]}
|
||||
dg = digraph_create_from_dict(d)
|
||||
e = dg.as_dict()
|
||||
self.assertEqual(d["A"], list(e["A"]))
|
||||
self.assertEqual(d["B"], list(e["B"]))
|
||||
|
||||
def test_constructor_007(self):
|
||||
"""Test conversion from dictionary: more complex graph"""
|
||||
|
||||
d = {"A": ["B"], "B": ["A", "D", "C"], "C": ["A", "D"],
|
||||
"D": ["D"]}
|
||||
dg = digraph_create_from_dict(d)
|
||||
e = dg.as_dict()
|
||||
self.assertEqual(d['A'], list(e['A']))
|
||||
self.assertEqual(set(d['B']), set(e['B']))
|
||||
self.assertEqual(set(d['C']), set(e['C']))
|
||||
self.assertEqual(d['D'], list(e['D']))
|
||||
|
||||
def test_find_01(self):
|
||||
"""Digraph find with element available"""
|
||||
|
||||
d = {"A": ["B"], "B": ["A", "C", "D"], "C": ["A", "D"],
|
||||
"D": ["D"]}
|
||||
dg = digraph_create_from_dict(d)
|
||||
n = dg.find("A")
|
||||
self.assertEqual("A", n.get_name(),)
|
||||
|
||||
def test_find_02(self):
|
||||
"""Digraph find with element not available"""
|
||||
|
||||
d = {"A": ["B"], "B": ["A", "C", "D"], "C": ["A", "D"],
|
||||
"D": ["D"]}
|
||||
dg = digraph_create_from_dict(d)
|
||||
n = dg.find("Z")
|
||||
self.assertIsNone(n)
|
||||
|
||||
def test_get_named_node_01(self):
|
||||
"""Digraph get named node with map available"""
|
||||
|
||||
d = {"A": ["B"], "B": ["A", "C", "D"], "C": ["A", "D"],
|
||||
"D": ["D"]}
|
||||
dg = digraph_create_from_dict(d)
|
||||
n = dg.find("A")
|
||||
self.assertEqual("A", n.get_name())
|
||||
|
||||
def test_add_node_01(self):
|
||||
"""Digraph add node with two times same name"""
|
||||
|
||||
dg = Digraph()
|
||||
n1 = Digraph.Node("myname")
|
||||
n2 = Digraph.Node("myname")
|
||||
dg.add_node(n1)
|
||||
try:
|
||||
dg.add_node(n2)
|
||||
self.assertTrue(False)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def test_iter_outgoing_weight_01(self):
|
||||
"""Tests iter_outgoing in a graph with weights"""
|
||||
|
||||
digraph = Digraph()
|
||||
node0 = Digraph.Node("R")
|
||||
digraph.add_node(node0)
|
||||
node1 = Digraph.Node("A")
|
||||
digraph.add_node(node1)
|
||||
node2 = Digraph.Node("B")
|
||||
digraph.add_node(node2)
|
||||
node3 = Digraph.Node("C")
|
||||
digraph.add_node(node3)
|
||||
|
||||
digraph.create_edge(node0, node1, 1)
|
||||
digraph.create_edge(node0, node2, 2)
|
||||
digraph.create_edge(node0, node3, 3)
|
||||
|
||||
self.assertEqual([node1, node2, node3],
|
||||
list(node0.get_iter_outgoing()))
|
@ -1,116 +0,0 @@
|
||||
# Copyright 2016 Andreas Florath (andreas@florath.net)
|
||||
#
|
||||
# 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 testtools
|
||||
|
||||
from diskimage_builder.graph.digraph import Digraph
|
||||
from diskimage_builder.graph.digraph import digraph_create_from_dict
|
||||
from diskimage_builder.graph.digraph import node_list_to_node_name_list
|
||||
|
||||
|
||||
class TestTopologicalSearch(testtools.TestCase):
|
||||
|
||||
def test_tsort_001(self):
|
||||
"""Simple three node digraph"""
|
||||
|
||||
dg = digraph_create_from_dict(
|
||||
{"A": ["B", "C"], "B": ["C"], "C": []})
|
||||
tsort = dg.topological_sort()
|
||||
tnames = node_list_to_node_name_list(tsort)
|
||||
self.assertEqual(tnames, ['A', 'B', 'C'], "incorrect")
|
||||
|
||||
def test_tsort_002(self):
|
||||
"""Zero node digraph"""
|
||||
|
||||
dg = digraph_create_from_dict({})
|
||||
tsort = dg.topological_sort()
|
||||
tnames = node_list_to_node_name_list(tsort)
|
||||
self.assertEqual(tnames, [], "incorrect")
|
||||
|
||||
def test_tsort_003(self):
|
||||
"""One node digraph"""
|
||||
|
||||
dg = digraph_create_from_dict({"A": []})
|
||||
tsort = dg.topological_sort()
|
||||
tnames = node_list_to_node_name_list(tsort)
|
||||
self.assertEqual(tnames, ["A"], "incorrect")
|
||||
|
||||
def test_tsort_004(self):
|
||||
"""More complex digraph"""
|
||||
|
||||
dg = digraph_create_from_dict(
|
||||
{"A": ["B", "C"], "B": ["C", "E"], "C": ["D", "E"],
|
||||
"D": ["E"], "E": []})
|
||||
tsort = dg.topological_sort()
|
||||
tnames = node_list_to_node_name_list(tsort)
|
||||
self.assertEqual(tnames, ['A', 'B', 'C', 'D', 'E'], "incorrect")
|
||||
|
||||
def test_tsort_005(self):
|
||||
"""Digraph with two components"""
|
||||
|
||||
dg = digraph_create_from_dict({"A": ["B", "C"], "B": ["C"], "C": [],
|
||||
"D": ["E"], "E": []})
|
||||
tsort = dg.topological_sort()
|
||||
tnames = node_list_to_node_name_list(tsort)
|
||||
# Because of two components, there exist a couple of different
|
||||
# possibilities - but these are all the requirements that have
|
||||
# to be fulfilled to be a correct topological sort:
|
||||
self.assertTrue(tnames.index('A') < tnames.index('B'))
|
||||
self.assertTrue(tnames.index('B') < tnames.index('C'))
|
||||
self.assertTrue(tnames.index('D') < tnames.index('E'))
|
||||
|
||||
def test_tsort_006(self):
|
||||
"""Complex digraph with weights"""
|
||||
|
||||
digraph = Digraph()
|
||||
node0 = Digraph.Node("R")
|
||||
digraph.add_node(node0)
|
||||
node1 = Digraph.Node("A")
|
||||
digraph.add_node(node1)
|
||||
node2 = Digraph.Node("B")
|
||||
digraph.add_node(node2)
|
||||
node3 = Digraph.Node("C")
|
||||
digraph.add_node(node3)
|
||||
node4 = Digraph.Node("B1")
|
||||
digraph.add_node(node4)
|
||||
node5 = Digraph.Node("B2")
|
||||
digraph.add_node(node5)
|
||||
node6 = Digraph.Node("B3")
|
||||
digraph.add_node(node6)
|
||||
|
||||
digraph.create_edge(node0, node1, 1)
|
||||
digraph.create_edge(node0, node2, 2)
|
||||
digraph.create_edge(node0, node3, 3)
|
||||
|
||||
digraph.create_edge(node2, node4, 7)
|
||||
digraph.create_edge(node2, node5, 14)
|
||||
digraph.create_edge(node2, node6, 21)
|
||||
|
||||
tsort = digraph.topological_sort()
|
||||
tnames = node_list_to_node_name_list(tsort)
|
||||
|
||||
# Also here: many possible solutions
|
||||
self.assertTrue(tnames.index('R') < tnames.index('A'))
|
||||
self.assertTrue(tnames.index('R') < tnames.index('B'))
|
||||
self.assertTrue(tnames.index('R') < tnames.index('C'))
|
||||
self.assertTrue(tnames.index('B') < tnames.index('B1'))
|
||||
self.assertTrue(tnames.index('B') < tnames.index('B2'))
|
||||
self.assertTrue(tnames.index('B') < tnames.index('B3'))
|
||||
|
||||
# In addition in the weighted graph the following
|
||||
# must also hold:
|
||||
self.assertTrue(tnames.index('B') < tnames.index('A'))
|
||||
self.assertTrue(tnames.index('C') < tnames.index('B'))
|
||||
self.assertTrue(tnames.index('B2') < tnames.index('B1'))
|
||||
self.assertTrue(tnames.index('B3') < tnames.index('B2'))
|
Loading…
Reference in New Issue
Block a user