def execGraph(yaml_graph, username): # Write YAML graph to a file #yaml_path = "graph.yaml";f = open(yaml_path,"w");f.write(yaml_graph);f.close() # TODO: How do we cache/lookup this execution again later? Hash the command? # Give our job a unique name job_name = username + "-" + str( datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f')) job_type = 'k8s.io/yggdrasil' # Specify the Docker image and command(s) to run docker_image = "cropsinsilico/jupyterlab:latest" init_command = "mkdir -p /pvc/" + job_name + " && cp -R /pvc/models/* /pvc/" + job_name + " && chown -R 1000:100 /pvc/" + job_name command = "echo '" + str( yaml_graph ) + "' > graph.yml && echo Running in $(pwd): && ls -al && yggrun graph.yml" # Encode our username with Jupyter's special homebrew recipe username = jupyterUserEncode(username) # Job must run in same namespace as the PVC namespace = "hub" # Specify some arbitrary limits timeout = 300 num_cpus = 2 max_ram_mb = 8384 # Create a record in the Job database jobModel = JobModel() job_model = jobModel.createJob(job_name, job_type, async=True, kwargs={ 'name': job_name, 'type': job_type, 'namespace': namespace, 'username': username, 'init_command': init_command, 'command': command, 'image': docker_image, 'timeout': timeout, 'num_cpus': num_cpus, 'max_ram_mb': max_ram_mb, }) jobModel.save(job_model) # Create and run the job k8s_job = KubernetesJob(username, job_name, namespace, timeout, init_command, command, docker_image, num_cpus, max_ram_mb) if not k8s_job.is_running(): jobModel.scheduleJob(job_model) k8s_job.submit() return job_name
def testWorker(self): # Test the settings resp = self.request('/system/setting', method='PUT', params={ 'list': json.dumps([{ 'key': worker.PluginSettings.BROKER, 'value': 'amqp://[email protected]' }, { 'key': worker.PluginSettings.BACKEND, 'value': 'amqp://[email protected]' }]) }, user=self.admin) self.assertStatusOk(resp) # Create a job to be handled by the worker plugin from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob( title='title', type='foo', handler='worker_handler', user=self.admin, public=False, args=(), kwargs={}) job['kwargs'] = { 'jobInfo': utils.jobInfoSpec(job), 'inputs': [ utils.girderInputSpec(self.adminFolder, resourceType='folder') ], 'outputs': [ utils.girderOutputSpec(self.adminFolder, token=self.adminToken) ] } job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock: instance = celeryMock.return_value instance.send_task.return_value = FakeAsyncResult() jobModel.scheduleJob(job) # Make sure we sent the job to celery self.assertEqual(len(celeryMock.mock_calls), 2) self.assertEqual(celeryMock.mock_calls[0][1], ('girder_worker',)) self.assertEqual(celeryMock.mock_calls[0][2], { 'broker': 'amqp://[email protected]', 'backend': 'amqp://[email protected]' }) sendTaskCalls = celeryMock.return_value.send_task.mock_calls self.assertEqual(len(sendTaskCalls), 1) self.assertEqual(sendTaskCalls[0][1], ( 'girder_worker.run', job['args'], job['kwargs'])) self.assertTrue('headers' in sendTaskCalls[0][2]) self.assertTrue('jobInfoSpec' in sendTaskCalls[0][2]['headers']) # Make sure we got and saved the celery task id job = jobModel.load(job['_id'], force=True) self.assertEqual(job['celeryTaskId'], 'fake_id') self.assertEqual(job['status'], JobStatus.QUEUED)
def testWorkerCancel(self): from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob( title='title', type='foo', handler='worker_handler', user=self.admin, public=False, args=(), kwargs={}) job['kwargs'] = { 'jobInfo': utils.jobInfoSpec(job), 'inputs': [ utils.girderInputSpec(self.adminFolder, resourceType='folder') ], 'outputs': [ utils.girderOutputSpec(self.adminFolder, token=self.adminToken) ] } job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock, \ mock.patch('girder.plugins.worker.AsyncResult') as asyncResult: instance = celeryMock.return_value instance.send_task.return_value = FakeAsyncResult() jobModel.scheduleJob(job) jobModel.cancelJob(job) asyncResult.assert_called_with('fake_id', app=mock.ANY) # Check we called revoke asyncResult.return_value.revoke.assert_called_once() job = jobModel.load(job['_id'], force=True) self.assertEqual(job['status'], CustomJobStatus.CANCELING)
def runSlicerCliTasksDescriptionForFolder(self, folder, image, args, pullImage, params): jobModel = Job() token = Token().createToken(days=3, scope='item_task.set_task_spec.%s' % folder['_id'], user=self.getCurrentUser()) job = jobModel.createJob(title='Read docker task specs: %s' % image, type='folder.item_task_slicer_cli_description', handler='worker_handler', user=self.getCurrentUser()) if args[-1:] == ['--xml']: args = args[:-1] jobOptions = { 'itemTaskId': folder['_id'], 'kwargs': { 'task': { 'mode': 'docker', 'docker_image': image, 'container_args': args + ['--xml'], 'pull_image': pullImage, 'outputs': [{ 'id': '_stdout', 'format': 'text' }], }, 'outputs': { '_stdout': { 'mode': 'http', 'method': 'POST', 'format': 'text', 'url': '/'.join((utils.getWorkerApiUrl(), 'folder', str(folder['_id']), 'item_task_slicer_cli_xml')), 'headers': { 'Girder-Token': token['_id'] }, 'params': { 'image': image, 'args': json.dumps(args), 'pullImage': pullImage } } }, 'jobInfo': utils.jobInfoSpec(job), 'validate': False, 'auto_convert': False } } job.update(jobOptions) job = jobModel.save(job) jobModel.scheduleJob(job) return job
def executeTask(self, item, jobTitle, includeJobInfo, inputs, outputs): user = self.getCurrentUser() if jobTitle is None: jobTitle = item['name'] task, handler = self._validateTask(item) jobModel = Job() job = jobModel.createJob(title=jobTitle, type='item_task', handler=handler, user=user) # If this is a user auth token, we make an IO-enabled token token = self.getCurrentToken() tokenModel = Token() if tokenModel.hasScope(token, TokenScope.USER_AUTH): token = tokenModel.createToken(user=user, days=7, scope=(TokenScope.DATA_READ, TokenScope.DATA_WRITE)) job['itemTaskTempToken'] = token['_id'] token = tokenModel.addScope(token, 'item_tasks.job_write:%s' % job['_id']) job.update({ 'itemTaskId': item['_id'], 'itemTaskBindings': { 'inputs': inputs, 'outputs': outputs }, 'kwargs': { 'task': task, 'inputs': self._transformInputs(inputs, token), 'outputs': self._transformOutputs(outputs, token, job, task, item['_id']), 'validate': False, 'auto_convert': False, 'cleanup': True } }) if includeJobInfo: job['kwargs']['jobInfo'] = utils.jobInfoSpec(job) if 'itemTaskCeleryQueue' in item.get('meta', {}): job['celeryQueue'] = item['meta']['itemTaskCeleryQueue'] job = jobModel.save(job) jobModel.scheduleJob(job) return job
def testWorkerDifferentTask(self): # Test the settings resp = self.request('/system/setting', method='PUT', params={ 'key': worker.PluginSettings.API_URL, 'value': 'bad value' }, user=self.admin) self.assertStatus(resp, 400) self.assertEqual(resp.json['message'], 'API URL must start with http:// or https://.') resp = self.request('/system/setting', method='PUT', params={ 'list': json.dumps([{ 'key': worker.PluginSettings.BROKER, 'value': 'amqp://[email protected]' }, { 'key': worker.PluginSettings.BACKEND, 'value': 'amqp://[email protected]' }]) }, user=self.admin) self.assertStatusOk(resp) # Create a job to be handled by the worker plugin from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob( title='title', type='foo', handler='worker_handler', user=self.admin, public=False, args=(), kwargs={}, otherFields={ 'celeryTaskName': 'some_other.task', 'celeryQueue': 'my_other_q' }) job['kwargs'] = { 'jobInfo': utils.jobInfoSpec(job), 'inputs': [ utils.girderInputSpec(self.adminFolder, resourceType='folder') ], 'outputs': [ utils.girderOutputSpec(self.adminFolder, token=self.adminToken) ] } job = jobModel.save(job) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock: instance = celeryMock.return_value instance.send_task.return_value = FakeAsyncResult() jobModel.scheduleJob(job) sendTaskCalls = celeryMock.return_value.send_task.mock_calls self.assertEqual(len(sendTaskCalls), 1) self.assertEqual(sendTaskCalls[0][1], ( 'some_other.task', job['args'], job['kwargs'])) self.assertIn('queue', sendTaskCalls[0][2]) self.assertEqual(sendTaskCalls[0][2]['queue'], 'my_other_q')
def executeTask(self, item, jobTitle, includeJobInfo, inputs, outputs): user = self.getCurrentUser() if jobTitle is None: jobTitle = item['name'] task, handler = self._validateTask(item) if task.get('mode') == 'girder_worker': return runCeleryTask(item['meta']['itemTaskImport'], inputs) jobModel = self.model('job', 'jobs') jobModel = Job() job = jobModel.createJob( title=jobTitle, type='item_task', handler=handler, user=user) # If this is a user auth token, we make an IO-enabled token token = self.getCurrentToken() tokenModel = Token() if tokenModel.hasScope(token, TokenScope.USER_AUTH): token = tokenModel.createToken( user=user, days=7, scope=(TokenScope.DATA_READ, TokenScope.DATA_WRITE)) job['itemTaskTempToken'] = token['_id'] token = tokenModel.addScope(token, 'item_tasks.job_write:%s' % job['_id']) job.update({ 'itemTaskId': item['_id'], 'itemTaskBindings': { 'inputs': inputs, 'outputs': outputs }, 'kwargs': { 'task': task, 'inputs': self._transformInputs(inputs, token), 'outputs': self._transformOutputs(outputs, token, job, task, item['_id']), 'validate': False, 'auto_convert': False, 'cleanup': True } }) if includeJobInfo: job['kwargs']['jobInfo'] = utils.jobInfoSpec(job) if 'itemTaskCeleryQueue' in item.get('meta', {}): job['celeryQueue'] = item['meta']['itemTaskCeleryQueue'] job = jobModel.save(job) jobModel.scheduleJob(job) return job
def runSlicerCliTasksDescriptionForFolder(self, folder, image, args, pullImage, params): jobModel = Job() token = Token().createToken( days=3, scope='item_task.set_task_spec.%s' % folder['_id'], user=self.getCurrentUser()) job = jobModel.createJob( title='Read docker task specs: %s' % image, type='folder.item_task_slicer_cli_description', handler='worker_handler', user=self.getCurrentUser()) if args[-1:] == ['--xml']: args = args[:-1] jobOptions = { 'itemTaskId': folder['_id'], 'kwargs': { 'task': { 'mode': 'docker', 'docker_image': image, 'container_args': args + ['--xml'], 'pull_image': pullImage, 'outputs': [{ 'id': '_stdout', 'format': 'text' }], }, 'outputs': { '_stdout': { 'mode': 'http', 'method': 'POST', 'format': 'text', 'url': '/'.join((utils.getWorkerApiUrl(), 'folder', str(folder['_id']), 'item_task_slicer_cli_xml')), 'headers': {'Girder-Token': token['_id']}, 'params': { 'image': image, 'args': json.dumps(args), 'pullImage': pullImage } } }, 'jobInfo': utils.jobInfoSpec(job), 'validate': False, 'auto_convert': False } } job.update(jobOptions) job = jobModel.save(job) jobModel.scheduleJob(job) return job
def testWorkerCancel(self): from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob(title='title', type='foo', handler='worker_handler', user=self.admin, public=False, args=(), kwargs={}) job['kwargs'] = { 'jobInfo': utils.jobInfoSpec(job), 'inputs': [utils.girderInputSpec(self.adminFolder, resourceType='folder')], 'outputs': [utils.girderOutputSpec(self.adminFolder, token=self.adminToken)] } job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock, \ mock.patch('girder.plugins.worker.AsyncResult') as asyncResult: instance = celeryMock.return_value instance.send_task.return_value = FakeAsyncResult() jobModel.scheduleJob(job) jobModel.cancelJob(job) asyncResult.assert_called_with('fake_id', app=mock.ANY) # Check we called revoke asyncResult.return_value.revoke.assert_called_once() job = jobModel.load(job['_id'], force=True) self.assertEqual(job['status'], CustomJobStatus.CANCELING)
def runSlicerCliTasksDescriptionForItem( self, item, image, args, setName, setDescription, pullImage, params): if 'meta' not in item: item['meta'] = {} if image is None: image = item.get('meta', {}).get('itemTaskSpec', {}).get('docker_image') if not image: raise RestException( 'You must pass an image parameter, or set the itemTaskSpec.docker_image ' 'field of the item.') jobModel = Job() token = Token().createToken( days=3, scope='item_task.set_task_spec.%s' % item['_id']) job = jobModel.createJob( title='Read docker Slicer CLI: %s' % image, type='item.item_task_slicer_cli_description', handler='worker_handler', user=self.getCurrentUser()) if args[-1:] == ['--xml']: args = args[:-1] job.update({ 'itemTaskId': item['_id'], 'kwargs': { 'task': { 'mode': 'docker', 'docker_image': image, 'container_args': args + ['--xml'], 'pull_image': pullImage, 'outputs': [{ 'id': '_stdout', 'format': 'text' }], }, 'outputs': { '_stdout': { 'mode': 'http', 'method': 'PUT', 'format': 'text', 'url': '/'.join((utils.getWorkerApiUrl(), 'item', str(item['_id']), 'item_task_slicer_cli_xml')), 'params': { 'setName': setName, 'setDescription': setDescription }, 'headers': {'Girder-Token': token['_id']} } }, 'jobInfo': utils.jobInfoSpec(job), 'validate': False, 'auto_convert': False } }) item['meta']['itemTaskSpec'] = { 'mode': 'docker', 'docker_image': image } if args: item['meta']['itemTaskSlicerCliArgs'] = args Item().save(item) job = jobModel.save(job) jobModel.scheduleJob(job) return job
def testInstanceFlow(self, lc, cv, uc, sc, rv): with mock.patch('girder_worker.task.celery.Task.apply_async', spec=True) \ as mock_apply_async: resp = self.request(path='/instance', method='POST', user=self.user, params={ 'taleId': str(self.tale_one['_id']), 'name': 'tale one' }) mock_apply_async.assert_called_once() self.assertStatusOk(resp) instance = resp.json # Create a job to be handled by the worker plugin from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob(title='Spawn Instance', type='celery', handler='worker_handler', user=self.user, public=False, args=[{ 'instanceId': instance['_id'] }], kwargs={}) job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock, \ mock.patch('girder.plugins.worker.getCeleryApp') as gca: celeryMock().AsyncResult.return_value = FakeAsyncResult( instance['_id']) gca().send_task.return_value = FakeAsyncResult(instance['_id']) jobModel.scheduleJob(job) for i in range(20): job = jobModel.load(job['_id'], force=True) if job['status'] == JobStatus.QUEUED: break time.sleep(0.1) self.assertEqual(job['status'], JobStatus.QUEUED) instance = Instance().load(instance['_id'], force=True) self.assertEqual(instance['status'], InstanceStatus.LAUNCHING) # Make sure we sent the job to celery sendTaskCalls = gca.return_value.send_task.mock_calls self.assertEqual(len(sendTaskCalls), 1) self.assertEqual(sendTaskCalls[0][1], ('girder_worker.run', job['args'], job['kwargs'])) self.assertTrue('headers' in sendTaskCalls[0][2]) self.assertTrue('jobInfoSpec' in sendTaskCalls[0][2]['headers']) # Make sure we got and saved the celery task id job = jobModel.load(job['_id'], force=True) self.assertEqual(job['celeryTaskId'], 'fake_id') Job().updateJob(job, log='job running', status=JobStatus.RUNNING) Job().updateJob(job, log='job ran', status=JobStatus.SUCCESS) resp = self.request(path='/job/{_id}/result'.format(**job), method='GET', user=self.user) self.assertStatusOk(resp) self.assertEqual(resp.json['nodeId'], '123456') # Check if set up properly resp = self.request(path='/instance/{_id}'.format(**instance), method='GET', user=self.user) self.assertEqual(resp.json['containerInfo']['imageId'], str(self.image['_id'])) self.assertEqual(resp.json['containerInfo']['digest'], self.tale_one['imageInfo']['digest']) self.assertEqual(resp.json['containerInfo']['nodeId'], '123456') self.assertEqual(resp.json['containerInfo']['volumeName'], 'blah_volume') self.assertEqual(resp.json['status'], InstanceStatus.RUNNING) # Save this response to populate containerInfo instance = resp.json # Check that the instance is a singleton resp = self.request(path='/instance', method='POST', user=self.user, params={ 'taleId': str(self.tale_one['_id']), 'name': 'tale one' }) self.assertStatusOk(resp) self.assertEqual(resp.json['_id'], str(instance['_id'])) # Update/restart the instance job = jobModel.createJob(title='Update Instance', type='celery', handler='worker_handler', user=self.user, public=False, args=[instance['_id']], kwargs={}) job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) with mock.patch('celery.Celery') as celeryMock, mock.patch( 'girder_worker.task.celery.Task.apply_async', spec=True) as mock_apply_async, mock.patch( 'girder.plugins.worker.getCeleryApp') as gca: gca().send_task.return_value = FakeAsyncResultForUpdate( instance['_id']) # PUT /instance/:id (currently a no-op) resp = self.request( path='/instance/{_id}'.format(**instance), method='PUT', user=self.user, body=json.dumps({ # ObjectId is not serializable '_id': str(instance['_id']), 'iframe': instance['iframe'], 'name': instance['name'], 'status': instance['status'], 'taleId': instance['status'], 'sessionId': instance['status'], 'url': instance['url'], 'containerInfo': { 'digest': instance['containerInfo']['digest'], 'imageId': instance['containerInfo']['imageId'], 'mountPoint': instance['containerInfo']['mountPoint'], 'name': instance['containerInfo']['name'], 'nodeId': instance['containerInfo']['nodeId'], 'urlPath': instance['containerInfo']['urlPath'], } })) self.assertStatusOk(resp) mock_apply_async.assert_called_once() jobModel.scheduleJob(job) for i in range(20): job = jobModel.load(job['_id'], force=True) if job['status'] == JobStatus.QUEUED: break time.sleep(0.1) self.assertEqual(job['status'], JobStatus.QUEUED) instance = Instance().load(instance['_id'], force=True) self.assertEqual(instance['status'], InstanceStatus.LAUNCHING) # Make sure we sent the job to celery sendTaskCalls = gca.return_value.send_task.mock_calls self.assertEqual(len(sendTaskCalls), 1) self.assertEqual(sendTaskCalls[0][1], ('girder_worker.run', job['args'], job['kwargs'])) self.assertTrue('headers' in sendTaskCalls[0][2]) self.assertTrue('jobInfoSpec' in sendTaskCalls[0][2]['headers']) # Make sure we got and saved the celery task id job = jobModel.load(job['_id'], force=True) self.assertEqual(job['celeryTaskId'], 'fake_update_id') Job().updateJob(job, log='job running', status=JobStatus.RUNNING) Job().updateJob(job, log='job running', status=JobStatus.RUNNING) Job().updateJob(job, log='job ran', status=JobStatus.SUCCESS) resp = self.request(path='/instance/{_id}'.format(**instance), method='GET', user=self.user) self.assertStatusOk(resp) self.assertEqual(resp.json['containerInfo']['digest'], 'sha256:7a789bc20359dce987653') instance = resp.json # Update/restart the instance and fail job = jobModel.createJob(title='Update Instance', type='celery', handler='worker_handler', user=self.user, public=False, args=[instance['_id']], kwargs={}) job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) with mock.patch('celery.Celery') as celeryMock, mock.patch( 'girder_worker.task.celery.Task.apply_async', spec=True) as mock_apply_async, mock.patch( 'girder.plugins.worker.getCeleryApp') as gca: gca().send_task.return_value = FakeAsyncResultForUpdate( instance['_id']) # PUT /instance/:id (currently a no-op) resp = self.request( path='/instance/{_id}'.format(**instance), method='PUT', user=self.user, body=json.dumps({ # ObjectId is not serializable '_id': str(instance['_id']), 'iframe': instance['iframe'], 'name': instance['name'], 'status': instance['status'], 'taleId': instance['status'], 'sessionId': instance['status'], 'url': instance['url'], 'containerInfo': { 'digest': instance['containerInfo']['digest'], 'imageId': instance['containerInfo']['imageId'], 'mountPoint': instance['containerInfo']['mountPoint'], 'name': instance['containerInfo']['name'], 'nodeId': instance['containerInfo']['nodeId'], 'urlPath': instance['containerInfo']['urlPath'], } })) self.assertStatusOk(resp) mock_apply_async.assert_called_once() jobModel.scheduleJob(job) for i in range(20): job = jobModel.load(job['_id'], force=True) self.assertEqual(job['status'], JobStatus.QUEUED) instance = Instance().load(instance['_id'], force=True) self.assertEqual(instance['status'], InstanceStatus.LAUNCHING) Job().updateJob(job, log='job failed', status=JobStatus.ERROR) instance = Instance().load(instance['_id'], force=True) self.assertEqual(instance['status'], InstanceStatus.ERROR) # Delete the instance with mock.patch('girder_worker.task.celery.Task.apply_async', spec=True) \ as mock_apply_async: resp = self.request(path='/instance/{_id}'.format(**instance), method='DELETE', user=self.user) self.assertStatusOk(resp) mock_apply_async.assert_called_once() resp = self.request(path='/instance/{_id}'.format(**instance), method='GET', user=self.user) self.assertStatus(resp, 400)
def runSlicerCliTasksDescriptionForItem(self, item, image, args, setName, setDescription, pullImage, params): if 'meta' not in item: item['meta'] = {} if image is None: image = item.get('meta', {}).get('itemTaskSpec', {}).get('docker_image') if not image: raise RestException( 'You must pass an image parameter, or set the itemTaskSpec.docker_image ' 'field of the item.') jobModel = Job() token = Token().createToken(days=3, scope='item_task.set_task_spec.%s' % item['_id']) job = jobModel.createJob(title='Read docker Slicer CLI: %s' % image, type='item.item_task_slicer_cli_description', handler='worker_handler', user=self.getCurrentUser()) if args[-1:] == ['--xml']: args = args[:-1] job.update({ 'itemTaskId': item['_id'], 'kwargs': { 'task': { 'mode': 'docker', 'docker_image': image, 'container_args': args + ['--xml'], 'pull_image': pullImage, 'outputs': [{ 'id': '_stdout', 'format': 'text' }], }, 'outputs': { '_stdout': { 'mode': 'http', 'method': 'PUT', 'format': 'text', 'url': '/'.join((utils.getWorkerApiUrl(), 'item', str(item['_id']), 'item_task_slicer_cli_xml')), 'params': { 'setName': setName, 'setDescription': setDescription }, 'headers': { 'Girder-Token': token['_id'] } } }, 'jobInfo': utils.jobInfoSpec(job), 'validate': False, 'auto_convert': False } }) item['meta']['itemTaskSpec'] = {'mode': 'docker', 'docker_image': image} if args: item['meta']['itemTaskSlicerCliArgs'] = args Item().save(item) job = jobModel.save(job) jobModel.scheduleJob(job) return job
class JobsTestCase(base.TestCase): def setUp(self): base.TestCase.setUp(self) self.users = [User().createUser( 'usr' + str(n), 'passwd', 'tst', 'usr', '*****@*****.**' % n) for n in range(3)] from girder.plugins.jobs.models.job import Job self.jobModel = Job() def testJobs(self): self.job = None def schedule(event): self.job = event.info if self.job['handler'] == 'my_handler': self.job['status'] = JobStatus.RUNNING self.job = self.jobModel.save(self.job) self.assertEqual(self.job['args'], ('hello', 'world')) self.assertEqual(self.job['kwargs'], {'a': 'b'}) events.bind('jobs.schedule', 'test', schedule) # Create a job job = self.jobModel.createJob( title='Job Title', type='my_type', args=('hello', 'world'), kwargs={'a': 'b'}, user=self.users[1], handler='my_handler', public=False) self.assertEqual(self.job, None) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure our handler was invoked self.jobModel.scheduleJob(job) self.assertEqual(self.job['_id'], job['_id']) self.assertEqual(self.job['status'], JobStatus.RUNNING) # Since the job is not public, user 2 should not have access path = '/job/%s' % job['_id'] resp = self.request(path, user=self.users[2]) self.assertStatus(resp, 403) resp = self.request(path, user=self.users[2], method='PUT') self.assertStatus(resp, 403) resp = self.request(path, user=self.users[2], method='DELETE') self.assertStatus(resp, 403) # If no user is specified, we should get a 401 error resp = self.request(path, user=None) self.assertStatus(resp, 401) # Make sure user who created the job can see it resp = self.request(path, user=self.users[1]) self.assertStatusOk(resp) # We should be able to update the job as the user who created it resp = self.request(path, method='PUT', user=self.users[1], params={ 'log': 'My log message\n' }) self.assertStatusOk(resp) # We should be able to create a job token and use that to update it too token = self.jobModel.createJobToken(job) resp = self.request(path, method='PUT', params={ 'log': 'append message', 'token': token['_id'] }) self.assertStatusOk(resp) # We shouldn't get the log back in this case self.assertNotIn('log', resp.json) # Do a fetch on the job itself to get the log resp = self.request(path, user=self.users[1]) self.assertStatusOk(resp) self.assertEqual( resp.json['log'], ['My log message\n', 'append message']) # Test overwriting the log and updating status resp = self.request(path, method='PUT', params={ 'log': 'overwritten log', 'overwrite': 'true', 'status': JobStatus.SUCCESS, 'token': token['_id'] }) self.assertStatusOk(resp) self.assertNotIn('log', resp.json) self.assertEqual(resp.json['status'], JobStatus.SUCCESS) job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['overwritten log']) # We should be able to delete the job as the user who created it resp = self.request(path, user=self.users[1], method='DELETE') self.assertStatusOk(resp) job = self.jobModel.load(job['_id'], force=True) self.assertIsNone(job) def testLegacyLogBehavior(self): # Force save a job with a string log to simulate a legacy job record job = self.jobModel.createJob( title='legacy', type='legacy', user=self.users[1], save=False) job['log'] = 'legacy log' job = self.jobModel.save(job, validate=False) self.assertEqual(job['log'], 'legacy log') # Load the record, we should now get the log as a list job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['legacy log']) def testListJobs(self): job = self.jobModel.createJob(title='A job', type='t', user=self.users[1], public=False) anonJob = self.jobModel.createJob(title='Anon job', type='t') # Ensure timestamp for public job is strictly higher (ms resolution) time.sleep(0.1) publicJob = self.jobModel.createJob( title='Anon job', type='t', public=True) # User 1 should be able to see their own jobs resp = self.request('/job', user=self.users[1], params={ 'userId': self.users[1]['_id'] }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 1) self.assertEqual(resp.json[0]['_id'], str(job['_id'])) # User 2 should not see user 1's jobs in the list resp = self.request('/job', user=self.users[2], params={ 'userId': self.users[1]['_id'] }) self.assertEqual(resp.json, []) # Omitting a userId should assume current user resp = self.request('/job', user=self.users[1]) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 1) self.assertEqual(resp.json[0]['_id'], str(job['_id'])) # Explicitly passing "None" should show anonymous jobs resp = self.request('/job', user=self.users[0], params={ 'userId': 'none' }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 2) self.assertEqual(resp.json[0]['_id'], str(publicJob['_id'])) self.assertEqual(resp.json[1]['_id'], str(anonJob['_id'])) # Non-admins should only see public anon jobs resp = self.request('/job', params={'userId': 'none'}) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 1) self.assertEqual(resp.json[0]['_id'], str(publicJob['_id'])) def testListAllJobs(self): self.jobModel.createJob(title='user 0 job', type='t', user=self.users[0], public=False) self.jobModel.createJob(title='user 1 job', type='t', user=self.users[1], public=False) self.jobModel.createJob(title='user 1 job', type='t', user=self.users[1], public=True) self.jobModel.createJob(title='user 2 job', type='t', user=self.users[2]) self.jobModel.createJob(title='anonymous job', type='t') self.jobModel.createJob(title='anonymous public job', type='t2', public=True) # User 0, as a site admin, should be able to see all jobs resp = self.request('/job/all', user=self.users[0]) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 6) # Test deprecated listAll method jobs = list(self.jobModel.listAll(limit=0, offset=0, sort=None, currentUser=self.users[0])) self.assertEqual(len(jobs), 6) # get with filter resp = self.request('/job/all', user=self.users[0], params={ 'types': json.dumps(['t']), 'statuses': json.dumps([0]) }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 5) # get with unmet filter conditions resp = self.request('/job/all', user=self.users[0], params={ 'types': json.dumps(['nonexisttype']) }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 0) # User 1, as non site admin, should encounter http 403 (Forbidden) resp = self.request('/job/all', user=self.users[1]) self.assertStatus(resp, 403) # Not authenticated user should encounter http 401 (unauthorized) resp = self.request('/job/all') self.assertStatus(resp, 401) def testFiltering(self): job = self.jobModel.createJob(title='A job', type='t', user=self.users[1], public=True) job['_some_other_field'] = 'foo' job = self.jobModel.save(job) resp = self.request('/job/%s' % job['_id']) self.assertStatusOk(resp) self.assertTrue('created' in resp.json) self.assertTrue('_some_other_field' not in resp.json) self.assertTrue('kwargs' not in resp.json) self.assertTrue('args' not in resp.json) resp = self.request('/job/%s' % job['_id'], user=self.users[0]) self.assertTrue('kwargs' in resp.json) self.assertTrue('args' in resp.json) self.jobModel.exposeFields(level=AccessType.READ, fields={'_some_other_field'}) self.jobModel.hideFields(level=AccessType.READ, fields={'created'}) resp = self.request('/job/%s' % job['_id']) self.assertStatusOk(resp) self.assertEqual(resp.json['_some_other_field'], 'foo') self.assertTrue('created' not in resp.json) def testJobProgressAndNotifications(self): job = self.jobModel.createJob(title='a job', type='t', user=self.users[1], public=True) path = '/job/%s' % job['_id'] resp = self.request(path) self.assertEqual(resp.json['progress'], None) self.assertEqual(resp.json['timestamps'], []) resp = self.request(path, method='PUT', user=self.users[1], params={ 'progressTotal': 100, 'progressCurrent': 3, 'progressMessage': 'Started', 'notify': 'false', 'status': JobStatus.QUEUED }) self.assertStatusOk(resp) self.assertEqual(resp.json['progress'], { 'total': 100, 'current': 3, 'message': 'Started', 'notificationId': None }) # The status update should make it so we now have a timestamp self.assertEqual(len(resp.json['timestamps']), 1) self.assertEqual(resp.json['timestamps'][0]['status'], JobStatus.QUEUED) self.assertIn('time', resp.json['timestamps'][0]) # If the status does not change on update, no timestamp should be added resp = self.request(path, method='PUT', user=self.users[1], params={ 'status': JobStatus.QUEUED }) self.assertStatusOk(resp) self.assertEqual(len(resp.json['timestamps']), 1) self.assertEqual(resp.json['timestamps'][0]['status'], JobStatus.QUEUED) # We passed notify=false, so we should only have the job creation notification resp = self.request(path='/notification/stream', method='GET', user=self.users[1], isJson=False, params={'timeout': 0}) messages = self.getSseMessages(resp) self.assertEqual(len(messages), 1) # Update progress with notify=true (the default) resp = self.request(path, method='PUT', user=self.users[1], params={ 'progressCurrent': 50, 'progressMessage': 'Something bad happened', 'status': JobStatus.ERROR }) self.assertStatusOk(resp) self.assertNotEqual(resp.json['progress']['notificationId'], None) # We should now see three notifications (job created + job status + progress) resp = self.request(path='/notification/stream', method='GET', user=self.users[1], isJson=False, params={'timeout': 0}) messages = self.getSseMessages(resp) job = self.jobModel.load(job['_id'], force=True) self.assertEqual(len(messages), 3) creationNotify = messages[0] progressNotify = messages[1] statusNotify = messages[2] self.assertEqual(creationNotify['type'], 'job_created') self.assertEqual(creationNotify['data']['_id'], str(job['_id'])) self.assertEqual(statusNotify['type'], 'job_status') self.assertEqual(statusNotify['data']['_id'], str(job['_id'])) self.assertEqual(int(statusNotify['data']['status']), JobStatus.ERROR) self.assertNotIn('kwargs', statusNotify['data']) self.assertNotIn('log', statusNotify['data']) self.assertEqual(progressNotify['type'], 'progress') self.assertEqual(progressNotify['data']['title'], job['title']) self.assertEqual(progressNotify['data']['current'], float(50)) self.assertEqual(progressNotify['data']['state'], 'error') self.assertEqual(progressNotify['_id'], str(job['progress']['notificationId'])) def testDotsInKwargs(self): kwargs = { '$key.with.dots': 'value', 'foo': [{ 'moar.dots': True }] } job = self.jobModel.createJob(title='dots', type='x', user=self.users[0], kwargs=kwargs) # Make sure we can update a job and notification creation works self.jobModel.updateJob(job, status=JobStatus.QUEUED, notify=True) self.assertEqual(job['kwargs'], kwargs) resp = self.request('/job/%s' % job['_id'], user=self.users[0]) self.assertStatusOk(resp) self.assertEqual(resp.json['kwargs'], kwargs) job = self.jobModel.load(job['_id'], force=True) self.assertEqual(job['kwargs'], kwargs) job = self.jobModel.filter(job, self.users[0]) self.assertEqual(job['kwargs'], kwargs) job = self.jobModel.filter(job, self.users[1]) self.assertFalse('kwargs' in job) def testLocalJob(self): job = self.jobModel.createLocalJob( title='local', type='local', user=self.users[0], kwargs={ 'hello': 'world' }, module='plugin_tests.local_job_impl') self.jobModel.scheduleJob(job) job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['job ran!']) job = self.jobModel.createLocalJob( title='local', type='local', user=self.users[0], kwargs={ 'hello': 'world' }, module='plugin_tests.local_job_impl', function='fail') self.jobModel.scheduleJob(job) job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['job failed']) def testValidateCustomStatus(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) def validateStatus(event): if event.info == 1234: event.preventDefault().addResponse(True) def validTransitions(event): if event.info['status'] == 1234: event.preventDefault().addResponse([JobStatus.INACTIVE]) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=1234) # Should fail with events.bound('jobs.status.validate', 'test', validateStatus), \ events.bound('jobs.status.validTransitions', 'test', validTransitions): self.jobModel.updateJob(job, status=1234) # Should work with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=4321) # Should fail def testValidateCustomStrStatus(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) def validateStatus(event): states = ['a', 'b', 'c'] if event.info in states: event.preventDefault().addResponse(True) def validTransitions(event): if event.info['status'] == 'a': event.preventDefault().addResponse([JobStatus.INACTIVE]) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status='a') with events.bound('jobs.status.validate', 'test', validateStatus), \ events.bound('jobs.status.validTransitions', 'test', validTransitions): self.jobModel.updateJob(job, status='a') self.assertEqual(job['status'], 'a') with self.assertRaises(ValidationException), \ events.bound('jobs.status.validate', 'test', validateStatus): self.jobModel.updateJob(job, status='foo') def testUpdateOtherFields(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) job = self.jobModel.updateJob(job, otherFields={'other': 'fields'}) self.assertEqual(job['other'], 'fields') def testCancelJob(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) # add to the log job = self.jobModel.updateJob(job, log='entry 1\n') # Reload without the log job = self.jobModel.load(id=job['_id'], force=True) self.assertEqual(len(job.get('log', [])), 0) # Cancel job = self.jobModel.cancelJob(job) self.assertEqual(job['status'], JobStatus.CANCELED) # Reloading should still have the log and be canceled job = self.jobModel.load(id=job['_id'], force=True, includeLog=True) self.assertEqual(job['status'], JobStatus.CANCELED) self.assertEqual(len(job.get('log', [])), 1) def testCancelJobEndpoint(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) # Ensure requires write perms jobCancelUrl = '/job/%s/cancel' % job['_id'] resp = self.request(jobCancelUrl, user=self.users[1], method='PUT') self.assertStatus(resp, 403) # Try again with the right user jobCancelUrl = '/job/%s/cancel' % job['_id'] resp = self.request(jobCancelUrl, user=self.users[0], method='PUT') self.assertStatusOk(resp) self.assertEqual(resp.json['status'], JobStatus.CANCELED) def testJobsTypesAndStatuses(self): self.jobModel.createJob(title='user 0 job', type='t1', user=self.users[0], public=False) self.jobModel.createJob(title='user 1 job', type='t2', user=self.users[1], public=False) self.jobModel.createJob(title='user 1 job', type='t3', user=self.users[1], public=True) self.jobModel.createJob(title='user 2 job', type='t4', user=self.users[2]) self.jobModel.createJob(title='anonymous job', type='t5') self.jobModel.createJob(title='anonymous public job', type='t6', public=True) # User 1, as non site admin, should encounter http 403 (Forbidden) resp = self.request('/job/typeandstatus/all', user=self.users[1]) self.assertStatus(resp, 403) # Admin user gets all types and statuses resp = self.request('/job/typeandstatus/all', user=self.users[0]) self.assertStatusOk(resp) self.assertEqual(len(resp.json['types']), 6) self.assertEqual(len(resp.json['statuses']), 1) # standard user gets types and statuses of its own jobs resp = self.request('/job/typeandstatus', user=self.users[1]) self.assertStatusOk(resp) self.assertEqual(len(resp.json['types']), 2) self.assertEqual(len(resp.json['statuses']), 1) def testDefaultParentId(self): job = self.jobModel.createJob(title='Job', type='Job', user=self.users[0]) # If not specified parentId should be None self.assertEquals(job['parentId'], None) def testIsParentIdCorrect(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) # During initialization parent job should be set correctly self.assertEqual(childJob['parentId'], parentJob['_id']) def testSetParentCorrectly(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob(title='Child Job', type='Child Job', user=self.users[0]) self.jobModel.setParentJob(childJob, parentJob) # After setParentJob method is called parent job should be set correctly self.assertEqual(childJob['parentId'], parentJob['_id']) def testParentCannotBeEqualToChild(self): childJob = self.jobModel.createJob(title='Child Job', type='Child Job', user=self.users[0]) # Cannot set a job as it's own parent with self.assertRaises(ValidationException): self.jobModel.setParentJob(childJob, childJob) def testParentIdCannotBeOverridden(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) anotherParentJob = self.jobModel.createJob( title='Another Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) with self.assertRaises(ValidationException): # If parent job is set, cannot be overridden self.jobModel.setParentJob(childJob, anotherParentJob) def testListChildJobs(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) self.jobModel.createJob( title='Another Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) # Should return a list with 2 jobs self.assertEquals(len(list(self.jobModel.listChildJobs(parentJob))), 2) # Should return an empty list self.assertEquals(len(list(self.jobModel.listChildJobs(childJob))), 0) def testListChildJobsRest(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) self.jobModel.createJob( title='Another Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) resp = self.request('/job', user=self.users[0], params={'parentId': str(parentJob['_id'])}) resp2 = self.request('/job', user=self.users[0], params={'parentId': str(childJob['_id'])}) self.assertStatusOk(resp) self.assertStatusOk(resp2) # Should return a list with 2 jobs self.assertEquals(len(resp.json), 2) # Should return an empty list self.assertEquals(len(resp2.json), 0) def testCreateJobRest(self): resp = self.request('/job', method='POST', user=self.users[0], params={'title': 'job', 'type': 'job'}) # If user does not have the necessary token status is 403 self.assertStatus(resp, 403) token = Token().createToken(scope=REST_CREATE_JOB_TOKEN_SCOPE) resp2 = self.request( '/job', method='POST', token=token, params={'title': 'job', 'type': 'job'}) # If user has the necessary token status is 200 self.assertStatusOk(resp2) def testJobStateTransitions(self): job = self.jobModel.createJob( title='user 0 job', type='t1', user=self.users[0], public=False) # We can't move straight to SUCCESS with self.assertRaises(ValidationException): job = self.jobModel.updateJob(job, status=JobStatus.SUCCESS) self.jobModel.updateJob(job, status=JobStatus.QUEUED) self.jobModel.updateJob(job, status=JobStatus.RUNNING) self.jobModel.updateJob(job, status=JobStatus.ERROR) # We shouldn't be able to move backwards with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=JobStatus.QUEUED) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=JobStatus.RUNNING) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=JobStatus.INACTIVE) def testJobSaveEventModification(self): def customSave(event): kwargs = json_util.loads(event.info['kwargs']) kwargs['key2'] = 'newvalue' event.info['kwargs'] = json_util.dumps(kwargs) job = self.jobModel.createJob(title='A job', type='t', user=self.users[1], public=True) job['kwargs'] = {'key1': 'value1', 'key2': 'value2'} with events.bound('model.job.save', 'test', customSave): job = self.jobModel.save(job) self.assertEqual(job['kwargs']['key2'], 'newvalue')
class JobsTestCase(base.TestCase): def setUp(self): base.TestCase.setUp(self) self.users = [User().createUser( 'usr' + str(n), 'passwd', 'tst', 'usr', '*****@*****.**' % n) for n in range(3)] from girder.plugins.jobs.models.job import Job self.jobModel = Job() def testJobs(self): self.job = None def schedule(event): self.job = event.info if self.job['handler'] == 'my_handler': self.job['status'] = JobStatus.RUNNING self.job = self.jobModel.save(self.job) self.assertEqual(self.job['args'], ('hello', 'world')) self.assertEqual(self.job['kwargs'], {'a': 'b'}) events.bind('jobs.schedule', 'test', schedule) # Create a job job = self.jobModel.createJob( title='Job Title', type='my_type', args=('hello', 'world'), kwargs={'a': 'b'}, user=self.users[1], handler='my_handler', public=False) self.assertEqual(self.job, None) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure our handler was invoked self.jobModel.scheduleJob(job) self.assertEqual(self.job['_id'], job['_id']) self.assertEqual(self.job['status'], JobStatus.RUNNING) # Since the job is not public, user 2 should not have access path = '/job/%s' % job['_id'] resp = self.request(path, user=self.users[2]) self.assertStatus(resp, 403) resp = self.request(path, user=self.users[2], method='PUT') self.assertStatus(resp, 403) resp = self.request(path, user=self.users[2], method='DELETE') self.assertStatus(resp, 403) # Make sure user who created the job can see it resp = self.request(path, user=self.users[1]) self.assertStatusOk(resp) # We should be able to update the job as the user who created it resp = self.request(path, method='PUT', user=self.users[1], params={ 'log': 'My log message\n' }) self.assertStatusOk(resp) # We should be able to create a job token and use that to update it too token = self.jobModel.createJobToken(job) resp = self.request(path, method='PUT', params={ 'log': 'append message', 'token': token['_id'] }) self.assertStatusOk(resp) # We shouldn't get the log back in this case self.assertNotIn('log', resp.json) # Do a fetch on the job itself to get the log resp = self.request(path, user=self.users[1]) self.assertStatusOk(resp) self.assertEqual( resp.json['log'], ['My log message\n', 'append message']) # Test overwriting the log and updating status resp = self.request(path, method='PUT', params={ 'log': 'overwritten log', 'overwrite': 'true', 'status': JobStatus.SUCCESS, 'token': token['_id'] }) self.assertStatusOk(resp) self.assertNotIn('log', resp.json) self.assertEqual(resp.json['status'], JobStatus.SUCCESS) job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['overwritten log']) # We should be able to delete the job as the user who created it resp = self.request(path, user=self.users[1], method='DELETE') self.assertStatusOk(resp) job = self.jobModel.load(job['_id'], force=True) self.assertIsNone(job) def testLegacyLogBehavior(self): # Force save a job with a string log to simulate a legacy job record job = self.jobModel.createJob( title='legacy', type='legacy', user=self.users[1], save=False) job['log'] = 'legacy log' job = self.jobModel.save(job, validate=False) self.assertEqual(job['log'], 'legacy log') # Load the record, we should now get the log as a list job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['legacy log']) def testListJobs(self): job = self.jobModel.createJob(title='A job', type='t', user=self.users[1], public=False) anonJob = self.jobModel.createJob(title='Anon job', type='t') # Ensure timestamp for public job is strictly higher (ms resolution) time.sleep(0.1) publicJob = self.jobModel.createJob( title='Anon job', type='t', public=True) # User 1 should be able to see their own jobs resp = self.request('/job', user=self.users[1], params={ 'userId': self.users[1]['_id'] }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 1) self.assertEqual(resp.json[0]['_id'], str(job['_id'])) # User 2 should not see user 1's jobs in the list resp = self.request('/job', user=self.users[2], params={ 'userId': self.users[1]['_id'] }) self.assertEqual(resp.json, []) # Omitting a userId should assume current user resp = self.request('/job', user=self.users[1]) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 1) self.assertEqual(resp.json[0]['_id'], str(job['_id'])) # Explicitly passing "None" should show anonymous jobs resp = self.request('/job', user=self.users[0], params={ 'userId': 'none' }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 2) self.assertEqual(resp.json[0]['_id'], str(publicJob['_id'])) self.assertEqual(resp.json[1]['_id'], str(anonJob['_id'])) # Non-admins should only see public anon jobs resp = self.request('/job', params={'userId': 'none'}) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 1) self.assertEqual(resp.json[0]['_id'], str(publicJob['_id'])) def testListAllJobs(self): self.jobModel.createJob(title='user 0 job', type='t', user=self.users[0], public=False) self.jobModel.createJob(title='user 1 job', type='t', user=self.users[1], public=False) self.jobModel.createJob(title='user 1 job', type='t', user=self.users[1], public=True) self.jobModel.createJob(title='user 2 job', type='t', user=self.users[2]) self.jobModel.createJob(title='anonymous job', type='t') self.jobModel.createJob(title='anonymous public job', type='t2', public=True) # User 0, as a site admin, should be able to see all jobs resp = self.request('/job/all', user=self.users[0]) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 6) # Test deprecated listAll method jobs = list(self.jobModel.listAll(limit=0, offset=0, sort=None, currentUser=self.users[0])) self.assertEqual(len(jobs), 6) # get with filter resp = self.request('/job/all', user=self.users[0], params={ 'types': json.dumps(['t']), 'statuses': json.dumps([0]) }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 5) # get with unmet filter conditions resp = self.request('/job/all', user=self.users[0], params={ 'types': json.dumps(['nonexisttype']) }) self.assertStatusOk(resp) self.assertEqual(len(resp.json), 0) # User 1, as non site admin, should encounter http 403 (Forbidden) resp = self.request('/job/all', user=self.users[1]) self.assertStatus(resp, 403) # Not authenticated user should encounter http 401 (unauthorized) resp = self.request('/job/all') self.assertStatus(resp, 401) def testFiltering(self): job = self.jobModel.createJob(title='A job', type='t', user=self.users[1], public=True) job['_some_other_field'] = 'foo' job = self.jobModel.save(job) resp = self.request('/job/%s' % job['_id']) self.assertStatusOk(resp) self.assertTrue('created' in resp.json) self.assertTrue('_some_other_field' not in resp.json) self.assertTrue('kwargs' not in resp.json) self.assertTrue('args' not in resp.json) resp = self.request('/job/%s' % job['_id'], user=self.users[0]) self.assertTrue('kwargs' in resp.json) self.assertTrue('args' in resp.json) self.jobModel.exposeFields(level=AccessType.READ, fields={'_some_other_field'}) self.jobModel.hideFields(level=AccessType.READ, fields={'created'}) resp = self.request('/job/%s' % job['_id']) self.assertStatusOk(resp) self.assertEqual(resp.json['_some_other_field'], 'foo') self.assertTrue('created' not in resp.json) def testJobProgressAndNotifications(self): job = self.jobModel.createJob(title='a job', type='t', user=self.users[1], public=True) path = '/job/%s' % job['_id'] resp = self.request(path) self.assertEqual(resp.json['progress'], None) self.assertEqual(resp.json['timestamps'], []) resp = self.request(path, method='PUT', user=self.users[1], params={ 'progressTotal': 100, 'progressCurrent': 3, 'progressMessage': 'Started', 'notify': 'false', 'status': JobStatus.QUEUED }) self.assertStatusOk(resp) self.assertEqual(resp.json['progress'], { 'total': 100, 'current': 3, 'message': 'Started', 'notificationId': None }) # The status update should make it so we now have a timestamp self.assertEqual(len(resp.json['timestamps']), 1) self.assertEqual(resp.json['timestamps'][0]['status'], JobStatus.QUEUED) self.assertIn('time', resp.json['timestamps'][0]) # If the status does not change on update, no timestamp should be added resp = self.request(path, method='PUT', user=self.users[1], params={ 'status': JobStatus.QUEUED }) self.assertStatusOk(resp) self.assertEqual(len(resp.json['timestamps']), 1) self.assertEqual(resp.json['timestamps'][0]['status'], JobStatus.QUEUED) # We passed notify=false, so we should only have the job creation notification resp = self.request(path='/notification/stream', method='GET', user=self.users[1], isJson=False, params={'timeout': 0}) messages = self.getSseMessages(resp) self.assertEqual(len(messages), 1) # Update progress with notify=true (the default) resp = self.request(path, method='PUT', user=self.users[1], params={ 'progressCurrent': 50, 'progressMessage': 'Something bad happened', 'status': JobStatus.ERROR }) self.assertStatusOk(resp) self.assertNotEqual(resp.json['progress']['notificationId'], None) # We should now see three notifications (job created + job status + progress) resp = self.request(path='/notification/stream', method='GET', user=self.users[1], isJson=False, params={'timeout': 0}) messages = self.getSseMessages(resp) job = self.jobModel.load(job['_id'], force=True) self.assertEqual(len(messages), 3) creationNotify = messages[0] progressNotify = messages[1] statusNotify = messages[2] self.assertEqual(creationNotify['type'], 'job_created') self.assertEqual(creationNotify['data']['_id'], str(job['_id'])) self.assertEqual(statusNotify['type'], 'job_status') self.assertEqual(statusNotify['data']['_id'], str(job['_id'])) self.assertEqual(int(statusNotify['data']['status']), JobStatus.ERROR) self.assertNotIn('kwargs', statusNotify['data']) self.assertNotIn('log', statusNotify['data']) self.assertEqual(progressNotify['type'], 'progress') self.assertEqual(progressNotify['data']['title'], job['title']) self.assertEqual(progressNotify['data']['current'], float(50)) self.assertEqual(progressNotify['data']['state'], 'error') self.assertEqual(progressNotify['_id'], str(job['progress']['notificationId'])) def testDotsInKwargs(self): kwargs = { '$key.with.dots': 'value', 'foo': [{ 'moar.dots': True }] } job = self.jobModel.createJob(title='dots', type='x', user=self.users[0], kwargs=kwargs) # Make sure we can update a job and notification creation works self.jobModel.updateJob(job, status=JobStatus.QUEUED, notify=True) self.assertEqual(job['kwargs'], kwargs) resp = self.request('/job/%s' % job['_id'], user=self.users[0]) self.assertStatusOk(resp) self.assertEqual(resp.json['kwargs'], kwargs) job = self.jobModel.load(job['_id'], force=True) self.assertEqual(job['kwargs'], kwargs) job = self.jobModel.filter(job, self.users[0]) self.assertEqual(job['kwargs'], kwargs) job = self.jobModel.filter(job, self.users[1]) self.assertFalse('kwargs' in job) def testLocalJob(self): job = self.jobModel.createLocalJob( title='local', type='local', user=self.users[0], kwargs={ 'hello': 'world' }, module='plugin_tests.local_job_impl') self.jobModel.scheduleJob(job) job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['job ran!']) job = self.jobModel.createLocalJob( title='local', type='local', user=self.users[0], kwargs={ 'hello': 'world' }, module='plugin_tests.local_job_impl', function='fail') self.jobModel.scheduleJob(job) job = self.jobModel.load(job['_id'], force=True, includeLog=True) self.assertEqual(job['log'], ['job failed']) def testValidateCustomStatus(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) def validateStatus(event): if event.info == 1234: event.preventDefault().addResponse(True) def validTransitions(event): if event.info['status'] == 1234: event.preventDefault().addResponse([JobStatus.INACTIVE]) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=1234) # Should fail with events.bound('jobs.status.validate', 'test', validateStatus), \ events.bound('jobs.status.validTransitions', 'test', validTransitions): self.jobModel.updateJob(job, status=1234) # Should work with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=4321) # Should fail def testValidateCustomStrStatus(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) def validateStatus(event): states = ['a', 'b', 'c'] if event.info in states: event.preventDefault().addResponse(True) def validTransitions(event): if event.info['status'] == 'a': event.preventDefault().addResponse([JobStatus.INACTIVE]) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status='a') with events.bound('jobs.status.validate', 'test', validateStatus), \ events.bound('jobs.status.validTransitions', 'test', validTransitions): self.jobModel.updateJob(job, status='a') self.assertEqual(job['status'], 'a') with self.assertRaises(ValidationException), \ events.bound('jobs.status.validate', 'test', validateStatus): self.jobModel.updateJob(job, status='foo') def testUpdateOtherFields(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) job = self.jobModel.updateJob(job, otherFields={'other': 'fields'}) self.assertEqual(job['other'], 'fields') def testCancelJob(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) # add to the log job = self.jobModel.updateJob(job, log='entry 1\n') # Reload without the log job = self.jobModel.load(id=job['_id'], force=True) self.assertEqual(len(job.get('log', [])), 0) # Cancel job = self.jobModel.cancelJob(job) self.assertEqual(job['status'], JobStatus.CANCELED) # Reloading should still have the log and be canceled job = self.jobModel.load(id=job['_id'], force=True, includeLog=True) self.assertEqual(job['status'], JobStatus.CANCELED) self.assertEqual(len(job.get('log', [])), 1) def testCancelJobEndpoint(self): job = self.jobModel.createJob(title='test', type='x', user=self.users[0]) # Ensure requires write perms jobCancelUrl = '/job/%s/cancel' % job['_id'] resp = self.request(jobCancelUrl, user=self.users[1], method='PUT') self.assertStatus(resp, 403) # Try again with the right user jobCancelUrl = '/job/%s/cancel' % job['_id'] resp = self.request(jobCancelUrl, user=self.users[0], method='PUT') self.assertStatusOk(resp) self.assertEqual(resp.json['status'], JobStatus.CANCELED) def testJobsTypesAndStatuses(self): self.jobModel.createJob(title='user 0 job', type='t1', user=self.users[0], public=False) self.jobModel.createJob(title='user 1 job', type='t2', user=self.users[1], public=False) self.jobModel.createJob(title='user 1 job', type='t3', user=self.users[1], public=True) self.jobModel.createJob(title='user 2 job', type='t4', user=self.users[2]) self.jobModel.createJob(title='anonymous job', type='t5') self.jobModel.createJob(title='anonymous public job', type='t6', public=True) # User 1, as non site admin, should encounter http 403 (Forbidden) resp = self.request('/job/typeandstatus/all', user=self.users[1]) self.assertStatus(resp, 403) # Admin user gets all types and statuses resp = self.request('/job/typeandstatus/all', user=self.users[0]) self.assertStatusOk(resp) self.assertEqual(len(resp.json['types']), 6) self.assertEqual(len(resp.json['statuses']), 1) # standard user gets types and statuses of its own jobs resp = self.request('/job/typeandstatus', user=self.users[1]) self.assertStatusOk(resp) self.assertEqual(len(resp.json['types']), 2) self.assertEqual(len(resp.json['statuses']), 1) def testDefaultParentId(self): job = self.jobModel.createJob(title='Job', type='Job', user=self.users[0]) # If not specified parentId should be None self.assertEquals(job['parentId'], None) def testIsParentIdCorrect(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) # During initialization parent job should be set correctly self.assertEqual(childJob['parentId'], parentJob['_id']) def testSetParentCorrectly(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob(title='Child Job', type='Child Job', user=self.users[0]) self.jobModel.setParentJob(childJob, parentJob) # After setParentJob method is called parent job should be set correctly self.assertEqual(childJob['parentId'], parentJob['_id']) def testParentCannotBeEqualToChild(self): childJob = self.jobModel.createJob(title='Child Job', type='Child Job', user=self.users[0]) # Cannot set a job as it's own parent with self.assertRaises(ValidationException): self.jobModel.setParentJob(childJob, childJob) def testParentIdCannotBeOverridden(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) anotherParentJob = self.jobModel.createJob( title='Another Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) with self.assertRaises(ValidationException): # If parent job is set, cannot be overridden self.jobModel.setParentJob(childJob, anotherParentJob) def testListChildJobs(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) self.jobModel.createJob( title='Another Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) # Should return a list with 2 jobs self.assertEquals(len(list(self.jobModel.listChildJobs(parentJob))), 2) # Should return an empty list self.assertEquals(len(list(self.jobModel.listChildJobs(childJob))), 0) def testListChildJobsRest(self): parentJob = self.jobModel.createJob( title='Parent Job', type='Parent Job', user=self.users[0]) childJob = self.jobModel.createJob( title='Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) self.jobModel.createJob( title='Another Child Job', type='Child Job', user=self.users[0], parentJob=parentJob) resp = self.request('/job', user=self.users[0], params={'parentId': str(parentJob['_id'])}) resp2 = self.request('/job', user=self.users[0], params={'parentId': str(childJob['_id'])}) self.assertStatusOk(resp) self.assertStatusOk(resp2) # Should return a list with 2 jobs self.assertEquals(len(resp.json), 2) # Should return an empty list self.assertEquals(len(resp2.json), 0) def testCreateJobRest(self): resp = self.request('/job', method='POST', user=self.users[0], params={'title': 'job', 'type': 'job'}) # If user does not have the necessary token status is 403 self.assertStatus(resp, 403) token = Token().createToken(scope=REST_CREATE_JOB_TOKEN_SCOPE) resp2 = self.request( '/job', method='POST', token=token, params={'title': 'job', 'type': 'job'}) # If user has the necessary token status is 200 self.assertStatusOk(resp2) def testJobStateTransitions(self): job = self.jobModel.createJob( title='user 0 job', type='t1', user=self.users[0], public=False) # We can't move straight to SUCCESS with self.assertRaises(ValidationException): job = self.jobModel.updateJob(job, status=JobStatus.SUCCESS) self.jobModel.updateJob(job, status=JobStatus.QUEUED) self.jobModel.updateJob(job, status=JobStatus.RUNNING) self.jobModel.updateJob(job, status=JobStatus.ERROR) # We shouldn't be able to move backwards with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=JobStatus.QUEUED) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=JobStatus.RUNNING) with self.assertRaises(ValidationException): self.jobModel.updateJob(job, status=JobStatus.INACTIVE)
def testWorker(self): # Test the settings resp = self.request('/system/setting', method='PUT', params={ 'list': json.dumps([{ 'key': worker.PluginSettings.BROKER, 'value': 'amqp://[email protected]' }, { 'key': worker.PluginSettings.BACKEND, 'value': 'amqp://[email protected]' }]) }, user=self.admin) self.assertStatusOk(resp) # Create a job to be handled by the worker plugin from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob(title='title', type='foo', handler='worker_handler', user=self.admin, public=False, args=(), kwargs={}) job['kwargs'] = { 'jobInfo': utils.jobInfoSpec(job), 'inputs': [utils.girderInputSpec(self.adminFolder, resourceType='folder')], 'outputs': [utils.girderOutputSpec(self.adminFolder, token=self.adminToken)] } job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock: instance = celeryMock.return_value instance.send_task.return_value = FakeAsyncResult() jobModel.scheduleJob(job) # Make sure we sent the job to celery self.assertEqual(len(celeryMock.mock_calls), 2) self.assertEqual(celeryMock.mock_calls[0][1], ('girder_worker', )) self.assertEqual( celeryMock.mock_calls[0][2], { 'broker': 'amqp://[email protected]', 'backend': 'amqp://[email protected]' }) sendTaskCalls = celeryMock.return_value.send_task.mock_calls self.assertEqual(len(sendTaskCalls), 1) self.assertEqual(sendTaskCalls[0][1], ('girder_worker.run', job['args'], job['kwargs'])) self.assertTrue('headers' in sendTaskCalls[0][2]) self.assertTrue('jobInfoSpec' in sendTaskCalls[0][2]['headers']) # Make sure we got and saved the celery task id job = jobModel.load(job['_id'], force=True) self.assertEqual(job['celeryTaskId'], 'fake_id') self.assertEqual(job['status'], JobStatus.QUEUED)
def testWorkerDifferentTask(self): # Test the settings resp = self.request('/system/setting', method='PUT', params={ 'key': worker.PluginSettings.API_URL, 'value': 'bad value' }, user=self.admin) self.assertStatus(resp, 400) self.assertEqual(resp.json['message'], 'API URL must start with http:// or https://.') resp = self.request('/system/setting', method='PUT', params={ 'list': json.dumps([{ 'key': worker.PluginSettings.BROKER, 'value': 'amqp://[email protected]' }, { 'key': worker.PluginSettings.BACKEND, 'value': 'amqp://[email protected]' }]) }, user=self.admin) self.assertStatusOk(resp) # Create a job to be handled by the worker plugin from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob(title='title', type='foo', handler='worker_handler', user=self.admin, public=False, args=(), kwargs={}, otherFields={ 'celeryTaskName': 'some_other.task', 'celeryQueue': 'my_other_q' }) job['kwargs'] = { 'jobInfo': utils.jobInfoSpec(job), 'inputs': [utils.girderInputSpec(self.adminFolder, resourceType='folder')], 'outputs': [utils.girderOutputSpec(self.adminFolder, token=self.adminToken)] } job = jobModel.save(job) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock: instance = celeryMock.return_value instance.send_task.return_value = FakeAsyncResult() jobModel.scheduleJob(job) sendTaskCalls = celeryMock.return_value.send_task.mock_calls self.assertEqual(len(sendTaskCalls), 1) self.assertEqual(sendTaskCalls[0][1], ('some_other.task', job['args'], job['kwargs'])) self.assertIn('queue', sendTaskCalls[0][2]) self.assertEqual(sendTaskCalls[0][2]['queue'], 'my_other_q')
def testImageBuild(self, it): resp = self.request(path='/tale', method='POST', user=self.user, type='application/json', body=json.dumps({ 'imageId': str(self.image['_id']), 'dataSet': [] })) self.assertStatusOk(resp) tale = resp.json with mock.patch('girder_worker.task.celery.Task.apply_async', spec=True) \ as mock_apply_async: mock_apply_async().job.return_value = json.dumps({ 'job': 1, 'blah': 2 }) resp = self.request(path='/tale/{}/build'.format(tale['_id']), method='PUT', user=self.user) self.assertStatusOk(resp) job_call = mock_apply_async.call_args_list[-1][-1] self.assertEqual(job_call['args'], (str(tale['_id']), False)) self.assertEqual(job_call['headers']['girder_job_title'], 'Build Tale Image') self.assertStatusOk(resp) # Create a job to be handled by the worker plugin from girder.plugins.jobs.models.job import Job jobModel = Job() job = jobModel.createJob(title='Build Tale Image', type='celery', handler='worker_handler', user=self.user, public=False, args=[str(tale['_id'])], kwargs={}) job = jobModel.save(job) self.assertEqual(job['status'], JobStatus.INACTIVE) # Schedule the job, make sure it is sent to celery with mock.patch('celery.Celery') as celeryMock, \ mock.patch('girder.plugins.worker.getCeleryApp') as gca: celeryMock().AsyncResult.return_value = FakeAsyncResult( tale['_id']) gca().send_task.return_value = FakeAsyncResult(tale['_id']) jobModel.scheduleJob(job) for i in range(20): job = jobModel.load(job['_id'], force=True) if job['status'] == JobStatus.QUEUED: break time.sleep(0.1) self.assertEqual(job['status'], JobStatus.QUEUED) tale = Tale().load(tale['_id'], force=True) self.assertEqual(tale['imageInfo']['status'], ImageStatus.BUILDING) # Set status to RUNNING job = jobModel.load(job['_id'], force=True) self.assertEqual(job['celeryTaskId'], 'fake_id') Job().updateJob(job, log='job running', status=JobStatus.RUNNING) tale = Tale().load(tale['_id'], force=True) self.assertEqual(tale['imageInfo']['status'], ImageStatus.BUILDING) # Set status to SUCCESS job = jobModel.load(job['_id'], force=True) self.assertEqual(job['celeryTaskId'], 'fake_id') Job().updateJob(job, log='job running', status=JobStatus.SUCCESS) tale = Tale().load(tale['_id'], force=True) self.assertEqual(tale['imageInfo']['status'], ImageStatus.AVAILABLE) self.assertEqual(tale['imageInfo']['digest'], 'digest123')