Commit ec92a0c4 authored by Cédric de Saint Martin's avatar Cédric de Saint Martin Committed by Rafael Monnerat

[slapos.core] Add retention_delay support.

parent abe4ec1f
...@@ -37,6 +37,7 @@ import subprocess ...@@ -37,6 +37,7 @@ import subprocess
import tarfile import tarfile
import tempfile import tempfile
import textwrap import textwrap
import time
import xmlrpclib import xmlrpclib
from supervisor import xmlrpc from supervisor import xmlrpc
...@@ -273,6 +274,8 @@ class Software(object): ...@@ -273,6 +274,8 @@ class Software(object):
class Partition(object): class Partition(object):
"""This class is responsible of the installation of an instance """This class is responsible of the installation of an instance
""" """
retention_lock_delay_filename = '.slapos-retention-lock-delay'
retention_lock_date_filename = '.slapos-retention-lock-date'
# XXX: we should give the url (or the "key") instead of the software_path # XXX: we should give the url (or the "key") instead of the software_path
# then compute the path from it, like in Software. # then compute the path from it, like in Software.
...@@ -289,6 +292,7 @@ class Partition(object): ...@@ -289,6 +292,7 @@ class Partition(object):
buildout, buildout,
logger, logger,
certificate_repository_path=None, certificate_repository_path=None,
retention_delay=0
): ):
"""Initialisation of class parameters""" """Initialisation of class parameters"""
self.buildout = buildout self.buildout = buildout
...@@ -315,6 +319,20 @@ class Partition(object): ...@@ -315,6 +319,20 @@ class Partition(object):
self.partition_id + '.crt') self.partition_id + '.crt')
self._updateCertificate() self._updateCertificate()
self.retention_delay = retention_delay
if type(self.retention_delay) not in (int, float) \
or self.retention_delay <= 0:
self.logger.warn('Retention delay value (%s) is not valid, ignoring.' \
% self.retention_delay)
self.retention_delay = 0
self.retention_lock_delay_file_path = os.path.join(
self.instance_path, self.retention_lock_delay_filename
)
self.retention_lock_date_file_path = os.path.join(
self.instance_path, self.retention_lock_date_filename
)
def _updateCertificate(self): def _updateCertificate(self):
try: try:
partition_certificate = self.computer_partition.getCertificate() partition_certificate = self.computer_partition.getCertificate()
...@@ -492,6 +510,7 @@ class Partition(object): ...@@ -492,6 +510,7 @@ class Partition(object):
buildout_binary=buildout_binary, buildout_binary=buildout_binary,
logger=self.logger) logger=self.logger)
self.generateSupervisorConfigurationFile() self.generateSupervisorConfigurationFile()
self.createRetentionLockDelay()
def generateSupervisorConfigurationFile(self): def generateSupervisorConfigurationFile(self):
""" """
...@@ -564,6 +583,11 @@ class Partition(object): ...@@ -564,6 +583,11 @@ class Partition(object):
""" """
self.logger.info("Destroying Computer Partition %s..." self.logger.info("Destroying Computer Partition %s..."
% self.computer_partition.getId()) % self.computer_partition.getId())
self.createRetentionLockDate()
if not self.checkRetentionIsAuthorized():
return False
# Launches "destroy" binary if exists # Launches "destroy" binary if exists
destroy_executable_location = os.path.join(self.instance_path, 'sbin', destroy_executable_location = os.path.join(self.instance_path, 'sbin',
'destroy') 'destroy')
...@@ -607,6 +631,8 @@ class Partition(object): ...@@ -607,6 +631,8 @@ class Partition(object):
except IOError as exc: except IOError as exc:
raise IOError("I/O error while freeing partition (%s): %s" % (self.instance_path, exc)) raise IOError("I/O error while freeing partition (%s): %s" % (self.instance_path, exc))
return True
def fetchInformations(self): def fetchInformations(self):
"""Fetch usage informations with buildout, returns it. """Fetch usage informations with buildout, returns it.
""" """
...@@ -649,3 +675,112 @@ class Partition(object): ...@@ -649,3 +675,112 @@ class Partition(object):
supervisor.addProcessGroup(gname) supervisor.addProcessGroup(gname)
self.logger.info('Updated %r' % gname) self.logger.info('Updated %r' % gname)
self.logger.debug('Supervisord updated') self.logger.debug('Supervisord updated')
def _set_ownership(self, path):
"""
If running as root: copy ownership of software_path to path
If not running as root: do nothing
"""
if os.getuid():
return
root_stat = os.stat(self.software_path)
path_stat = os.stat(path)
if (root_stat.st_uid != path_stat.st_uid or
root_stat.st_gid != path_stat.st_gid):
os.chown(path, root_stat.st_uid, root_stat.st_gid)
def checkRetentionIsAuthorized(self):
"""
Check if retention is authorized by checking retention lock delay or
retention lock date.
A retention lock delay is a delay which is:
* Defined by the user/machine who requested the instance
* Hardcoded the first time the instance is deployed, then is read-only
during the whole lifetime of the instance
* Triggered the first time the instance is requested to be destroyed
(retention will be ignored).
From this point, it is not possible to destroy the instance until the
delay is over.
* Accessible in read-only mode from the partition
A retention lock date is the date computed from (date of first
retention request + retention lock delay in days).
Example:
* User requests an instance with delay as 10 (days) to a SlapOS Master
* SlapOS Master transmits this information to the SlapOS Node (current code)
* SlapOS Node hardcodes this delay at first deployment
* User requests retention of instance
* SlapOS Node tries to destroy for the first time: it doesn't actually
destroy, but it triggers the creation of a retention lock date from
from the hardcoded delay. At this point it is not possible to
destroy instance until current date + 10 days.
* SlapOS Node continues to try to destroy: it doesn't do anything until
retention lock date is reached.
"""
retention_lock_date = self.getExistingRetentionLockDate()
now = time.time()
if not retention_lock_date:
if self.getExistingRetentionLockDelay() > 0:
self.logger.info('Impossible to destroy partition yet because of retention lock.')
return False
# Else: OK to destroy
else:
if now < retention_lock_date:
self.logger.info('Impossible to destroy partition yet because of retention lock.')
return False
# Else: OK to destroy
return True
def createRetentionLockDelay(self):
"""
Create a retention lock delay for the current partition.
If retention delay is not specified, create it wth "0" as value
"""
if os.path.exists(self.retention_lock_delay_file_path):
return
with open(self.retention_lock_delay_file_path, 'w') as delay_file_path:
delay_file_path.write(str(self.retention_delay))
self._set_ownership(self.retention_lock_delay_file_path)
def getExistingRetentionLockDelay(self):
"""
Return the retention lock delay of current partition (created at first
deployment) if exist.
Return -1 otherwise.
"""
retention_delay = -1
if os.path.exists(self.retention_lock_delay_file_path):
with open(self.retention_lock_delay_file_path) as delay_file_path:
retention_delay = float(delay_file_path.read())
return retention_delay
def createRetentionLockDate(self):
"""
If retention lock delay > 0:
Create a retention lock date for the current partition from the
retention lock delay.
Do nothing otherwise.
"""
if os.path.exists(self.retention_lock_date_file_path):
return
retention_delay = self.getExistingRetentionLockDelay()
if retention_delay <= 0:
return
now = int(time.time())
retention_date = now + retention_delay * 24 * 3600
with open(self.retention_lock_date_file_path, 'w') as date_file_path:
date_file_path.write(str(retention_date))
self._set_ownership(self.retention_lock_date_file_path)
def getExistingRetentionLockDate(self):
"""
Return the retention lock delay of current partition if exist.
Return None otherwise.
"""
if os.path.exists(self.retention_lock_date_file_path):
with open(self.retention_lock_date_file_path) as date_file_path:
return float(date_file_path.read())
else:
return None
...@@ -662,7 +662,8 @@ class Slapgrid(object): ...@@ -662,7 +662,8 @@ class Slapgrid(object):
software_release_url=software_url, software_release_url=software_url,
certificate_repository_path=self.certificate_repository_path, certificate_repository_path=self.certificate_repository_path,
buildout=self.buildout, buildout=self.buildout,
logger=self.logger) logger=self.logger,
retention_delay=getattr(computer_partition, '_retention_delay', 0))
computer_partition_state = computer_partition.getState() computer_partition_state = computer_partition.getState()
# XXX this line breaks 37 tests # XXX this line breaks 37 tests
...@@ -1088,7 +1089,8 @@ class Slapgrid(object): ...@@ -1088,7 +1089,8 @@ class Slapgrid(object):
software_release_url=software_url, software_release_url=software_url,
certificate_repository_path=self.certificate_repository_path, certificate_repository_path=self.certificate_repository_path,
buildout=self.buildout, buildout=self.buildout,
logger=self.logger) logger=self.logger,
retention_delay=getattr(computer_partition, '_retention_delay', 0))
local_partition.stop() local_partition.stop()
try: try:
computer_partition.stopped() computer_partition.stopped()
...@@ -1101,7 +1103,7 @@ class Slapgrid(object): ...@@ -1101,7 +1103,7 @@ class Slapgrid(object):
self.logger.info('Ignoring destruction of %r, as no report usage was sent' % self.logger.info('Ignoring destruction of %r, as no report usage was sent' %
computer_partition.getId()) computer_partition.getId())
continue continue
local_partition.destroy() destroyed = local_partition.destroy()
except (SystemExit, KeyboardInterrupt): except (SystemExit, KeyboardInterrupt):
computer_partition.error(traceback.format_exc(), logger=self.logger) computer_partition.error(traceback.format_exc(), logger=self.logger)
raise raise
...@@ -1111,7 +1113,8 @@ class Slapgrid(object): ...@@ -1111,7 +1113,8 @@ class Slapgrid(object):
exc = traceback.format_exc() exc = traceback.format_exc()
computer_partition.error(exc, logger=self.logger) computer_partition.error(exc, logger=self.logger)
try: try:
computer_partition.destroyed() if destroyed:
computer_partition.destroyed()
except NotFoundError: except NotFoundError:
self.logger.debug('Ignored slap error while trying to inform about ' self.logger.debug('Ignored slap error while trying to inform about '
'destroying not fully configured Computer Partition %r' % 'destroying not fully configured Computer Partition %r' %
......
This diff is collapsed.
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
import logging import logging
import os import os
import time
import unittest import unittest
from slapos.slap import ComputerPartition as SlapComputerPartition from slapos.slap import ComputerPartition as SlapComputerPartition
...@@ -125,7 +126,8 @@ class MasterMixin(BasicMixin, unittest.TestCase): ...@@ -125,7 +126,8 @@ class MasterMixin(BasicMixin, unittest.TestCase):
self, self,
software_release_url, software_release_url,
partition_id=None, partition_id=None,
slap_computer_partition=None slap_computer_partition=None,
retention_delay=None,
): ):
""" """
Create a partition, and return a Partition object created Create a partition, and return a Partition object created
...@@ -153,10 +155,11 @@ class MasterMixin(BasicMixin, unittest.TestCase): ...@@ -153,10 +155,11 @@ class MasterMixin(BasicMixin, unittest.TestCase):
self.instance_root, 'supervisor') self.instance_root, 'supervisor')
os.mkdir(supervisor_configuration_path) os.mkdir(supervisor_configuration_path)
return Partition( partition = Partition(
software_path=software_path, software_path=software_path,
instance_path=instance_path, instance_path=instance_path,
supervisord_partition_configuration_path=supervisor_configuration_path, supervisord_partition_configuration_path=os.path.join(
supervisor_configuration_path, partition_id),
supervisord_socket=os.path.join( supervisord_socket=os.path.join(
supervisor_configuration_path, 'supervisor.sock'), supervisor_configuration_path, 'supervisor.sock'),
computer_partition=slap_computer_partition, computer_partition=slap_computer_partition,
...@@ -168,6 +171,11 @@ class MasterMixin(BasicMixin, unittest.TestCase): ...@@ -168,6 +171,11 @@ class MasterMixin(BasicMixin, unittest.TestCase):
logger=logging.getLogger(), logger=logging.getLogger(),
) )
partition.updateSupervisor = FakeCallAndNoop
if retention_delay:
partition.retention_delay = retention_delay
return partition
class TestSoftwareNetworkCacheSlapObject(MasterMixin, unittest.TestCase): class TestSoftwareNetworkCacheSlapObject(MasterMixin, unittest.TestCase):
""" """
...@@ -382,3 +390,87 @@ class TestPartitionSlapObject(MasterMixin, unittest.TestCase): ...@@ -382,3 +390,87 @@ class TestPartitionSlapObject(MasterMixin, unittest.TestCase):
# XXX: What should it raise? # XXX: What should it raise?
self.assertRaises(IOError, partition.install) self.assertRaises(IOError, partition.install)
class TestPartitionDestructionLock(MasterMixin, unittest.TestCase):
def setUp(self):
MasterMixin.setUp(self)
Partition.generateSupervisorConfigurationFile = FakeCallAndNoop()
utils.bootstrapBuildout = FakeCallAndNoop()
utils.launchBuildout = FakeCallAndStore()
def test_retention_lock_delay_creation(self):
delay = 42
software = self.createSoftware()
partition = self.createPartition(software.url, retention_delay=delay)
partition.install()
deployed_delay = int(open(partition.retention_lock_delay_file_path).read())
self.assertEqual(delay, deployed_delay)
def test_no_retention_lock_delay(self):
software = self.createSoftware()
partition = self.createPartition(software.url)
partition.install()
delay = open(partition.retention_lock_delay_file_path).read()
self.assertTrue(delay, '0')
self.assertTrue(partition.destroy())
def test_retention_lock_delay_does_not_change(self):
delay = 42
software = self.createSoftware()
partition = self.createPartition(software.url, retention_delay=delay)
partition.install()
partition.retention_delay = 23
# install/destroy many times
partition.install()
partition.destroy()
partition.destroy()
partition.install()
partition.destroy()
deployed_delay = int(open(partition.retention_lock_delay_file_path).read())
self.assertEqual(delay, deployed_delay)
def test_retention_lock_delay_is_respected(self):
delay = 2.0 / (3600 * 24)
software = self.createSoftware()
partition = self.createPartition(software.url, retention_delay=delay)
partition.install()
deployed_delay = float(open(partition.retention_lock_delay_file_path).read())
self.assertEqual(int(delay), int(deployed_delay))
self.assertFalse(partition.destroy())
time.sleep(1)
self.assertFalse(partition.destroy())
time.sleep(1)
self.assertTrue(partition.destroy())
def test_retention_lock_date_creation(self):
delay = 42
software = self.createSoftware()
partition = self.createPartition(software.url, retention_delay=delay)
partition.install()
self.assertFalse(os.path.exists(partition.retention_lock_date_file_path))
partition.destroy()
deployed_date = float(open(partition.retention_lock_date_file_path).read())
self.assertEqual(delay * 3600 * 24 + int(time.time()), int(deployed_date))
def test_retention_lock_date_does_not_change(self):
delay = 42
software = self.createSoftware()
partition = self.createPartition(software.url, retention_delay=delay)
now = time.time()
partition.install()
partition.destroy()
partition.retention_delay = 23
# install/destroy many times
partition.install()
partition.destroy()
partition.destroy()
partition.install()
partition.destroy()
deployed_date = float(open(partition.retention_lock_date_file_path).read())
self.assertEqual(delay * 3600 * 24 + int(now), int(deployed_date))
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