Mini Shell
"""
Retrieve EC2 instance data for minions for ec2_tags and ec2_tags_list
The minion id must be the AWS instance-id or value in ``tag_match_key``. For
example set ``tag_match_key`` to ``Name`` to have the minion-id matched against
the tag 'Name'. The tag contents must be unique. The value of
``tag_match_value`` can be 'uqdn' or 'asis'. if 'uqdn', then the domain will be
stripped before comparison.
Additionally, the ``use_grain`` option can be set to ``True``. This allows the
use of an instance-id grain instead of the minion-id. Since this is a potential
security risk, the configuration can be further expanded to include a list of
minions that are trusted to only allow the alternate id of the instances to
specific hosts. There is no glob matching at this time.
.. note::
If you are using ``use_grain: True`` in the configuration for this external
pillar module, the minion must have :conf_minion:`metadata_server_grains`
enabled in the minion config file (see also :py:mod:`here
<salt.grains.metadata>`).
It is important to also note that enabling the ``use_grain`` option allows
the minion to manipulate the pillar data returned, as described above.
The optional ``tag_list_key`` indicates which keys should be added to
``ec2_tags_list`` and be split by ``tag_list_sep`` (by default ``;``). If a tag
key is included in ``tag_list_key`` it is removed from ec2_tags. If a tag does
not exist it is still included as an empty list.
.. note::
As with any master configuration change, restart the salt-master daemon for
changes to take effect.
.. code-block:: yaml
ext_pillar:
- ec2_pillar:
tag_match_key: 'Name'
tag_match_value: 'asis'
tag_list_key:
- Role
tag_list_sep: ';'
use_grain: True
minion_ids:
- trusted-minion-1
- trusted-minion-2
- trusted-minion-3
This is a very simple pillar configuration that simply retrieves the instance
data from AWS. Currently the only portion implemented are EC2 tags, which
returns a list of key/value pairs for all of the EC2 tags assigned to the
instance.
"""
import logging
import re
from salt.utils.versions import Version
try:
import boto.ec2
import boto.exception
import boto.utils
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
# Set up logging
log = logging.getLogger(__name__)
# DEBUG boto is far too verbose
logging.getLogger("boto").setLevel(logging.WARNING)
def __virtual__():
"""
Check for required version of boto and make this pillar available
depending on outcome.
"""
if not HAS_BOTO:
return False
boto_version = Version(boto.__version__)
required_boto_version = Version("2.8.0")
if boto_version < required_boto_version:
log.error(
"%s: installed boto version %s < %s, can't retrieve instance data",
__name__,
boto_version,
required_boto_version,
)
return False
return True
def _get_instance_info():
"""
Helper function to return the instance ID and region of the master where
this pillar is run.
"""
identity = boto.utils.get_instance_identity()["document"]
return (identity["instanceId"], identity["region"])
def ext_pillar(
minion_id,
pillar, # pylint: disable=W0613
use_grain=False,
minion_ids=None,
tag_match_key=None,
tag_match_value="asis",
tag_list_key=None,
tag_list_sep=";",
):
"""
Execute a command and read the output as YAML
"""
valid_tag_match_value = ["uqdn", "asis"]
# meta-data:instance-id
grain_instance_id = __grains__.get("meta-data", {}).get("instance-id", None)
if not grain_instance_id:
# dynamic:instance-identity:document:instanceId
grain_instance_id = (
__grains__.get("dynamic", {})
.get("instance-identity", {})
.get("document", {})
.get("instance-id", None)
)
if (
grain_instance_id
and re.search(r"^i-([0-9a-z]{17}|[0-9a-z]{8})$", grain_instance_id) is None
):
log.error(
"External pillar %s, instance-id '%s' is not valid for '%s'",
__name__,
grain_instance_id,
minion_id,
)
grain_instance_id = None # invalid instance id found, remove it from use.
# Check AWS Tag restrictions .i.e. letters, spaces, and numbers and + - = . _ : / @
if tag_match_key and re.match(r"[\w=.:/@-]+$", tag_match_key) is None:
log.error(
"External pillar %s, tag_match_key '%s' is not valid ",
__name__,
tag_match_key if isinstance(tag_match_key, str) else "non-string",
)
return {}
if tag_match_key and tag_match_value not in valid_tag_match_value:
log.error(
"External pillar %s, tag_value '%s' is not valid must be one of %s",
__name__,
tag_match_value,
" ".join(valid_tag_match_value),
)
return {}
if not tag_match_key:
base_msg = (
"External pillar %s, querying EC2 tags for minion id '%s' "
"against instance-id",
__name__,
minion_id,
)
else:
base_msg = (
"External pillar %s, querying EC2 tags for minion id '%s' "
"against instance-id or '%s' against '%s'",
__name__,
minion_id,
tag_match_key,
tag_match_value,
)
log.debug(base_msg)
find_filter = None
find_id = None
if re.search(r"^i-([0-9a-z]{17}|[0-9a-z]{8})$", minion_id) is not None:
find_filter = None
find_id = minion_id
elif tag_match_key:
if tag_match_value == "uqdn":
find_filter = {f"tag:{tag_match_key}": minion_id.split(".", 1)[0]}
else:
find_filter = {f"tag:{tag_match_key}": minion_id}
if grain_instance_id:
# we have an untrusted grain_instance_id, use it to narrow the search
# even more. Combination will be unique even if uqdn is set.
find_filter.update({"instance-id": grain_instance_id})
# Add this if running state is not dependent on EC2Config
# find_filter.update('instance-state-name': 'running')
# no minion-id is instance-id and no suitable filter, try use_grain if enabled
if not find_filter and not find_id and use_grain:
if not grain_instance_id:
log.debug(
"Minion-id is not in AWS instance-id formation, and there "
"is no instance-id grain for minion %s",
minion_id,
)
return {}
if minion_ids is not None and minion_id not in minion_ids:
log.debug(
"Minion-id is not in AWS instance ID format, and minion_ids "
"is set in the ec2_pillar configuration, but minion %s is "
"not in the list of allowed minions %s",
minion_id,
minion_ids,
)
return {}
find_id = grain_instance_id
if not (find_filter or find_id):
log.debug(
"External pillar %s, querying EC2 tags for minion id '%s' against "
"instance-id or '%s' against '%s' noughthing to match against",
__name__,
minion_id,
tag_match_key,
tag_match_value,
)
return {}
myself = boto.utils.get_instance_metadata(timeout=0.1, num_retries=1)
if len(myself.keys()) < 1:
log.info("%s: salt master not an EC2 instance, skipping", __name__)
return {}
# Get the Master's instance info, primarily the region
(_, region) = _get_instance_info()
# If the Minion's region is available, use it instead
if use_grain:
region = __grains__.get("ec2", {}).get("region", region)
try:
conn = boto.ec2.connect_to_region(region)
except boto.exception.AWSConnectionError as exc:
log.error("%s: invalid AWS credentials, %s", __name__, exc)
return {}
if conn is None:
log.error("%s: Could not connect to region %s", __name__, region)
return {}
try:
if find_id:
instance_data = conn.get_only_instances(
instance_ids=[find_id], dry_run=False
)
else:
# filters and max_results can not be used togther.
instance_data = conn.get_only_instances(filters=find_filter, dry_run=False)
except boto.exception.EC2ResponseError as exc:
log.error("%s failed with '%s'", base_msg, exc)
return {}
if not instance_data:
log.debug(
"%s no match using '%s'", base_msg, find_id if find_id else find_filter
)
return {}
# Find a active instance, i.e. ignore terminated and stopped instances
active_inst = []
for idx, inst_data in enumerate(instance_data):
if inst_data.state not in ["terminated", "stopped"]:
active_inst.append(idx)
valid_inst = len(active_inst)
if not valid_inst:
log.debug(
"%s match found but not active '%s'",
base_msg,
find_id if find_id else find_filter,
)
return {}
if valid_inst > 1:
log.error(
"%s multiple matches, ignored, using '%s'",
base_msg,
find_id if find_id else find_filter,
)
return {}
instance = instance_data[active_inst[0]]
if instance.tags:
ec2_tags = instance.tags
ec2_tags_list = {}
log.debug(
"External pillar %s, for minion id '%s', tags: %s",
__name__,
minion_id,
instance.tags,
)
if tag_list_key and isinstance(tag_list_key, list):
for item in tag_list_key:
if item in ec2_tags:
ec2_tags_list[item] = ec2_tags[item].split(tag_list_sep)
del ec2_tags[item] # make sure its only in ec2_tags_list
else:
ec2_tags_list[item] = [] # always return a result
return {"ec2_tags": ec2_tags, "ec2_tags_list": ec2_tags_list}
return {}
Zerion Mini Shell 1.0