Commit 20a0f457 authored by Rafael Monnerat's avatar Rafael Monnerat

Disable Disconnection of instance on the graph

See merge request nexedi/slapos.core!591
parents c988e4a8 2ebb32ba
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Alarm" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>active_sense_method_id</string> </key>
<value> <string>Alarm_garbageCollectDestroyUnlinkedInstance</string> </value>
</item>
<item>
<key> <string>automatic_solve</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>enabled</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>slapos_garbage_collect_destroy_unlinked_instance</string> </value>
</item>
<item>
<key> <string>periodicity_hour</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_hour_frequency</string> </key>
<value> <int>1</int> </value>
</item>
<item>
<key> <string>periodicity_minute</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_minute_frequency</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>periodicity_month</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_month_day</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>periodicity_start_date</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="DateTime" module="DateTime.DateTime"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<tuple>
<float>15638400.0</float>
<string>GMT</string>
</tuple>
</state>
</object>
</value>
</item>
<item>
<key> <string>periodicity_week</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Alarm</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Garbage Collect Unlinked Instances</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
portal = context.getPortalObject()
from Products.ZSQLCatalog.SQLCatalog import SimpleQuery
portal.portal_catalog.searchAndActivate(
portal_type=["Software Instance", "Slave Instance"],
validation_state="validated",
specialise_validation_state="validated",
successor_related_uid=SimpleQuery(successor_related_uid=None, comparison_operator='is'),
method_id='SoftwareInstance_tryToGarbageUnlinkedInstance',
activate_kw={'tag': tag}
)
context.activate(after_tag=tag).getId()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>tag, fixit, params</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Alarm_garbageCollectDestroyUnlinkedInstance</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
from zExceptions import Unauthorized
from DateTime import DateTime
from erp5.component.module.DateUtils import addToDate
if REQUEST is not None:
raise Unauthorized
instance = context
def checkInstanceTree(instance_list):
"""
Check if successor link is really removed to this instance
"""
sub_instance_list = []
if instance_list == []:
return
for item in instance_list:
if item.getUid() == instance.getUid():
return item
sub_instance_list.extend(item.getSuccessorValueList())
return checkInstanceTree(sub_instance_list)
if instance.getSlapState() == "destroy_requested":
return
instance_tree = instance.getSpecialiseValue()
if instance_tree is None or \
instance_tree.getSlapState() == "destroy_requested":
return
root_instance = instance_tree.getSuccessorValue()
if root_instance is None:
# Refuse to destroy root instance
raise ValueError("Instance Tree %s has no root instance, this should "\
"not happen!!" % instance_tree.getRelativeUrl())
# If instance modificationDate is too recent, skip
# Delay destroy of unlinked instances
if instance.getModificationDate() - addToDate(DateTime(), {'minute': -1*delay_time}) > 0:
return
if checkInstanceTree([root_instance]) is None:
# This unlinked instance to parent should be removed
is_slave = False
if instance.getPortalType() == 'Slave Instance':
is_slave = True
promise_kw = {
'instance_xml': instance.getTextContent(),
'software_type': instance.getSourceReference(),
'sla_xml': instance.getSlaXml(),
'software_release': instance.getUrlString(),
'shared': is_slave,
}
instance.requestDestroy(**promise_kw)
# Unlink all children of this instance
instance.edit(successor="", comment="Destroyed garbage collector!")
return instance.getRelativeUrl()
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="_reconstructor" module="copy_reg"/>
</klass>
<tuple>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
<global name="object" module="__builtin__"/>
<none/>
</tuple>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>REQUEST=None, delay_time=50</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SoftwareInstance_tryToGarbageUnlinkedInstance</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
# Copyright (c) 2002-2012 Nexedi SA and Contributors. All Rights Reserved.
import transaction
from erp5.component.test.SlapOSTestCaseMixin import SlapOSTestCaseMixin
import time
from zExceptions import Unauthorized
from DateTime import DateTime
from erp5.component.module.DateUtils import addToDate
......@@ -848,227 +847,6 @@ class TestSlapOSGarbageCollectNonAllocatedRootTreeAlarm(SlapOSTestCaseMixin):
)
class TestSlapOSGarbageCollectUnlinkedInstanceAlarm(SlapOSTestCaseMixin):
def createInstance(self):
instance_tree = self.portal.instance_tree_module\
.template_instance_tree.Base_createCloneDocument(batch_mode=1)
instance_tree.validate()
instance_tree.edit(
title=self.generateNewSoftwareTitle(),
reference="TESTHS-%s" % self.generateNewId(),
)
request_kw = dict(
software_release=\
self.generateNewSoftwareReleaseUrl(),
software_type=self.generateNewSoftwareType(),
instance_xml=self.generateSafeXml(),
sla_xml=self.generateSafeXml(),
shared=False,
software_title=instance_tree.getTitle(),
state='started'
)
instance_tree.requestStart(**request_kw)
instance_tree.requestInstance(**request_kw)
self.instance_tree = instance_tree
instance = instance_tree.getSuccessorValue()
return instance
def createComputePartition(self):
compute_node = self.portal.compute_node_module\
.template_compute_node.Base_createCloneDocument(batch_mode=1)
compute_node.validate()
compute_node.edit(
title=self.generateNewSoftwareTitle(),
reference="TESTCOMP-%s" % self.generateNewId(),
)
partition = compute_node.newContent(portal_type="Compute Partition")
return partition
def doRequestInstance(self, instance, title, slave=False):
instance_kw = dict(software_release=self.generateNewSoftwareReleaseUrl(),
software_type=self.generateNewSoftwareType(),
instance_xml=self.generateSafeXml(),
sla_xml=self.generateSafeXml(),
shared=slave,
software_title=title,
state='started'
)
instance.requestInstance(**instance_kw)
self.tic()
sub_instance = instance.getSuccessorValue()
partition = self.createComputePartition()
sub_instance.edit(aggregate_value=partition)
self.tic()
self.assertEqual(self.instance_tree.getRelativeUrl(),
sub_instance.getSpecialise())
return sub_instance
def test_SoftwareInstance_tryToGarbageUnlinkedInstance(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
instance0 = self.doRequestInstance(instance, 'instance0')
self.assertEqual(instance0.getSuccessorRelatedTitle(), instance.getTitle())
# Remove successor link
instance.edit(successor_list=[])
self.tic()
self.assertEqual(instance0.getSuccessorRelatedTitle(), None)
instance0.SoftwareInstance_tryToGarbageUnlinkedInstance(delay_time=-1)
self.tic()
self.assertEqual(instance0.getSlapState(), 'destroy_requested')
def test_SoftwareInstance_tryToGarbageUnlinkedInstance_hosting_destroyed(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
instance0 = self.doRequestInstance(instance, 'instance0')
instance.edit(successor_list=[])
self.tic()
self.instance_tree.archive()
self.portal.portal_workflow._jumpToStateFor(self.instance_tree,
'destroy_requested')
self.portal.portal_workflow._jumpToStateFor(instance, 'destroy_requested')
self.tic()
instance0.SoftwareInstance_tryToGarbageUnlinkedInstance()
self.tic()
# Will not be destroyed by this script
self.assertEqual(instance0.getSlapState(), 'start_requested')
def test_SoftwareInstance_tryToGarbageUnlinkedInstance_will_unlink_children(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
instance0 = self.doRequestInstance(instance, 'instance0')
instance_instance0 = self.doRequestInstance(instance0, 'Subinstance0')
self.assertEqual(instance_instance0.getSuccessorRelatedTitle(),
'instance0')
instance.edit(successor_list=[])
self.tic()
self.assertEqual(instance0.getSuccessorRelatedTitle(), None)
instance0.SoftwareInstance_tryToGarbageUnlinkedInstance(delay_time=-1)
self.tic()
self.assertEqual(instance0.getSlapState(), 'destroy_requested')
self.assertEqual(instance_instance0.getSlapState(), 'start_requested')
# Link of child removed
self.assertEqual(instance_instance0.getSuccessorRelatedTitle(), None)
def test_SoftwareInstance_tryToGarbageUnlinkedInstance_will_delay(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
instance0 = self.doRequestInstance(instance, 'instance0')
instance_instance0 = self.doRequestInstance(instance0, 'Subinstance0')
self.assertEqual(instance_instance0.getSuccessorRelatedTitle(),
'instance0')
instance.edit(successor_list=[])
self.tic()
self.assertEqual(instance0.getSuccessorRelatedTitle(), None)
instance0.SoftwareInstance_tryToGarbageUnlinkedInstance()
self.tic()
self.assertEqual(instance0.getSlapState(), 'start_requested')
self.assertEqual(instance_instance0.getSlapState(), 'start_requested')
# delay a bit
time.sleep(2)
# run with delay of 3 seconds
instance0.SoftwareInstance_tryToGarbageUnlinkedInstance(delay_time=3/60.0)
self.tic()
self.assertEqual(instance0.getSlapState(), 'destroy_requested')
self.assertEqual(instance_instance0.getSlapState(), 'start_requested')
# Link of child removed
self.assertEqual(instance_instance0.getSuccessorRelatedTitle(), None)
def test_SoftwareInstance_tryToGarbageUnlinkedInstance_unlinked_root(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
self.assertEqual(self.instance_tree.getTitle(), instance.getTitle())
# Remove successor link
self.instance_tree.edit(successor_list=[])
self.tic()
self.assertEqual(instance.getSuccessorRelatedTitle(), None)
# will not destroy
self.assertRaises(
ValueError,
instance.SoftwareInstance_tryToGarbageUnlinkedInstance,
delay_time=-10)
self.tic()
self.assertEqual(instance.getSlapState(), 'start_requested')
def test_SoftwareInstance_tryToGarbageUnlinkedInstance_not_unlinked(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
instance0 = self.doRequestInstance(instance, 'instance0')
instance_instance0 = self.doRequestInstance(instance0, 'Subinstance0')
self.assertEqual(instance_instance0.getSuccessorRelatedTitle(),
'instance0')
self.assertEqual(instance_instance0.getSlapState(), 'start_requested')
# Try to remove without delete successor link
instance_instance0.SoftwareInstance_tryToGarbageUnlinkedInstance(delay_time=-1)
self.tic()
self.assertEqual(instance_instance0.getSlapState(), 'start_requested')
def test_alarm_search_inlinked_instance(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
instance0 = self.doRequestInstance(instance, 'instance0')
self.assertEqual(instance.getSuccessorReference(),
instance0.getReference())
self._test_alarm_not_visited(
self.portal.portal_alarms.slapos_garbage_collect_destroy_unlinked_instance,
instance0,
'SoftwareInstance_tryToGarbageUnlinkedInstance'
)
# Remove successor link
instance.edit(successor_list=[])
self._test_alarm(
self.portal.portal_alarms.slapos_garbage_collect_destroy_unlinked_instance,
instance0,
'SoftwareInstance_tryToGarbageUnlinkedInstance'
)
def test_alarm_search_inlinked_instance_slave(self):
instance = self.createInstance()
partition = self.createComputePartition()
instance.edit(aggregate_value=partition)
self.tic()
slave_instance0 = self.doRequestInstance(instance, 'slaveInstance0', True)
self.assertEqual(instance.getSuccessorTitle(), 'slaveInstance0')
self._test_alarm_not_visited(
self.portal.portal_alarms.slapos_garbage_collect_destroy_unlinked_instance,
slave_instance0,
'SoftwareInstance_tryToGarbageUnlinkedInstance'
)
instance.edit(successor_list=[])
self._test_alarm(
self.portal.portal_alarms.slapos_garbage_collect_destroy_unlinked_instance,
slave_instance0,
'SoftwareInstance_tryToGarbageUnlinkedInstance'
)
class TestSlapOSInvalidateDestroyedInstance(SlapOSTestCaseMixin):
def createSoftwareInstance(self):
......
......@@ -19,6 +19,8 @@
#
##############################################################################
from erp5.component.test.SlapOSTestCaseMixin import SlapOSTestCaseMixin
from erp5.component.document.SoftwareInstance import SoftwareInstance, \
DisconnectedSoftwareTree, CyclicSoftwareTree
import transaction
from time import sleep
from zExceptions import Unauthorized
......@@ -408,6 +410,127 @@ class TestSlapOSCoreInstanceSlapInterfaceWorkflow(SlapOSTestCaseMixin):
self.assertSameSet(B_instance.getSuccessorList(),
[C_instance.getRelativeUrl()])
def test_request_tree_disconnected(self):
"""Checks tree change forced by request
For a tree like:
A
|
A
|\
B C
Them force C to be disconnected:
A
|
A
|
B C
When A requests C the request should fail.
"""
request_kw = self.request_kw.copy()
request_kw['software_title'] = self.generateNewSoftwareTitle()
self.software_instance.requestInstance(**request_kw)
B_instance = self.software_instance.REQUEST.get('request_instance')
request_kw['software_title'] = self.generateNewSoftwareTitle()
self.software_instance.requestInstance(**request_kw)
C_instance = self.software_instance.REQUEST.get('request_instance')
self.assertSameSet(
self.software_instance.getSuccessorList(),
[B_instance.getRelativeUrl(), C_instance.getRelativeUrl()])
self.tic() # in order to recalculate tree
self.software_instance.setSuccessorList([B_instance.getRelativeUrl()])
self.tic() # in order to recalculate tree
self.assertRaises(DisconnectedSoftwareTree, self.software_instance.requestInstance, **request_kw)
# Just re-set sucessor fixes the problem
self.software_instance.setSuccessorList([B_instance.getRelativeUrl(), C_instance.getRelativeUrl()])
self.tic() # in order to recalculate tree
self.software_instance.requestInstance(**request_kw)
C1_instance = self.software_instance.REQUEST.get('request_instance')
self.assertEqual(C_instance.getRelativeUrl(), C1_instance.getRelativeUrl())
def test_request_check_cycle(self):
"""Checks tree change forced by request
For a tree like:
A
|
A
|
B
|
C
|
D
Them force D to request A to be cycled (dupplicated successor related):
A
|
A
|\
B D
|/
C
In normal conditions there is no raise because either DisconnectedSoftwareTree is raised before,
so we hot patch the checkConnected to ensure we get the proper raise
"""
request_kw = self.request_kw.copy()
request_kw['software_title'] = self.generateNewSoftwareTitle()
self.software_instance.requestInstance(**request_kw)
B_instance = self.software_instance.REQUEST.get('request_instance')
request_kw['software_title'] = self.generateNewSoftwareTitle()
B_instance.requestInstance(**request_kw)
C_instance = self.software_instance.REQUEST.get('request_instance')
request_kw['software_title'] = self.generateNewSoftwareTitle()
C_instance.requestInstance(**request_kw)
D_instance = self.software_instance.REQUEST.get('request_instance')
self.assertSameSet(
self.software_instance.getSuccessorList(),
[B_instance.getRelativeUrl()])
self.assertSameSet(
B_instance.getSuccessorList(), [C_instance.getRelativeUrl()])
self.assertSameSet(
C_instance.getSuccessorList(), [D_instance.getRelativeUrl()])
self.tic() # in order to recalculate tree
def checkConnected(self, graph, root):
"Patch and return skip"
return
checkConnected_original = SoftwareInstance.checkConnected
try:
SoftwareInstance.checkConnected = checkConnected
request_kw['software_title'] = B_instance.getTitle()
self.assertRaises(CyclicSoftwareTree, D_instance.requestInstance, **request_kw)
finally:
SoftwareInstance.checkConnected = checkConnected_original
def test_request_tree_change_not_indexed(self):
"""Checks tree change forced by request
......
......@@ -12,7 +12,6 @@ portal_alarms/slapos_allocate_instance
portal_alarms/slapos_assert_instance_tree_successor
portal_alarms/slapos_cloud_invalidate_destroyed_instance
portal_alarms/slapos_free_compute_partition
portal_alarms/slapos_garbage_collect_destroy_unlinked_instance
portal_alarms/slapos_garbage_collect_destroyed_root_tree
portal_alarms/slapos_garbage_collect_non_allocated_root_tree
portal_alarms/slapos_stop_collect_instance
......
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