Commit 212fb30e authored by Łukasz Nowak's avatar Łukasz Nowak

Merge branch 'master' into promises

parents 73bdb764 6d3bed94
...@@ -29,6 +29,13 @@ from AccessControl import ClassSecurityInfo ...@@ -29,6 +29,13 @@ from AccessControl import ClassSecurityInfo
from Products.ERP5Type import Permissions from Products.ERP5Type import Permissions
from Products.ERP5.Document.Item import Item from Products.ERP5.Document.Item import Item
from lxml import etree from lxml import etree
import collections
class DisconnectedSoftwareTree(Exception):
pass
class CyclicSoftwareTree(Exception):
pass
class SoftwareInstance(Item): class SoftwareInstance(Item):
""" """
...@@ -60,3 +67,44 @@ class SoftwareInstance(Item): ...@@ -60,3 +67,44 @@ class SoftwareInstance(Item):
value = element.text value = element.text
result_dict[key] = value result_dict[key] = value
return result_dict return result_dict
security.declareProtected(Permissions.AccessContentsInformation,
'checkNotCyclic')
def checkNotCyclic(self, graph):
# see http://neopythonic.blogspot.com/2009/01/detecting-cycles-in-directed-graph.html
todo = set(graph.keys())
while todo:
node = todo.pop()
stack = [node]
while stack:
top = stack[-1]
for node in graph[top]:
if node in stack:
raise CyclicSoftwareTree
if node in todo:
stack.append(node)
todo.remove(node)
break
else:
node = stack.pop()
return True
security.declareProtected(Permissions.AccessContentsInformation,
'checkConnected')
def checkConnected(self, graph, root):
size = len(graph)
visited = set()
to_crawl = collections.deque(graph[root])
while to_crawl:
current = to_crawl.popleft()
if current in visited:
continue
visited.add(current)
node_children = set(graph[current])
to_crawl.extend(node_children - visited)
# add one to visited, as root won't be visited, only children
# this is false positive in case of cyclic graphs, but they are
# anyway wrong in Software Instance trees
if size != len(visited) + 1:
raise DisconnectedSoftwareTree
return True
...@@ -64,9 +64,12 @@ instance_xml = kwargs["instance_xml"]\n ...@@ -64,9 +64,12 @@ instance_xml = kwargs["instance_xml"]\n
sla_xml = kwargs["sla_xml"]\n sla_xml = kwargs["sla_xml"]\n
state = kwargs["state"]\n state = kwargs["state"]\n
\n \n
# Get root software instance\n # graph allows to "simulate" tree change after requested operation\n
graph = {}\n
# Get root software instance and create initial graph\n
predecessor_software_instance = software_instance\n predecessor_software_instance = software_instance\n
while (predecessor_software_instance is not None):\n while (predecessor_software_instance is not None):\n
graph[predecessor_software_instance.getUid()] = predecessor_software_instance.getPredecessorUidList()\n
root_software_instance = predecessor_software_instance\n root_software_instance = predecessor_software_instance\n
predecessor_software_instance = predecessor_software_instance.getPredecessorRelatedValue(\n predecessor_software_instance = predecessor_software_instance.getPredecessorRelatedValue(\n
portal_type="Software Instance")\n portal_type="Software Instance")\n
...@@ -84,6 +87,11 @@ request_software_instance = software_instance.portal_catalog.getResultValue(\n ...@@ -84,6 +87,11 @@ request_software_instance = software_instance.portal_catalog.getResultValue(\n
root_uid=root_software_instance.getUid(),\n root_uid=root_software_instance.getUid(),\n
)\n )\n
\n \n
# above query does not find root software instance, but as this case is easy\n
# to find, just check if such request does not come and raise\n
if root_software_instance.getTitle() == requested_partition_reference:\n
raise ValueError(\'It is disallowed to request root software instance\')\n
\n
if (request_software_instance is None):\n if (request_software_instance is None):\n
if (portal.portal_activities.countMessageWithTag(tag) > 0):\n if (portal.portal_activities.countMessageWithTag(tag) > 0):\n
# The software instance is already under creation but can not be fetched from catalog\n # The software instance is already under creation but can not be fetched from catalog\n
...@@ -130,6 +138,8 @@ else:\n ...@@ -130,6 +138,8 @@ else:\n
predecessor_software_instance.edit(\n predecessor_software_instance.edit(\n
predecessor_uid_list=predecessor_uid_list,\n predecessor_uid_list=predecessor_uid_list,\n
activate_kw={\'tag\': tag},)\n activate_kw={\'tag\': tag},)\n
graph[predecessor_software_instance.getUid()] = predecessor_uid_list\n
\n
if state == \'started\':\n if state == \'started\':\n
request_software_instance.startRequested()\n request_software_instance.startRequested()\n
request_software_instance.activate(after_tag=tag).requestStartComputerPartition()\n request_software_instance.activate(after_tag=tag).requestStartComputerPartition()\n
...@@ -139,7 +149,16 @@ else:\n ...@@ -139,7 +149,16 @@ else:\n
else:\n else:\n
raise ValueError(\'State %r is not supported\' % state)\n raise ValueError(\'State %r is not supported\' % state)\n
predecessor_list = software_instance.getPredecessorList() + [request_software_instance.getRelativeUrl()]\n predecessor_list = software_instance.getPredecessorList() + [request_software_instance.getRelativeUrl()]\n
# Add requested software instance to graph if does not exists there yet\n
if not request_software_instance.getUid() in graph:\n
graph[request_software_instance.getUid()] = []\n
\n
# update graph to reflect requested operation\n
graph[software_instance.getUid()] = software_instance.getPredecessorUidList() + [request_software_instance.getUid()]\n
\n \n
# check if all elements are still connected\n
software_instance.checkNotCyclic(graph)\n
software_instance.checkConnected(graph, root_software_instance.getUid())\n
\n \n
software_instance.edit(\n software_instance.edit(\n
predecessor_list=predecessor_list,\n predecessor_list=predecessor_list,\n
......
246 252
\ No newline at end of file \ No newline at end of file
...@@ -361,6 +361,7 @@ class TestVifibSlapWebService(testVifibMixin): ...@@ -361,6 +361,7 @@ class TestVifibSlapWebService(testVifibMixin):
self.assertEqual(1, len(software_instance_list)) self.assertEqual(1, len(software_instance_list))
software_instance = software_instance_list[0] software_instance = software_instance_list[0]
sequence.edit( sequence.edit(
root_software_instance_title=software_title,
software_instance_uid=software_instance.getUid(), software_instance_uid=software_instance.getUid(),
software_instance_reference=software_instance.getReference(), software_instance_reference=software_instance.getReference(),
hosting_subscription_uid=software_instance.getAggregateRelatedValue( hosting_subscription_uid=software_instance.getAggregateRelatedValue(
...@@ -1187,6 +1188,10 @@ class TestVifibSlapWebService(testVifibMixin): ...@@ -1187,6 +1188,10 @@ class TestVifibSlapWebService(testVifibMixin):
def stepSelectRequestedReferenceChildrenBChild(self, sequence, **kw): def stepSelectRequestedReferenceChildrenBChild(self, sequence, **kw):
sequence.edit(requested_reference='children_b_child') sequence.edit(requested_reference='children_b_child')
def stepSelectRequestedReferenceRootSoftwareInstanceTitle(self, sequence,
**kw):
sequence.edit(requested_reference=sequence['root_software_instance_title'])
def stepSelectRequestedReferenceB(self, sequence, **kw): def stepSelectRequestedReferenceB(self, sequence, **kw):
sequence.edit(requested_reference='b') sequence.edit(requested_reference='b')
...@@ -3110,7 +3115,7 @@ class TestVifibSlapWebService(testVifibMixin): ...@@ -3110,7 +3115,7 @@ class TestVifibSlapWebService(testVifibMixin):
def stepCheckSoftwareInstanceAndRelatedComputerPartition(self, def stepCheckSoftwareInstanceAndRelatedComputerPartition(self,
sequence, **kw): sequence, **kw):
self.stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingList(sequence, **kw) self.stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck(sequence, **kw)
self._checkSoftwareInstanceAndRelatedPartition(software_instance) self._checkSoftwareInstanceAndRelatedPartition(software_instance)
def stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck(self, def stepCheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck(self,
...@@ -8049,12 +8054,13 @@ class TestVifibSlapWebService(testVifibMixin): ...@@ -8049,12 +8054,13 @@ class TestVifibSlapWebService(testVifibMixin):
sequence_list.addSequenceString(sequence_string) sequence_list.addSequenceString(sequence_string)
sequence_list.play(self) sequence_list.play(self)
def stepDirectRequestComputerPartitionRaisesValueError(self, def stepDirectRequestComputerPartitionRaisesDisconnectedSoftwareTree(self,
sequence, **kw): sequence, **kw):
software_instance = self.portal.portal_catalog.getResultValue( software_instance = self.portal.portal_catalog.getResultValue(
uid = sequence['software_instance_uid']) uid = sequence['software_instance_uid'])
requested_reference = sequence['requested_reference'] requested_reference = sequence['requested_reference']
self.assertRaises(ValueError, from erp5.document.SoftwareInstance import DisconnectedSoftwareTree
self.assertRaises(DisconnectedSoftwareTree,
software_instance.requestSoftwareInstance, software_instance.requestSoftwareInstance,
software_release=sequence['software_release_uri'], software_release=sequence['software_release_uri'],
software_type=sequence['requested_reference'], software_type=sequence['requested_reference'],
...@@ -8192,12 +8198,502 @@ class TestVifibSlapWebService(testVifibMixin): ...@@ -8192,12 +8198,502 @@ class TestVifibSlapWebService(testVifibMixin):
# Try to: from C request B and prove that it raises # Try to: from C request B and prove that it raises
SelectRequestedReferenceB SelectRequestedReferenceB
LoginDefaultUser # login as superuser in order to work in erp5
DirectRequestComputerPartitionRaisesDisconnectedSoftwareTree
"""
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def stepDirectRequestComputerPartitionRaisesCyclicSoftwareTree(self,
sequence, **kw):
software_instance = self.portal.portal_catalog.getResultValue(
uid = sequence['software_instance_uid'])
requested_reference = sequence['requested_reference']
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree,
software_instance.requestSoftwareInstance,
software_release=sequence['software_release_uri'],
software_type=sequence['requested_reference'],
partition_reference=sequence['requested_reference'],
shared=False,
instance_xml=self.minimal_correct_xml,
sla_xml=self.minimal_correct_xml,
state='started'
)
def test_bug_cyclic_software_instance(self):
"""Check that no cyclic Software Instance trees would be created
In below scenario system shall behave like mentioned:
OpenOrder.request(SR, A) | SR(A)
A.request(SR, B) | SR(A) <- SR(B)
B.request(SR, A) | SR(A) <- SR(B) <- SR(C)
C.request(SR, B) raises immediately, because the result would be:
SR(A)
SR(B) <-> SR(C)
so B and C would be cyclic
"""
# Setup sufficient amount of CP
self.computer_partition_amount = 3
sequence_list = SequenceList()
sequence_string = """
# Prepare software release
LoginTestVifibDeveloper
SelectNewSoftwareReleaseUri
CreateSoftwareRelease
Tic
SubmitSoftwareRelease
Tic
CreateSoftwareProduct
Tic
ValidateSoftwareProduct
Tic
SetSoftwareProductToSoftwareRelease
PublishByActionSoftwareRelease
Logout
# Create the computer
LoginTestVifibAdmin
CreateComputer
Tic
Logout
SlapLoginCurrentComputer
FormatComputer
Tic
SlapLogout
StoreCurrentComputerReferenceBufferA
StoreCurrentComputerUidBufferA
# Install the software release
LoginTestVifibAdmin
RequestSoftwareInstallation
Tic
Logout
SlapLoginCurrentComputer
ComputerSoftwareReleaseAvailable
Tic
SlapLogout
# Create Software Instance A (originates from Open Order)
LoginTestVifibCustomer
PersonRequestSoftwareInstance
Tic
Logout
LoginDefaultUser
ConfirmOrderedSaleOrderActiveSense
Tic
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
LoginDefaultUser
CheckComputerPartitionInstanceSetupSalePackingListConfirmed
Logout
# From root request B
SelectRequestedReferenceB
SlapLoginCurrentSoftwareInstance
RequestComputerPartitionNotReadyResponse
Tic
SlapLogout
SlapLoginCurrentSoftwareInstance
RequestComputerPartition
Tic
SlapLogout
LoginDefaultUser
CheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck
CheckRequestedSoftwareInstanceAndRelatedComputerPartition
Logout
LoginDefaultUser
SetCurrentSoftwareInstanceRequested
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
SlapLoginCurrentSoftwareInstance
CheckRequestedComputerPartitionCleanParameterList
Logout
# From B request C
SelectRequestedReferenceC
SlapLoginCurrentSoftwareInstance
RequestComputerPartitionNotReadyResponse
Tic
SlapLogout
SlapLoginCurrentSoftwareInstance
RequestComputerPartition
Tic
SlapLogout
LoginDefaultUser
CheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck
CheckRequestedSoftwareInstanceAndRelatedComputerPartition
Logout
LoginDefaultUser
SetCurrentSoftwareInstanceRequested
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
SlapLoginCurrentSoftwareInstance
CheckRequestedComputerPartitionCleanParameterList
Logout
# Try to: from C request B and prove that it raises
SelectRequestedReferenceB
LoginDefaultUser # login as superuser in order to work in erp5
DirectRequestComputerPartitionRaisesCyclicSoftwareTree
"""
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def stepDirectRequestComputerPartitionRaisesValueError(self,
sequence, **kw):
software_instance = self.portal.portal_catalog.getResultValue(
uid = sequence['software_instance_uid'])
requested_reference = sequence['requested_reference']
self.assertRaises(ValueError,
software_instance.requestSoftwareInstance,
software_release=sequence['software_release_uri'],
software_type=sequence['requested_reference'],
partition_reference=sequence['requested_reference'],
shared=False,
instance_xml=self.minimal_correct_xml,
sla_xml=self.minimal_correct_xml,
state='started'
)
def test_bug_cyclic_software_instance_small_tree(self):
"""Check that no cyclic Software Instance trees would be created
In below scenario system shall behave like mentioned:
OpenOrder.request(SR, A) | SR(A)
A.request(SR, B) | SR(A) <- SR(B)
B.request(SR, A) raises immediately, because the result would be:
SR(A) <-> SR(B)
so B and A would be cyclic
"""
# Setup sufficient amount of CP
self.computer_partition_amount = 2
sequence_list = SequenceList()
sequence_string = """
# Prepare software release
LoginTestVifibDeveloper
SelectNewSoftwareReleaseUri
CreateSoftwareRelease
Tic
SubmitSoftwareRelease
Tic
CreateSoftwareProduct
Tic
ValidateSoftwareProduct
Tic
SetSoftwareProductToSoftwareRelease
PublishByActionSoftwareRelease
Logout
# Create the computer
LoginTestVifibAdmin
CreateComputer
Tic
Logout
SlapLoginCurrentComputer
FormatComputer
Tic
SlapLogout
StoreCurrentComputerReferenceBufferA
StoreCurrentComputerUidBufferA
# Install the software release
LoginTestVifibAdmin
RequestSoftwareInstallation
Tic
Logout
SlapLoginCurrentComputer
ComputerSoftwareReleaseAvailable
Tic
SlapLogout
# Create Software Instance A (originates from Open Order)
LoginTestVifibCustomer
PersonRequestSoftwareInstance
Tic
Logout
LoginDefaultUser
ConfirmOrderedSaleOrderActiveSense
Tic
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
LoginDefaultUser
CheckComputerPartitionInstanceSetupSalePackingListConfirmed
Logout
# From root request B
SelectRequestedReferenceB
SlapLoginCurrentSoftwareInstance
RequestComputerPartitionNotReadyResponse
Tic
SlapLogout
SlapLoginCurrentSoftwareInstance
RequestComputerPartition
Tic
SlapLogout
LoginDefaultUser
CheckSoftwareInstanceAndRelatedComputerPartitionNoPackingListCheck
CheckRequestedSoftwareInstanceAndRelatedComputerPartition
Logout
LoginDefaultUser
SetCurrentSoftwareInstanceRequested
SetSelectedComputerPartition
SelectCurrentlyUsedSalePackingListUid
Logout
SlapLoginCurrentSoftwareInstance
CheckRequestedComputerPartitionCleanParameterList
Logout
# Try to: From B request root
SelectRequestedReferenceRootSoftwareInstanceTitle
LoginDefaultUser # login as superuser in order to work in erp5 LoginDefaultUser # login as superuser in order to work in erp5
DirectRequestComputerPartitionRaisesValueError DirectRequestComputerPartitionRaisesValueError
""" """
sequence_list.addSequenceString(sequence_string) sequence_list.addSequenceString(sequence_string)
sequence_list.play(self) sequence_list.play(self)
########################################
# Software Instance graph helpers
########################################
def _test_si_tree(self):
software_instance = self.portal.software_instance_module.newContent(
portal_type='Software Instance')
self.checkConnected = software_instance.checkConnected
self.checkNotCyclic = software_instance.checkNotCyclic
def test_si_tree_simple_connected(self):
"""Graph of one element is connected
A
"""
self._test_si_tree()
graph = {'A': []}
root = 'A'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_simple_list_connected(self):
"""Graph of list is connected
B->C->A
"""
self._test_si_tree()
graph = {'A': [], 'B': ['C'], 'C': ['A']}
root = 'B'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_complex_connected(self):
"""Tree is connected
B --> A
\-> C --> D
\-> E --> F
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'C'],
'C': ['D', 'E'],
'D': [],
'E': ['F'],
'F': [],
}
root = 'B'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_simple_list_disconnected(self):
"""Two lists are disconnected
A->B
C
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': [], 'C': []}
root = 'A'
from erp5.document.SoftwareInstance import DisconnectedSoftwareTree
self.assertRaises(DisconnectedSoftwareTree, self.checkConnected, graph,
root)
# For now limitation of implementation gives false positive
@expectedFailure
def test_si_tree_cyclic_connected(self):
"""Cyclic is connected
A<->B
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': ['A']}
root = 'B'
self.assertEqual(True, self.checkConnected(graph, root))
def test_si_tree_cyclic_disconnected(self):
"""Two trees, where one is cyclic are disconnected
B --> A
\-> H
C --> D --> G
^ \-> E --> F \
\------------/
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'H'],
'C': ['D', 'E'],
'D': ['G'],
'E': ['F'],
'F': ['C'],
'G': [],
'H': [],
}
root = 'B'
from erp5.document.SoftwareInstance import DisconnectedSoftwareTree
self.assertRaises(DisconnectedSoftwareTree, self.checkConnected, graph,
root)
def test_si_tree_simple_not_cyclic(self):
"""Graph of one element is not cyclic
A
"""
self._test_si_tree()
graph = {'A': []}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_simple_list_not_cyclic(self):
"""Graph of list is not cyclic
B->C->A
"""
self._test_si_tree()
graph = {'A': [], 'B': ['C'], 'C': ['A']}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_simple_list_cyclic(self):
"""Graph of cyclic list is cyclic
B->C->A-\
^-------/
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': ['C'], 'C': ['A']}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_simple_list_cyclic(self):
"""Graph of cyclic list is cyclic
B->C->D->A-\
^-------/
"""
self._test_si_tree()
graph = {'A': ['C'], 'B': ['C'], 'C': ['D'], 'D': ['A']}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_complex_not_cyclic(self):
"""Tree is not cyclic
B --> A
\-> C --> D
\-> E --> F
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'C'],
'C': ['D', 'E'],
'D': [],
'E': ['F'],
'F': [],
}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_complex_cyclic(self):
"""Tree is not cyclic
B --> A
\-> C --> D
^ \-> E --> F -\
\-------------/
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'C'],
'C': ['D', 'E'],
'D': [],
'E': ['F'],
'F': ['C'],
}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_simple_list_disconnected_not_cyclic(self):
"""Two lists are disconnected
A->B
C
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': [], 'C': []}
self.assertEqual(True, self.checkNotCyclic(graph))
def test_si_tree_cyclic(self):
"""Cyclic is connected
A<->B
"""
self._test_si_tree()
graph = {'A': ['B'], 'B': ['A']}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
def test_si_tree_cyclic_disconnected_cyclic(self):
"""Two trees, where one is cyclic are disconnected
B --> A
\-> H
C --> D --> G
^ \-> E --> F \
\------------/
"""
self._test_si_tree()
graph = {
'A': [],
'B': ['A', 'H'],
'C': ['D', 'E'],
'D': ['G'],
'E': ['F'],
'F': ['C'],
'G': [],
'H': ['A'],
}
from erp5.document.SoftwareInstance import CyclicSoftwareTree
self.assertRaises(CyclicSoftwareTree, self.checkNotCyclic, graph)
######################################## ########################################
# Other tests # Other tests
######################################## ########################################
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment