Merge pull request #22963 from garethgreenaway/verify_schedule_functions_with_events

Additions to schedule module
This commit is contained in:
Thomas S Hatch 2015-04-23 21:45:25 -06:00
commit 9a1a55850a
4 changed files with 259 additions and 83 deletions

View File

@ -131,14 +131,27 @@ def purge(**kwargs):
continue
if 'test' in kwargs and kwargs['test']:
ret['result'] = True
ret['comment'].append('Job: {0} would be deleted from schedule.'.format(name))
else:
out = __salt__['event.fire']({'name': name, 'func': 'delete'}, 'manage_schedule')
if out:
ret['comment'].append('Deleted job: {0} from schedule.'.format(name))
else:
ret['comment'].append('Failed to delete job {0} from schedule.'.format(name))
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'func': 'delete'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_delete_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
if name not in schedule:
ret['result'] = True
ret['comment'].append('Deleted job: {0} from schedule.'.format(name))
else:
ret['comment'].append('Failed to delete job {0} from schedule.'.format(name))
ret['result'] = True
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule add failed.'
ret['result'] = True
return ret
@ -153,36 +166,55 @@ def delete(name, **kwargs):
salt '*' schedule.delete job1
'''
ret = {'comment': [],
'result': True}
ret = {'comment': 'Failed to delete job {0} from schedule.'.format(name),
'result': False}
if not name:
ret['comment'] = 'Job name is required.'
ret['result'] = False
if name in __opts__['schedule']:
if 'test' in kwargs and kwargs['test']:
ret['comment'] = 'Job: {0} would be deleted from schedule.'.format(name)
ret['result'] = True
else:
out = __salt__['event.fire']({'name': name, 'func': 'delete'}, 'manage_schedule')
if out:
ret['comment'] = 'Deleted Job {0} from schedule.'.format(name)
else:
ret['comment'] = 'Failed to delete job {0} from schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'func': 'delete'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_delete_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
if name not in schedule:
ret['result'] = True
ret['comment'] = 'Deleted Job {0} from schedule.'.format(name)
else:
ret['comment'] = 'Failed to delete job {0} from schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule add failed.'
elif 'schedule' in __pillar__ and name in __pillar__['schedule']:
if 'test' in kwargs and kwargs['test']:
ret['comment'] = 'Job: {0} would be deleted from schedule.'.format(name)
else:
out = __salt__['event.fire']({'name': name, 'where': 'pillar', 'func': 'delete'}, 'manage_schedule')
if out:
ret['comment'] = 'Deleted Job {0} from schedule.'.format(name)
else:
ret['comment'] = 'Failed to delete job {0} from schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'where': 'pillar', 'func': 'delete'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_delete_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
if name not in schedule:
ret['result'] = True
ret['comment'] = 'Deleted Job {0} from schedule.'.format(name)
else:
ret['comment'] = 'Failed to delete job {0} from schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule add failed.'
else:
ret['comment'] = 'Job {0} does not exist.'.format(name)
ret['result'] = False
return ret
@ -286,8 +318,8 @@ def add(name, **kwargs):
salt '*' schedule.add job2 function='cmd.run' job_args=['date >> /tmp/date.log'] seconds=60
'''
ret = {'comment': [],
'result': True}
ret = {'comment': 'Failed to add job {0} to schedule.'.format(name),
'result': False}
current_schedule = __opts__['schedule'].copy()
if 'schedule' in __pillar__:
@ -310,29 +342,36 @@ def add(name, **kwargs):
time_conflict = True
if time_conflict:
ret['result'] = False
ret['comment'] = 'Error: Unable to use "seconds", "minutes", "hours", or "days" with "when" or "cron" options.'
return ret
if 'when' in kwargs and 'cron' in kwargs:
ret['result'] = False
ret['comment'] = 'Unable to use "when" and "cron" options together. Ignoring.'
return ret
_new = build_schedule_item(name, **kwargs)
schedule = {}
schedule[name] = _new
schedule_data = {}
schedule_data[name] = _new
if 'test' in kwargs and kwargs['test']:
ret['comment'] = 'Job: {0} would be added to schedule.'.format(name)
ret['result'] = True
else:
out = __salt__['event.fire']({'name': name, 'schedule': schedule, 'func': 'add'}, 'manage_schedule')
if out:
ret['comment'] = 'Added job: {0} to schedule.'.format(name)
else:
ret['comment'] = 'Failed to modify job {0} to schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'schedule': schedule_data, 'func': 'add'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_add_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
if name in schedule:
ret['result'] = True
ret['comment'] = 'Added job: {0} to schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule add failed.'
return ret
@ -490,22 +529,46 @@ def enable_job(name, **kwargs):
if 'test' in __opts__ and __opts__['test']:
ret['comment'] = 'Job: {0} would be enabled in schedule.'.format(name)
else:
out = __salt__['event.fire']({'name': name, 'func': 'enable_job'}, 'manage_schedule')
if out:
ret['comment'] = 'Enabled Job {0} in schedule.'.format(name)
else:
ret['comment'] = 'Failed to enable job {0} from schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'func': 'enable_job'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_enabled_job_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
# check item exists in schedule and is enabled
if name in schedule and schedule[name]['enabled']:
ret['result'] = True
ret['comment'] = 'Enabled Job {0} in schedule.'.format(name)
else:
ret['result'] = False
ret['comment'] = 'Failed to enable job {0} in schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule enable job failed.'
elif 'schedule' in __pillar__ and name in __pillar__['schedule']:
if 'test' in kwargs and kwargs['test']:
ret['comment'].append('Job: {0} would be enabled in schedule.'.format(name))
else:
out = __salt__['event.fire']({'name': name, 'where': 'pillar', 'func': 'enable_job'}, 'manage_schedule')
if out:
ret['comment'] = 'Enabled Job {0} in schedule.'.format(name)
else:
ret['comment'] = 'Failed to enable job {0} from schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'where': 'pillar', 'func': 'enable_job'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_enabled_job_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
# check item exists in schedule and is enabled
if name in schedule and schedule[name]['enabled']:
ret['result'] = True
ret['comment'] = 'Enabled Job {0} in schedule.'.format(name)
else:
ret['result'] = False
ret['comment'] = 'Failed to enable job {0} in schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule enable job failed.'
else:
ret['comment'] = 'Job {0} does not exist.'.format(name)
ret['result'] = False
@ -534,22 +597,46 @@ def disable_job(name, **kwargs):
if 'test' in kwargs and kwargs['test']:
ret['comment'] = 'Job: {0} would be disabled in schedule.'.format(name)
else:
out = __salt__['event.fire']({'name': name, 'func': 'disable_job'}, 'manage_schedule')
if out:
ret['comment'] = 'Disabled Job {0} in schedule.'.format(name)
else:
ret['comment'] = 'Failed to disable job {0} from schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'func': 'disable_job'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_disabled_job_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
# check item exists in schedule and is enabled
if name in schedule and not schedule[name]['enabled']:
ret['result'] = True
ret['comment'] = 'Disabled Job {0} in schedule.'.format(name)
else:
ret['result'] = False
ret['comment'] = 'Failed to disable job {0} in schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule enable job failed.'
elif 'schedule' in __pillar__ and name in __pillar__['schedule']:
if 'test' in kwargs and kwargs['test']:
ret['comment'].append('Job: {0} would be disabled in schedule.'.format(name))
else:
out = __salt__['event.fire']({'name': name, 'where': 'pillar', 'func': 'disable_job'}, 'manage_schedule')
if out:
ret['comment'] = 'Disabled Job {0} in schedule.'.format(name)
else:
ret['comment'] = 'Failed to disable job {0} from schedule.'.format(name)
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'name': name, 'where': 'pillar', 'func': 'disable_job'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_disabled_job_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
# check item exists in schedule and is enabled
if name in schedule and not schedule[name]['enabled']:
ret['result'] = True
ret['comment'] = 'Disabled Job {0} in schedule.'.format(name)
else:
ret['result'] = False
ret['comment'] = 'Failed to disable job {0} in schedule.'.format(name)
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule enable job failed.'
else:
ret['comment'] = 'Job {0} does not exist.'.format(name)
ret['result'] = False
@ -607,12 +694,23 @@ def enable(**kwargs):
if 'test' in kwargs and kwargs['test']:
ret['comment'] = 'Schedule would be enabled.'
else:
out = __salt__['event.fire']({'func': 'enable'}, 'manage_schedule')
if out:
ret['comment'] = 'Enabled schedule on minion.'
else:
ret['comment'] = 'Failed to enable schedule on minion.'
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'func': 'enable'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_enabled_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
if 'enabled' in schedule and schedule['enabled']:
ret['result'] = True
ret['comment'] = 'Enabled schedule on minion.'
else:
ret['result'] = False
ret['comment'] = 'Failed to enable schedule on minion.'
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule enable job failed.'
return ret
@ -633,12 +731,23 @@ def disable(**kwargs):
if 'test' in kwargs and kwargs['test']:
ret['comment'] = 'Schedule would be disabled.'
else:
out = __salt__['event.fire']({'func': 'disable'}, 'manage_schedule')
if out:
ret['comment'] = 'Disabled schedule on minion.'
else:
ret['comment'] = 'Failed to disable schedule on minion.'
ret['result'] = False
try:
eventer = salt.utils.event.get_event('minion', opts=__opts__)
res = __salt__['event.fire']({'func': 'disable'}, 'manage_schedule')
if res:
event_ret = eventer.get_event(tag='/salt/minion/minion_schedule_disabled_complete', wait=30)
if event_ret and event_ret['complete']:
schedule = event_ret['schedule']
if 'enabled' in schedule and not schedule['enabled']:
ret['result'] = True
ret['comment'] = 'Disabled schedule on minion.'
else:
ret['result'] = False
ret['comment'] = 'Failed to disable schedule on minion.'
return ret
except KeyError:
# Effectively a no-op, since we can't really return without an event system
ret['comment'] = 'Event module not available. Schedule enable job failed.'
return ret

View File

@ -336,11 +336,18 @@ class Schedule(object):
# ensure job exists, then delete it
if name in self.opts['schedule']:
del self.opts['schedule'][name]
schedule = self.opts['schedule']
else:
# If job is in pillar, delete it there too
if 'schedule' in self.opts['pillar']:
if name in self.opts['pillar']['schedule']:
del self.opts['pillar']['schedule'][name]
schedule = self.opts['pillar']['schedule']
# Fire the complete event back along with updated list of schedule
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': True, 'schedule': schedule},
tag='/salt/minion/minion_schedule_delete_complete')
# remove from self.intervals
if name in self.intervals:
@ -367,7 +374,14 @@ class Schedule(object):
'job: {0}'.format(new_job))
else:
log.info('Added new job {0} to scheduler'.format(new_job))
self.opts['schedule'].update(data)
# Fire the complete event back along with updated list of schedule
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': True, 'schedule': self.opts['schedule']},
tag='/salt/minion/minion_schedule_add_complete')
self.persist()
def enable_job(self, name, where=None):
@ -376,8 +390,16 @@ class Schedule(object):
'''
if where == 'pillar':
self.opts['pillar']['schedule'][name]['enabled'] = True
schedule = self.opts['pillar']['schedule']
else:
self.opts['schedule'][name]['enabled'] = True
schedule = self.opts['schedule']
# Fire the complete event back along with updated list of schedule
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': True, 'schedule': schedule},
tag='/salt/minion/minion_schedule_enabled_job_complete')
log.info('Enabling job {0} in scheduler'.format(name))
def disable_job(self, name, where=None):
@ -386,8 +408,16 @@ class Schedule(object):
'''
if where == 'pillar':
self.opts['pillar']['schedule'][name]['enabled'] = False
schedule = self.opts['pillar']['schedule']
else:
self.opts['schedule'][name]['enabled'] = False
schedule = self.opts['schedule']
# Fire the complete event back along with updated list of schedule
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': True, 'schedule': schedule},
tag='/salt/minion/minion_schedule_disabled_job_complete')
log.info('Disabling job {0} in scheduler'.format(name))
def modify_job(self, name, schedule, where=None):
@ -447,12 +477,22 @@ class Schedule(object):
'''
self.opts['schedule']['enabled'] = True
# Fire the complete event back along with updated list of schedule
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': True, 'schedule': self.opts['schedule']},
tag='/salt/minion/minion_schedule_enabled_complete')
def disable_schedule(self):
'''
Disable the scheduler.
'''
self.opts['schedule']['enabled'] = False
# Fire the complete event back along with updated list of schedule
evt = salt.utils.event.get_event('minion', opts=self.opts)
evt.fire_event({'complete': True, 'schedule': self.opts['schedule']},
tag='/salt/minion/minion_schedule_disabled_complete')
def reload(self, schedule):
'''
Reload the schedule from saved schedule file.

View File

@ -17,6 +17,10 @@ from salttesting.mock import (
from salttesting.helpers import ensure_in_syspath
import os
import integration
SOCK_DIR = os.path.join(integration.TMP, 'test-socks')
ensure_in_syspath('../../')
# Import Salt Libs
@ -250,7 +254,7 @@ class ScheduleTestCase(TestCase):
comm1 = 'no servers answered the published schedule.add command'
comm2 = 'the following minions return False'
comm3 = 'Moved Job job1 from schedule.'
with patch.dict(schedule.__opts__, {'schedule': {'job1': JOB1}}):
with patch.dict(schedule.__opts__, {'schedule': {'job1': JOB1}, 'sock_dir': SOCK_DIR}):
mock = MagicMock(return_value={})
with patch.dict(schedule.__salt__, {'publish.publish': mock}):
self.assertDictEqual(schedule.move('job1', 'minion1'),
@ -276,7 +280,7 @@ class ScheduleTestCase(TestCase):
'result': False})
mock = MagicMock(side_effect=[{}, {'job1': {}}])
with patch.dict(schedule.__opts__, {'schedule': mock}):
with patch.dict(schedule.__opts__, {'schedule': mock, 'sock_dir': SOCK_DIR}):
with patch.dict(schedule.__pillar__, {'schedule': {'job1': JOB1}}):
mock = MagicMock(return_value={})
with patch.dict(schedule.__salt__, {'publish.publish': mock}):

View File

@ -5,6 +5,7 @@
# Import python libs
from __future__ import absolute_import
import os
# Import Salt Libs
from salt.utils.schedule import Schedule
@ -14,7 +15,10 @@ from salttesting import skipIf, TestCase
from salttesting.mock import MagicMock, patch, NO_MOCK, NO_MOCK_REASON
from salttesting.helpers import ensure_in_syspath
import integration
ensure_in_syspath('../../')
SOCK_DIR = os.path.join(integration.TMP, 'test-socks')
@skipIf(NO_MOCK, NO_MOCK_REASON)
@ -33,7 +37,8 @@ class ScheduleTestCase(TestCase):
'''
Tests ensuring the job exists and deleting it
'''
self.schedule.opts = {'schedule': {'foo': 'bar'}, 'pillar': ''}
self.schedule.opts = {'schedule': {'foo': 'bar'}, 'pillar': '',
'sock_dir': SOCK_DIR}
self.schedule.delete_job('foo')
self.assertNotIn('foo', self.schedule.opts)
@ -41,7 +46,8 @@ class ScheduleTestCase(TestCase):
'''
Tests deleting job in pillar
'''
self.schedule.opts = {'pillar': {'schedule': {'foo': 'bar'}}, 'schedule': ''}
self.schedule.opts = {'pillar': {'schedule': {'foo': 'bar'}}, 'schedule': '',
'sock_dir': SOCK_DIR}
self.schedule.delete_job('foo')
self.assertNotIn('foo', self.schedule.opts)
@ -49,7 +55,8 @@ class ScheduleTestCase(TestCase):
'''
Tests removing job from intervals
'''
self.schedule.opts = {'pillar': '', 'schedule': ''}
self.schedule.opts = {'pillar': '', 'schedule': '',
'sock_dir': SOCK_DIR}
self.schedule.intervals = {'foo': 'bar'}
self.schedule.delete_job('foo')
self.assertNotIn('foo', self.schedule.intervals)
@ -76,8 +83,10 @@ class ScheduleTestCase(TestCase):
'''
data = {'foo': 'bar'}
ret = {'schedule': {'foo': 'bar', 'hello': 'world'}}
self.schedule.opts = {'schedule': {'hello': 'world'}}
self.schedule.opts = {'schedule': {'hello': 'world'},
'sock_dir': SOCK_DIR}
Schedule.add_job(self.schedule, data)
del self.schedule.opts['sock_dir']
self.assertEqual(self.schedule.opts, ret)
# enable_job tests
@ -86,16 +95,20 @@ class ScheduleTestCase(TestCase):
'''
Tests enabling a job
'''
self.schedule.opts = {'schedule': {'name': {'enabled': 'foo'}}}
self.schedule.opts = {'schedule': {'name': {'enabled': 'foo'}},
'sock_dir': SOCK_DIR}
Schedule.enable_job(self.schedule, 'name')
del self.schedule.opts['sock_dir']
self.assertTrue(self.schedule.opts['schedule']['name']['enabled'])
def test_enable_job_pillar(self):
'''
Tests enabling a job in pillar
'''
self.schedule.opts = {'pillar': {'schedule': {'name': {'enabled': 'foo'}}}}
self.schedule.opts = {'pillar': {'schedule': {'name': {'enabled': 'foo'}}},
'sock_dir': SOCK_DIR}
Schedule.enable_job(self.schedule, 'name', where='pillar')
del self.schedule.opts['sock_dir']
self.assertTrue(self.schedule.opts['pillar']['schedule']['name']['enabled'])
# disable_job tests
@ -104,16 +117,20 @@ class ScheduleTestCase(TestCase):
'''
Tests disabling a job
'''
self.schedule.opts = {'schedule': {'name': {'enabled': 'foo'}}}
self.schedule.opts = {'schedule': {'name': {'enabled': 'foo'}},
'sock_dir': SOCK_DIR}
Schedule.disable_job(self.schedule, 'name')
del self.schedule.opts['sock_dir']
self.assertFalse(self.schedule.opts['schedule']['name']['enabled'])
def test_disable_job_pillar(self):
'''
Tests disabling a job in pillar
'''
self.schedule.opts = {'pillar': {'schedule': {'name': {'enabled': 'foo'}}}}
self.schedule.opts = {'pillar': {'schedule': {'name': {'enabled': 'foo'}}},
'sock_dir': SOCK_DIR}
Schedule.disable_job(self.schedule, 'name', where='pillar')
del self.schedule.opts['sock_dir']
self.assertFalse(self.schedule.opts['pillar']['schedule']['name']['enabled'])
# modify_job tests
@ -134,8 +151,10 @@ class ScheduleTestCase(TestCase):
'''
schedule = {'foo': 'bar'}
ret = {'pillar': {'schedule': {'name': {'foo': 'bar'}}}}
self.schedule.opts = {'pillar': {'schedule': {'name': {'foo': 'bar'}}}}
self.schedule.opts = {'pillar': {'schedule': {'name': {'foo': 'bar'}}},
'sock_dir': SOCK_DIR}
Schedule.modify_job(self.schedule, 'name', schedule, where='pillar')
del self.schedule.opts['sock_dir']
self.assertEqual(self.schedule.opts, ret)
# enable_schedule tests
@ -144,8 +163,10 @@ class ScheduleTestCase(TestCase):
'''
Tests enabling the scheduler
'''
self.schedule.opts = {'schedule': {'enabled': 'foo'}}
self.schedule.opts = {'schedule': {'enabled': 'foo'},
'sock_dir': SOCK_DIR}
Schedule.enable_schedule(self.schedule)
del self.schedule.opts['sock_dir']
self.assertTrue(self.schedule.opts['schedule']['enabled'])
# disable_schedule tests
@ -154,8 +175,10 @@ class ScheduleTestCase(TestCase):
'''
Tests disabling the scheduler
'''
self.schedule.opts = {'schedule': {'enabled': 'foo'}}
self.schedule.opts = {'schedule': {'enabled': 'foo'},
'sock_dir': SOCK_DIR}
Schedule.disable_schedule(self.schedule)
del self.schedule.opts['sock_dir']
self.assertFalse(self.schedule.opts['schedule']['enabled'])
# reload tests