added IoT job_execution and job mocks

This commit is contained in:
Stephan 2019-01-07 14:22:12 +01:00
parent 3ea673b3d0
commit cfd12b6d19
5 changed files with 1399 additions and 72 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,42 @@
from __future__ import unicode_literals
from moto.core.exceptions import JsonRESTError
class IoTClientError(JsonRESTError):
code = 400
class ResourceNotFoundException(IoTClientError):
def __init__(self):
self.code = 404
super(ResourceNotFoundException, self).__init__(
"ResourceNotFoundException",
"The specified resource does not exist"
)
class InvalidRequestException(IoTClientError):
def __init__(self, msg=None):
self.code = 400
super(InvalidRequestException, self).__init__(
"InvalidRequestException",
msg or "The request is not valid."
)
class VersionConflictException(IoTClientError):
def __init__(self, name):
self.code = 409
super(VersionConflictException, self).__init__(
'VersionConflictException',
'The version for thing %s does not match the expected version.' % name
)
from __future__ import unicode_literals
from moto.core.exceptions import JsonRESTError
class IoTClientError(JsonRESTError):
code = 400
class ResourceNotFoundException(IoTClientError):
def __init__(self):
self.code = 404
super(ResourceNotFoundException, self).__init__(
"ResourceNotFoundException",
"The specified resource does not exist"
)
class InvalidRequestException(IoTClientError):
def __init__(self, msg=None):
self.code = 400
super(InvalidRequestException, self).__init__(
"InvalidRequestException",
msg or "The request is not valid."
)
class InvalidStateTransitionException(IoTClientError):
def __init__(self, msg=None):
self.code = 409
super(InvalidStateTransitionException, self).__init__(
"InvalidStateTransitionException",
msg or "An attempt was made to change to an invalid state."
)
class VersionConflictException(IoTClientError):
def __init__(self, name):
self.code = 409
super(VersionConflictException, self).__init__(
'VersionConflictException',
'The version for thing %s does not match the expected version.' % name
)

View File

@ -15,6 +15,7 @@ from moto.core import BaseBackend, BaseModel
from .exceptions import (
ResourceNotFoundException,
InvalidRequestException,
InvalidStateTransitionException,
VersionConflictException
)
@ -247,7 +248,6 @@ class FakeJob(BaseModel):
self.document_parameters = document_parameters
def to_dict(self):
obj = {
'jobArn': self.job_arn,
'jobId': self.job_id,
@ -260,7 +260,7 @@ class FakeJob(BaseModel):
'comment': self.comment,
'createdAt': self.created_at,
'lastUpdatedAt': self.last_updated_at,
'completedAt': self.completedAt,
'completedAt': self.completed_at,
'jobProcessDetails': self.job_process_details,
'documentParameters': self.document_parameters,
'document': self.document,
@ -290,13 +290,13 @@ class FakeJobExecution(BaseModel):
self.version_number = 123
self.approximate_seconds_before_time_out = 123
def to_dict(self):
def to_get_dict(self):
obj = {
'jobId': self.job_id,
'status': self.status,
'forceCancel': self.force_canceled,
'forceCanceled': self.force_canceled,
'statusDetails': {'detailsMap': self.status_details_map},
'thing_arn': self.thing_arn,
'thingArn': self.thing_arn,
'queuedAt': self.queued_at,
'startedAt': self.started_at,
'lastUpdatedAt': self.last_updated_at,
@ -307,6 +307,21 @@ class FakeJobExecution(BaseModel):
return obj
def to_dict(self):
obj = {
'jobId': self.job_id,
'thingArn': self.thing_arn,
'jobExecutionSummary': {
'status': self.status,
'queuedAt': self.queued_at,
'startedAt': self.started_at,
'lastUpdatedAt': self.last_updated_at,
'executionNumber': self.execution_number,
}
}
return obj
class IoTBackend(BaseBackend):
def __init__(self, region_name=None):
@ -760,24 +775,114 @@ class IoTBackend(BaseBackend):
self.jobs[job_id] = job
for thing_arn in targets:
thing_name = thing_arn.split(':')[-1]
thing_name = thing_arn.split(':')[-1].split('/')[-1]
job_execution = FakeJobExecution(job_id, thing_arn)
self.job_executions[(job_id, thing_name)] = job_execution
return job.job_arn, job_id, description
def describe_job(self, job_id):
return self.jobs[job_id]
jobs = [_ for _ in self.jobs.values() if _.job_id == job_id]
if len(jobs) == 0:
raise ResourceNotFoundException()
return jobs[0]
def get_job_document(self, job_id):
return self.jobs[job_id]
def list_jobs(self, status, target_selection, max_results, token, thing_group_name, thing_group_id):
# TODO: implement filters
all_jobs = [_.to_dict() for _ in self.jobs.values()]
filtered_jobs = all_jobs
if token is None:
jobs = filtered_jobs[0:max_results]
next_token = str(max_results) if len(filtered_jobs) > max_results else None
else:
token = int(token)
jobs = filtered_jobs[token:token + max_results]
next_token = str(token + max_results) if len(filtered_jobs) > token + max_results else None
return jobs, next_token
def describe_job_execution(self, job_id, thing_name, execution_number):
# TODO filter with execution number
return self.job_executions[(job_id, thing_name)]
try:
job_execution = self.job_executions[(job_id, thing_name)]
except KeyError:
raise ResourceNotFoundException()
if job_execution is None or \
(execution_number is not None and job_execution.execution_number != execution_number):
raise ResourceNotFoundException()
return job_execution
def cancel_job_execution(self, job_id, thing_name, force, expected_version, status_details):
job_execution = self.job_executions[(job_id, thing_name)]
if job_execution is None:
raise ResourceNotFoundException()
job_execution.force_canceled = force if force is not None else job_execution.force_canceled
# TODO: implement expected_version and status_details (at most 10 can be specified)
if job_execution.status == 'IN_PROGRESS' and force:
job_execution.status = 'CANCELED'
self.job_executions[(job_id, thing_name)] = job_execution
elif job_execution.status != 'IN_PROGRESS':
job_execution.status = 'CANCELED'
self.job_executions[(job_id, thing_name)] = job_execution
else:
raise InvalidStateTransitionException()
def delete_job_execution(self, job_id, thing_name, execution_number, force):
job_execution = self.job_executions[(job_id, thing_name)]
if job_execution.execution_number != execution_number:
raise ResourceNotFoundException()
if job_execution.status == 'IN_PROGRESS' and force:
del self.job_executions[(job_id, thing_name)]
elif job_execution.status != 'IN_PROGRESS':
del self.job_executions[(job_id, thing_name)]
else:
raise InvalidStateTransitionException()
def list_job_executions_for_job(self, job_id, status, max_results, next_token):
job_executions = [self.job_executions[je] for je in self.job_executions if je[0] == job_id]
# TODO: implement filters
job_executions = [self.job_executions[je].to_dict() for je in self.job_executions if je[0] == job_id]
if status is not None:
job_executions = list(filter(lambda elem:
status in elem["status"] and
elem["status"] == status, job_executions))
token = next_token
if token is None:
job_executions = job_executions[0:max_results]
next_token = str(max_results) if len(job_executions) > max_results else None
else:
token = int(token)
job_executions = job_executions[token:token + max_results]
next_token = str(token + max_results) if len(job_executions) > token + max_results else None
return job_executions, next_token
def list_job_executions_for_thing(self, thing_name, status, max_results, next_token):
job_executions = [self.job_executions[je].to_dict() for je in self.job_executions if je[1] == thing_name]
if status is not None:
job_executions = list(filter(lambda elem:
status in elem["status"] and
elem["status"] == status, job_executions))
token = next_token
if token is None:
job_executions = job_executions[0:max_results]
next_token = str(max_results) if len(job_executions) > max_results else None
else:
token = int(token)
job_executions = job_executions[token:token + max_results]
next_token = str(token + max_results) if len(job_executions) > token + max_results else None
return job_executions, next_token

View File

@ -160,15 +160,83 @@ class IoTResponse(BaseResponse):
# TODO: needs to be implemented to get document_source's content from S3
return json.dumps({'document': ''})
def list_job_executions_for_job(self):
job_executions, next_token = self.iot_backend.list_job_executions_for_job(job_id=self._get_param("jobId"),
status=self._get_param("status"),
max_results=self._get_param(
"maxResults"),
next_token=self._get_param(
"nextToken"))
def list_jobs(self):
status = self._get_param("status"),
target_selection = self._get_param("targetSelection"),
max_results = self._get_int_param("maxResults", 50) # not the default, but makes testing easier
previous_next_token = self._get_param("nextToken")
thing_group_name = self._get_param("thingGroupName"),
thing_group_id = self._get_param("thingGroupId")
jobs, next_token = self.iot_backend.list_jobs(status=status,
target_selection=target_selection,
max_results=max_results,
token=previous_next_token,
thing_group_name=thing_group_name,
thing_group_id=thing_group_id)
return json.dumps(dict(executionSummaries=[_.to_dict() for _ in job_executions], nextToken=next_token))
return json.dumps(dict(jobs=jobs, nextToken=next_token))
def describe_job_execution(self):
job_id = self._get_param("jobId")
thing_name = self._get_param("thingName")
execution_number = self._get_int_param("executionNumber")
job_execution = self.iot_backend.describe_job_execution(job_id=job_id,
thing_name=thing_name,
execution_number=execution_number)
return json.dumps(dict(execution=job_execution.to_get_dict()))
def cancel_job_execution(self):
job_id = self._get_param("jobId")
thing_name = self._get_param("thingName")
force = self._get_bool_param("force")
expected_version = self._get_int_param("expectedVersion")
status_details = self._get_param("statusDetails")
self.iot_backend.cancel_job_execution(job_id=job_id,
thing_name=thing_name,
force=force,
expected_version=expected_version,
status_details=status_details)
return json.dumps(dict())
def delete_job_execution(self):
job_id = self._get_param("jobId")
thing_name = self._get_param("thingName")
execution_number = self._get_int_param("executionNumber")
force = self._get_bool_param("force")
self.iot_backend.delete_job_execution(job_id=job_id,
thing_name=thing_name,
execution_number=execution_number,
force=force)
return json.dumps(dict())
def list_job_executions_for_job(self):
job_id = self._get_param("jobId")
status = self._get_param("status")
max_results = self._get_int_param("maxResults", 50) # not the default, but makes testing easier
next_token = self._get_param("nextToken")
job_executions, next_token = self.iot_backend.list_job_executions_for_job(job_id=job_id,
status=status,
max_results=max_results,
next_token=next_token)
return json.dumps(dict(executionSummaries=job_executions, nextToken=next_token))
def list_job_executions_for_thing(self):
thing_name = self._get_param("thingName")
status = self._get_param("status")
max_results = self._get_int_param("maxResults", 50) # not the default, but makes testing easier
next_token = self._get_param("nextToken")
job_executions, next_token = self.iot_backend.list_job_executions_for_thing(thing_name=thing_name,
status=status,
max_results=max_results,
next_token=next_token)
return json.dumps(dict(executionSummaries=job_executions, nextToken=next_token))
def create_keys_and_certificate(self):
set_as_active = self._get_bool_param("setAsActive")

View File

@ -4,9 +4,9 @@ import json
import sure #noqa
import boto3
from botocore.exceptions import ClientError
from moto import mock_iot
@mock_iot
def test_attach_policy():
client = boto3.client('iot', region_name='ap-northeast-1')
@ -711,6 +711,69 @@ def test_create_job():
job.should.have.key('description')
@mock_iot
def test_list_jobs():
client = boto3.client('iot', region_name='eu-west-1')
name = "my-thing"
job_id = "TestJob"
# thing# job document
# job_document = {
# "field": "value"
# }
thing = client.create_thing(thingName=name)
thing.should.have.key('thingName').which.should.equal(name)
thing.should.have.key('thingArn')
# job document
job_document = {
"field": "value"
}
job1 = client.create_job(
jobId=job_id,
targets=[thing["thingArn"]],
document=json.dumps(job_document),
description="Description",
presignedUrlConfig={
'roleArn': 'arn:aws:iam::1:role/service-role/iot_job_role',
'expiresInSec': 123
},
targetSelection="CONTINUOUS",
jobExecutionsRolloutConfig={
'maximumPerMinute': 10
}
)
job1.should.have.key('jobId').which.should.equal(job_id)
job1.should.have.key('jobArn')
job1.should.have.key('description')
job2 = client.create_job(
jobId=job_id+"1",
targets=[thing["thingArn"]],
document=json.dumps(job_document),
description="Description",
presignedUrlConfig={
'roleArn': 'arn:aws:iam::1:role/service-role/iot_job_role',
'expiresInSec': 123
},
targetSelection="CONTINUOUS",
jobExecutionsRolloutConfig={
'maximumPerMinute': 10
}
)
job2.should.have.key('jobId').which.should.equal(job_id+"1")
job2.should.have.key('jobArn')
job2.should.have.key('description')
jobs = client.list_jobs()
jobs.should.have.key('jobs')
jobs.should_not.have.key('nextToken')
jobs['jobs'][0].should.have.key('jobId').which.should.equal(job_id)
jobs['jobs'][1].should.have.key('jobId').which.should.equal(job_id+"1")
@mock_iot
def test_describe_job():
client = boto3.client('iot', region_name='eu-west-1')
@ -875,6 +938,162 @@ def test_get_job_document_with_document():
job_document = client.get_job_document(jobId=job_id)
job_document.should.have.key('document').which.should.equal("{\"field\": \"value\"}")
@mock_iot
def test_describe_job_execution():
client = boto3.client('iot', region_name='eu-west-1')
name = "my-thing"
job_id = "TestJob"
# thing
thing = client.create_thing(thingName=name)
thing.should.have.key('thingName').which.should.equal(name)
thing.should.have.key('thingArn')
# job document
job_document = {
"field": "value"
}
job = client.create_job(
jobId=job_id,
targets=[thing["thingArn"]],
document=json.dumps(job_document),
description="Description",
presignedUrlConfig={
'roleArn': 'arn:aws:iam::1:role/service-role/iot_job_role',
'expiresInSec': 123
},
targetSelection="CONTINUOUS",
jobExecutionsRolloutConfig={
'maximumPerMinute': 10
}
)
job.should.have.key('jobId').which.should.equal(job_id)
job.should.have.key('jobArn')
job.should.have.key('description')
job_execution = client.describe_job_execution(jobId=job_id, thingName=name)
job_execution.should.have.key('execution')
job_execution['execution'].should.have.key('jobId').which.should.equal(job_id)
job_execution['execution'].should.have.key('status').which.should.equal('QUEUED')
job_execution['execution'].should.have.key('forceCanceled').which.should.equal(False)
job_execution['execution'].should.have.key('statusDetails').which.should.equal({'detailsMap': {}})
job_execution['execution'].should.have.key('thingArn').which.should.equal(thing["thingArn"])
job_execution['execution'].should.have.key('queuedAt')
job_execution['execution'].should.have.key('startedAt')
job_execution['execution'].should.have.key('lastUpdatedAt')
job_execution['execution'].should.have.key('executionNumber').which.should.equal(123)
job_execution['execution'].should.have.key('versionNumber').which.should.equal(123)
job_execution['execution'].should.have.key('approximateSecondsBeforeTimedOut').which.should.equal(123)
job_execution = client.describe_job_execution(jobId=job_id, thingName=name, executionNumber=123)
job_execution.should.have.key('execution')
job_execution['execution'].should.have.key('jobId').which.should.equal(job_id)
job_execution['execution'].should.have.key('status').which.should.equal('QUEUED')
job_execution['execution'].should.have.key('forceCanceled').which.should.equal(False)
job_execution['execution'].should.have.key('statusDetails').which.should.equal({'detailsMap': {}})
job_execution['execution'].should.have.key('thingArn').which.should.equal(thing["thingArn"])
job_execution['execution'].should.have.key('queuedAt')
job_execution['execution'].should.have.key('startedAt')
job_execution['execution'].should.have.key('lastUpdatedAt')
job_execution['execution'].should.have.key('executionNumber').which.should.equal(123)
job_execution['execution'].should.have.key('versionNumber').which.should.equal(123)
job_execution['execution'].should.have.key('approximateSecondsBeforeTimedOut').which.should.equal(123)
try:
client.describe_job_execution(jobId=job_id, thingName=name, executionNumber=456)
except ClientError as exc:
error_code = exc.response['Error']['Code']
error_code.should.equal('ResourceNotFoundException')
else:
raise Exception("Should have raised error")
@mock_iot
def test_cancel_job_execution():
client = boto3.client('iot', region_name='eu-west-1')
name = "my-thing"
job_id = "TestJob"
# thing
thing = client.create_thing(thingName=name)
thing.should.have.key('thingName').which.should.equal(name)
thing.should.have.key('thingArn')
# job document
job_document = {
"field": "value"
}
job = client.create_job(
jobId=job_id,
targets=[thing["thingArn"]],
document=json.dumps(job_document),
description="Description",
presignedUrlConfig={
'roleArn': 'arn:aws:iam::1:role/service-role/iot_job_role',
'expiresInSec': 123
},
targetSelection="CONTINUOUS",
jobExecutionsRolloutConfig={
'maximumPerMinute': 10
}
)
job.should.have.key('jobId').which.should.equal(job_id)
job.should.have.key('jobArn')
job.should.have.key('description')
client.cancel_job_execution(jobId=job_id, thingName=name)
job_execution = client.describe_job_execution(jobId=job_id, thingName=name)
job_execution.should.have.key('execution')
job_execution['execution'].should.have.key('status').which.should.equal('CANCELED')
@mock_iot
def test_delete_job_execution():
client = boto3.client('iot', region_name='eu-west-1')
name = "my-thing"
job_id = "TestJob"
# thing
thing = client.create_thing(thingName=name)
thing.should.have.key('thingName').which.should.equal(name)
thing.should.have.key('thingArn')
# job document
job_document = {
"field": "value"
}
job = client.create_job(
jobId=job_id,
targets=[thing["thingArn"]],
document=json.dumps(job_document),
description="Description",
presignedUrlConfig={
'roleArn': 'arn:aws:iam::1:role/service-role/iot_job_role',
'expiresInSec': 123
},
targetSelection="CONTINUOUS",
jobExecutionsRolloutConfig={
'maximumPerMinute': 10
}
)
job.should.have.key('jobId').which.should.equal(job_id)
job.should.have.key('jobArn')
job.should.have.key('description')
client.delete_job_execution(jobId=job_id, thingName=name, executionNumber=123)
try:
client.describe_job_execution(jobId=job_id, thingName=name, executionNumber=123)
except ClientError as exc:
error_code = exc.response['Error']['Code']
error_code.should.equal('ResourceNotFoundException')
else:
raise Exception("Should have raised error")
@mock_iot
def test_list_job_executions_for_job():
client = boto3.client('iot', region_name='eu-west-1')
@ -911,5 +1130,44 @@ def test_list_job_executions_for_job():
job_execution = client.list_job_executions_for_job(jobId=job_id)
job_execution.should.have.key('executionSummaries')
job_execution['executionSummaries'][0].should.have.key('thingArn').which.should.equal(thing["thingArn"])
@mock_iot
def test_list_job_executions_for_thing():
client = boto3.client('iot', region_name='eu-west-1')
name = "my-thing"
job_id = "TestJob"
# thing
thing = client.create_thing(thingName=name)
thing.should.have.key('thingName').which.should.equal(name)
thing.should.have.key('thingArn')
# job document
job_document = {
"field": "value"
}
job = client.create_job(
jobId=job_id,
targets=[thing["thingArn"]],
document=json.dumps(job_document),
description="Description",
presignedUrlConfig={
'roleArn': 'arn:aws:iam::1:role/service-role/iot_job_role',
'expiresInSec': 123
},
targetSelection="CONTINUOUS",
jobExecutionsRolloutConfig={
'maximumPerMinute': 10
}
)
job.should.have.key('jobId').which.should.equal(job_id)
job.should.have.key('jobArn')
job.should.have.key('description')
job_execution = client.list_job_executions_for_thing(thingName=name)
job_execution.should.have.key('executionSummaries')
job_execution['executionSummaries'][0].should.have.key('jobId').which.should.equal(job_id)