diff --git a/moto/s3/models.py b/moto/s3/models.py
index f3994b5d..4b1a343d 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -341,8 +341,9 @@ class LifecycleAndFilter(BaseModel):
class LifecycleRule(BaseModel):
def __init__(self, id=None, prefix=None, lc_filter=None, status=None, expiration_days=None,
- expiration_date=None, transition_days=None, expired_object_delete_marker=None,
- transition_date=None, storage_class=None):
+ expiration_date=None, transition_days=None, transition_date=None, storage_class=None,
+ expired_object_delete_marker=None, nve_noncurrent_days=None, nvt_noncurrent_days=None,
+ nvt_storage_class=None, aimu_days=None):
self.id = id
self.prefix = prefix
self.filter = lc_filter
@@ -351,8 +352,12 @@ class LifecycleRule(BaseModel):
self.expiration_date = expiration_date
self.transition_days = transition_days
self.transition_date = transition_date
- self.expired_object_delete_marker = expired_object_delete_marker
self.storage_class = storage_class
+ self.expired_object_delete_marker = expired_object_delete_marker
+ self.nve_noncurrent_days = nve_noncurrent_days
+ self.nvt_noncurrent_days = nvt_noncurrent_days
+ self.nvt_storage_class = nvt_storage_class
+ self.aimu_days = aimu_days
class CorsRule(BaseModel):
@@ -414,8 +419,12 @@ class FakeBucket(BaseModel):
def set_lifecycle(self, rules):
self.rules = []
for rule in rules:
+ # Extract actions from Lifecycle rule
expiration = rule.get('Expiration')
transition = rule.get('Transition')
+ nve = rule.get('NoncurrentVersionExpiration')
+ nvt = rule.get('NoncurrentVersionTransition')
+ aimu = rule.get('AbortIncompleteMultipartUpload')
eodm = None
if expiration and expiration.get("ExpiredObjectDeleteMarker") is not None:
@@ -459,11 +468,14 @@ class FakeBucket(BaseModel):
status=rule['Status'],
expiration_days=expiration.get('Days') if expiration else None,
expiration_date=expiration.get('Date') if expiration else None,
- expired_object_delete_marker=eodm,
transition_days=transition.get('Days') if transition else None,
transition_date=transition.get('Date') if transition else None,
- storage_class=transition[
- 'StorageClass'] if transition else None,
+ storage_class=transition.get('StorageClass') if transition else None,
+ expired_object_delete_marker=eodm,
+ nve_noncurrent_days = nve.get('NoncurrentDays') if nve else None,
+ nvt_noncurrent_days = nvt.get('NoncurrentDays') if nvt else None,
+ nvt_storage_class = nvt.get('StorageClass') if nvt else None,
+ aimu_days=aimu.get('DaysAfterInitiation') if aimu else None,
))
def delete_lifecycle(self):
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index f8dc7e42..de101a19 100755
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1228,6 +1228,22 @@ S3_BUCKET_LIFECYCLE_CONFIGURATION = """
{% endif %}
{% endif %}
+ {% if rule.nvt_noncurrent_days and rule.nvt_storage_class %}
+
+ {{ rule.nvt_noncurrent_days }}
+ {{ rule.nvt_storage_class }}
+
+ {% endif %}
+ {% if rule.nve_noncurrent_days %}
+
+ {{ rule.nve_noncurrent_days }}
+
+ {% endif %}
+ {% if rule.aimu_days %}
+
+ {{ rule.aimu_days }}
+
+ {% endif %}
{% endfor %}
diff --git a/tests/test_s3/test_s3_lifecycle.py b/tests/test_s3/test_s3_lifecycle.py
index d176e95c..9c4bd49b 100644
--- a/tests/test_s3/test_s3_lifecycle.py
+++ b/tests/test_s3/test_s3_lifecycle.py
@@ -191,6 +191,135 @@ def test_lifecycle_with_eodm():
assert err.exception.response["Error"]["Code"] == "MalformedXML"
+@mock_s3
+def test_lifecycle_with_nve():
+ client = boto3.client("s3")
+ client.create_bucket(Bucket="bucket")
+
+ lfc = {
+ "Rules": [
+ {
+ "NoncurrentVersionExpiration": {
+ "NoncurrentDays": 30
+ },
+ "ID": "wholebucket",
+ "Filter": {
+ "Prefix": ""
+ },
+ "Status": "Enabled"
+ }
+ ]
+ }
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["NoncurrentVersionExpiration"]["NoncurrentDays"] == 30
+
+ # Change NoncurrentDays:
+ lfc["Rules"][0]["NoncurrentVersionExpiration"]["NoncurrentDays"] = 10
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["NoncurrentVersionExpiration"]["NoncurrentDays"] == 10
+
+ # With failures for missing children:
+ del lfc["Rules"][0]["NoncurrentVersionExpiration"]["NoncurrentDays"]
+ with assert_raises(ClientError) as err:
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ assert err.exception.response["Error"]["Code"] == "MalformedXML"
+
+
+@mock_s3
+def test_lifecycle_with_nvt():
+ client = boto3.client("s3")
+ client.create_bucket(Bucket="bucket")
+
+ lfc = {
+ "Rules": [
+ {
+ "NoncurrentVersionTransition": {
+ "NoncurrentDays": 30,
+ "StorageClass": "ONEZONE_IA"
+ },
+ "ID": "wholebucket",
+ "Filter": {
+ "Prefix": ""
+ },
+ "Status": "Enabled"
+ }
+ ]
+ }
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["NoncurrentVersionTransition"]["NoncurrentDays"] == 30
+ assert result["Rules"][0]["NoncurrentVersionTransition"]["StorageClass"] == "ONEZONE_IA"
+
+ # Change NoncurrentDays:
+ lfc["Rules"][0]["NoncurrentVersionTransition"]["NoncurrentDays"] = 10
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["NoncurrentVersionTransition"]["NoncurrentDays"] == 10
+
+ # Change StorageClass:
+ lfc["Rules"][0]["NoncurrentVersionTransition"]["StorageClass"] = "GLACIER"
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["NoncurrentVersionTransition"]["StorageClass"] == "GLACIER"
+
+ # With failures for missing children:
+ del lfc["Rules"][0]["NoncurrentVersionTransition"]["NoncurrentDays"]
+ with assert_raises(ClientError) as err:
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ assert err.exception.response["Error"]["Code"] == "MalformedXML"
+ lfc["Rules"][0]["NoncurrentVersionTransition"]["NoncurrentDays"] = 30
+
+ del lfc["Rules"][0]["NoncurrentVersionTransition"]["StorageClass"]
+ with assert_raises(ClientError) as err:
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ assert err.exception.response["Error"]["Code"] == "MalformedXML"
+
+
+@mock_s3
+def test_lifecycle_with_aimu():
+ client = boto3.client("s3")
+ client.create_bucket(Bucket="bucket")
+
+ lfc = {
+ "Rules": [
+ {
+ "AbortIncompleteMultipartUpload": {
+ "DaysAfterInitiation": 7
+ },
+ "ID": "wholebucket",
+ "Filter": {
+ "Prefix": ""
+ },
+ "Status": "Enabled"
+ }
+ ]
+ }
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["AbortIncompleteMultipartUpload"]["DaysAfterInitiation"] == 7
+
+ # Change DaysAfterInitiation:
+ lfc["Rules"][0]["AbortIncompleteMultipartUpload"]["DaysAfterInitiation"] = 30
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ result = client.get_bucket_lifecycle_configuration(Bucket="bucket")
+ assert len(result["Rules"]) == 1
+ assert result["Rules"][0]["AbortIncompleteMultipartUpload"]["DaysAfterInitiation"] == 30
+
+ # With failures for missing children:
+ del lfc["Rules"][0]["AbortIncompleteMultipartUpload"]["DaysAfterInitiation"]
+ with assert_raises(ClientError) as err:
+ client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc)
+ assert err.exception.response["Error"]["Code"] == "MalformedXML"
+
+
@mock_s3_deprecated
def test_lifecycle_with_glacier_transition():
conn = boto.s3.connect_to_region("us-west-1")