Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port #51684 to master #57500

Merged
merged 6 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/57500.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added resource tagging functions to boto_dynamodb execution module
134 changes: 133 additions & 1 deletion salt/modules/boto_dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@
import salt.utils.versions
from salt.exceptions import SaltInvocationError

log = logging.getLogger(__name__)
logging.getLogger("boto").setLevel(logging.INFO)


try:
# pylint: disable=unused-import
import boto
import boto3 # pylint: disable=unused-import
import boto.dynamodb2
import botocore

# pylint: enable=unused-import
from boto.dynamodb2.fields import (
Expand All @@ -79,12 +85,138 @@ def __virtual__():
"""
Only load if boto libraries exist.
"""
has_boto_reqs = salt.utils.versions.check_boto_reqs(check_boto3=False)
has_boto_reqs = salt.utils.versions.check_boto_reqs()
if has_boto_reqs is True:
__utils__["boto.assign_funcs"](__name__, "dynamodb2", pack=__salt__)
return has_boto_reqs


def list_tags_of_resource(
resource_arn, region=None, key=None, keyid=None, profile=None
):
"""
Returns a dictionary of all tags currently attached to a given resource.

CLI Example:

.. code-block:: bash

salt myminion boto_dynamodb.list_tags_of_resource \
resource_arn=arn:aws:dynamodb:us-east-1:012345678901:table/my-table
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3006
"""
conn3 = __utils__["boto3.get_connection"](
"dynamodb", region=region, key=key, keyid=keyid, profile=profile
)
retries = 10
sleep = 6
tags = []
while retries:
try:
log.debug("Garnering tags of resource %s", resource_arn)
marker = ""
while marker is not None:
ret = conn3.list_tags_of_resource(
ResourceArn=resource_arn, NextToken=marker
)
tags += ret.get("Tags", [])
marker = ret.get("NextToken")
return {tag["Key"]: tag["Value"] for tag in tags}
except botocore.exceptions.ParamValidationError as err:
raise SaltInvocationError(str(err))
except botocore.exceptions.ClientError as err:
if retries and err.response.get("Error", {}).get("Code") == "Throttling":
retries -= 1
log.debug("Throttled by AWS API, retrying in %s seconds...", sleep)
time.sleep(sleep)
continue
log.error(
"Failed to list tags for resource %s: %s", resource_arn, err.message
)
return False


def tag_resource(resource_arn, tags, region=None, key=None, keyid=None, profile=None):
"""
Sets given tags (provided as list or dict) on the given resource.

CLI Example:

.. code-block:: bash

salt myminion boto_dynamodb.tag_resource \
resource_arn=arn:aws:dynamodb:us-east-1:012345678901:table/my-table \
tags='{Name: my-table, Owner: Ops}'
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3006
"""
conn3 = __utils__["boto3.get_connection"](
"dynamodb", region=region, key=key, keyid=keyid, profile=profile
)
retries = 10
sleep = 6
if isinstance(tags, dict):
tags = [{"Key": key, "Value": val} for key, val in tags.items()]
while retries:
try:
log.debug("Setting tags on resource %s", resource_arn)
conn3.tag_resource(ResourceArn=resource_arn, Tags=tags)
return True
except botocore.exceptions.ParamValidationError as err:
raise SaltInvocationError(str(err))
except botocore.exceptions.ClientError as err:
if retries and err.response.get("Error", {}).get("Code") == "Throttling":
retries -= 1
log.debug("Throttled by AWS API, retrying in %s seconds...", sleep)
time.sleep(sleep)
continue
log.error(
"Failed to set tags on resource %s: %s", resource_arn, err.message
)
return False


def untag_resource(
resource_arn, tag_keys, region=None, key=None, keyid=None, profile=None
):
"""
Removes given tags (provided as list) from the given resource.

CLI Example:

.. code-block:: bash

salt myminion boto_dynamodb.untag_resource \
resource_arn=arn:aws:dynamodb:us-east-1:012345678901:table/my-table \
tag_keys='[Name, Owner]'
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3006
"""
conn3 = __utils__["boto3.get_connection"](
"dynamodb", region=region, key=key, keyid=keyid, profile=profile
)
retries = 10
sleep = 6
while retries:
try:
log.debug("Removing tags from resource %s", resource_arn)
ret = conn3.untag_resource(ResourceArn=resource_arn, TagKeys=tag_keys)
return True
except botocore.exceptions.ParamValidationError as err:
raise SaltInvocationError(str(err))
except botocore.exceptions.ClientError as err:
if retries and err.response.get("Error", {}).get("Code") == "Throttling":
retries -= 1
log.debug("Throttled by AWS API, retrying in %s seconds...", sleep)
time.sleep(sleep)
continue
log.error(
"Failed to remove tags from resource %s: %s", resource_arn, err.message
)
return False


def create_table(
table_name,
region=None,
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/modules/test_boto_dynamodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import salt.modules.boto_dynamodb as boto_dynamodb
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.mock import MagicMock, patch
from tests.support.unit import TestCase

ARN = "arn:aws:dynamodb:us-east-1:012345678901:table/my-table"
TAGS = {"foo": "bar", "hello": "world"}
TAGS_AS_LIST = [{"Key": "foo", "Value": "bar"}, {"Key": "hello", "Value": "world"}]


class DummyConn:
def __init__(self):
self.list_tags_of_resource = MagicMock(
return_value={"Tags": TAGS_AS_LIST, "NextToken": None}
)
self.tag_resource = MagicMock(return_value=True)
self.untag_resource = MagicMock(return_value=True)


class BotoDynamoDBTestCase(TestCase, LoaderModuleMockMixin):
"""
TestCase for salt.modules.boto_elb module
"""

def setup_loader_modules(self):
return {boto_dynamodb: {"__opts__": {}, "__utils__": {}}}

def test_list_tags_of_resource(self):
"""
Test that the correct API call is made and correct return format is
returned.
"""
conn = DummyConn()
utils = {"boto3.get_connection": MagicMock(return_value=conn)}
with patch.dict(boto_dynamodb.__utils__, utils):
ret = boto_dynamodb.list_tags_of_resource(resource_arn=ARN)

assert ret == TAGS, ret
conn.list_tags_of_resource.assert_called_once_with(
ResourceArn=ARN, NextToken=""
)

def test_tag_resource(self):
"""
Test that the correct API call is made and correct return format is
returned.
"""
conn = DummyConn()
utils = {"boto3.get_connection": MagicMock(return_value=conn)}
with patch.dict(boto_dynamodb.__utils__, utils):
ret = boto_dynamodb.tag_resource(resource_arn=ARN, tags=TAGS)

assert ret is True, ret
# Account for differing dict iteration order among Python versions by
# being more explicit in asserts.
assert len(conn.tag_resource.mock_calls) == 1
call = conn.tag_resource.mock_calls[0]
# No positional args
assert not call.args
# Make sure there aren't any additional kwargs beyond what we expect
assert len(call.kwargs) == 2
assert call.kwargs["ResourceArn"] == ARN
# Make sure there aren't any additional tags beyond what we expect
assert len(call.kwargs["Tags"]) == 2
for tag_dict in TAGS_AS_LIST:
assert tag_dict in call.kwargs["Tags"]

def test_untag_resource(self):
"""
Test that the correct API call is made and correct return format is
returned.
"""
conn = DummyConn()
utils = {"boto3.get_connection": MagicMock(return_value=conn)}
with patch.dict(boto_dynamodb.__utils__, utils):
ret = boto_dynamodb.untag_resource(resource_arn=ARN, tag_keys=sorted(TAGS))

assert ret is True, ret
conn.untag_resource.assert_called_once_with(
ResourceArn=ARN, TagKeys=sorted(TAGS)
)