Mini Shell
"""
Boto3 Common Utils
=================
Note: This module depends on the dicts packed by the loader and,
therefore, must be accessed via the loader or from the __utils__ dict.
This module provides common functionality for the boto execution modules.
The expected usage is to call `apply_funcs` from the `__virtual__` function
of the module. This will bring properly initilized partials of `_get_conn`
and `_cache_id` into the module's namespace.
Example Usage:
.. code-block:: python
def __virtual__():
__utils__['boto.apply_funcs'](__name__, 'vpc')
def test():
conn = _get_conn()
vpc_id = _cache_id('test-vpc')
.. versionadded:: 2015.8.0
"""
import hashlib
import logging
import sys
from functools import partial
import salt.loader.context
import salt.utils.stringutils
import salt.utils.versions
from salt.exceptions import SaltInvocationError
# pylint: disable=import-error
try:
# pylint: disable=import-error
import boto3
import boto3.session
import botocore # pylint: disable=W0611
import botocore.exceptions
# pylint: enable=import-error
logging.getLogger("boto3").setLevel(logging.CRITICAL)
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
# pylint: enable=import-error
log = logging.getLogger(__name__)
__virtualname__ = "boto3"
__salt_loader__ = salt.loader.context.LoaderContext()
__context__ = __salt_loader__.named_context("__context__", {})
def __virtual__():
"""
Only load if boto libraries exist and if boto libraries are greater than
a given version.
"""
has_boto = salt.utils.versions.check_boto_reqs()
if has_boto is True:
return __virtualname__
return has_boto
def _option(value):
"""
Look up the value for an option.
"""
if value in __opts__:
return __opts__[value]
master_opts = __pillar__.get("master", {})
if value in master_opts:
return master_opts[value]
if value in __pillar__:
return __pillar__[value]
def _get_profile(service, region, key, keyid, profile):
if profile:
if isinstance(profile, str):
_profile = _option(profile)
elif isinstance(profile, dict):
_profile = profile
key = _profile.get("key", None)
keyid = _profile.get("keyid", None)
region = _profile.get("region", None)
if not region and _option(service + ".region"):
region = _option(service + ".region")
if not region:
region = "us-east-1"
log.info("Assuming default region %s", region)
if not key and _option(service + ".key"):
key = _option(service + ".key")
if not keyid and _option(service + ".keyid"):
keyid = _option(service + ".keyid")
label = f"boto_{service}:"
if keyid:
hash_string = region + keyid + key
hash_string = salt.utils.stringutils.to_bytes(hash_string)
cxkey = label + hashlib.md5(hash_string).hexdigest()
else:
cxkey = label + region
return (cxkey, region, key, keyid)
def cache_id(
service,
name,
sub_resource=None,
resource_id=None,
invalidate=False,
region=None,
key=None,
keyid=None,
profile=None,
):
"""
Cache, invalidate, or retrieve an AWS resource id keyed by name.
.. code-block:: python
__utils__['boto.cache_id']('ec2', 'myinstance',
'i-a1b2c3',
profile='custom_profile')
"""
cxkey, _, _, _ = _get_profile(service, region, key, keyid, profile)
if sub_resource:
cxkey = f"{cxkey}:{sub_resource}:{name}:id"
else:
cxkey = f"{cxkey}:{name}:id"
if invalidate:
if cxkey in __context__:
del __context__[cxkey]
return True
elif resource_id in __context__.values():
ctx = {k: v for k, v in __context__.items() if v != resource_id}
__context__.clear()
__context__.update(ctx)
return True
else:
return False
if resource_id:
__context__[cxkey] = resource_id
return True
return __context__.get(cxkey)
def cache_id_func(service):
"""
Returns a partial `cache_id` function for the provided service.
.. code-block:: python
cache_id = __utils__['boto.cache_id_func']('ec2')
cache_id('myinstance', 'i-a1b2c3')
instance_id = cache_id('myinstance')
"""
return partial(cache_id, service)
def get_connection(
service, module=None, region=None, key=None, keyid=None, profile=None
):
"""
Return a boto connection for the service.
.. code-block:: python
conn = __utils__['boto.get_connection']('ec2', profile='custom_profile')
"""
module = module or service
cxkey, region, key, keyid = _get_profile(service, region, key, keyid, profile)
cxkey = cxkey + ":conn3"
if cxkey in __context__:
return __context__[cxkey]
try:
session = boto3.session.Session(
aws_access_key_id=keyid, aws_secret_access_key=key, region_name=region
)
if session is None:
raise SaltInvocationError(f'Region "{region}" is not valid.')
conn = session.client(module)
if conn is None:
raise SaltInvocationError(f'Region "{region}" is not valid.')
except botocore.exceptions.NoCredentialsError:
raise SaltInvocationError(
"No authentication credentials found when "
"attempting to make boto {} connection to "
'region "{}".'.format(service, region)
)
__context__[cxkey] = conn
return conn
def get_connection_func(service, module=None):
"""
Returns a partial `get_connection` function for the provided service.
.. code-block:: python
get_conn = __utils__['boto.get_connection_func']('ec2')
conn = get_conn()
"""
return partial(get_connection, service, module=module)
def get_region(service, region, profile):
"""
Retrieve the region for a particular AWS service based on configured region and/or profile.
"""
_, region, _, _ = _get_profile(service, region, None, None, profile)
return region
def get_error(e):
# The returns from boto modules vary greatly between modules. We need to
# assume that none of the data we're looking for exists.
aws = {}
message = ""
message = e.args[0]
r = {"message": message}
if aws:
r["aws"] = aws
return r
def exactly_n(l, n=1):
"""
Tests that exactly N items in an iterable are "truthy" (neither None,
False, nor 0).
"""
i = iter(l)
return all(any(i) for j in range(n)) and not any(i)
def exactly_one(l):
return exactly_n(l)
def assign_funcs(
modname,
service,
module=None,
get_conn_funcname="_get_conn",
cache_id_funcname="_cache_id",
exactly_one_funcname="_exactly_one",
):
"""
Assign _get_conn and _cache_id functions to the named module.
.. code-block:: python
_utils__['boto.assign_partials'](__name__, 'ec2')
"""
mod = sys.modules[modname]
setattr(mod, get_conn_funcname, get_connection_func(service, module=module))
setattr(mod, cache_id_funcname, cache_id_func(service))
# TODO: Remove this and import salt.utils.data.exactly_one into boto_* modules instead
# Leaving this way for now so boto modules can be back ported
if exactly_one_funcname is not None:
setattr(mod, exactly_one_funcname, exactly_one)
def paged_call(function, *args, **kwargs):
"""Retrieve full set of values from a boto3 API call that may truncate
its results, yielding each page as it is obtained.
"""
marker_flag = kwargs.pop("marker_flag", "NextMarker")
marker_arg = kwargs.pop("marker_arg", "Marker")
while True:
ret = function(*args, **kwargs)
marker = ret.get(marker_flag)
yield ret
if not marker:
break
kwargs[marker_arg] = marker
def ordered(obj):
if isinstance(obj, (list, tuple)):
return sorted(ordered(x) for x in obj)
elif isinstance(obj, dict):
return {str(k) if isinstance(k, str) else k: ordered(v) for k, v in obj.items()}
elif isinstance(obj, str):
return str(obj)
return obj
def json_objs_equal(left, right):
"""Compare two parsed JSON objects, given non-ordering in JSON objects"""
return ordered(left) == ordered(right)
Zerion Mini Shell 1.0