1"""
2Connection module for Amazon Elasticsearch Service
3
4.. versionadded:: 2016.11.0
5
6:configuration: This module accepts explicit AWS credentials but can also
7    utilize IAM roles assigned to the instance trough Instance Profiles.
8    Dynamic credentials are then automatically obtained from AWS API and no
9    further configuration is necessary. More Information available at:
10
11    .. code-block:: text
12
13        http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
14
15    If IAM roles are not used you need to specify them either in a pillar or
16    in the minion's config file:
17
18    .. code-block:: yaml
19
20        lambda.keyid: GKTADJGHEIQSXMKKRBJ08H
21        lambda.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
22
23    A region may also be specified in the configuration:
24
25    .. code-block:: yaml
26
27        lambda.region: us-east-1
28
29    If a region is not specified, the default is us-east-1.
30
31    It's also possible to specify key, keyid and region via a profile, either
32    as a passed in dict, or as a string to pull from pillars or minion config:
33
34    .. code-block:: yaml
35
36        myprofile:
37            keyid: GKTADJGHEIQSXMKKRBJ08H
38            key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
39            region: us-east-1
40
41    Create and delete methods return:
42
43    .. code-block:: yaml
44
45        created: true
46
47    or
48
49    .. code-block:: yaml
50
51        created: false
52        error:
53          message: error message
54
55    Request methods (e.g., `describe_function`) return:
56
57    .. code-block:: yaml
58
59        domain:
60          - {...}
61          - {...}
62
63    or
64
65    .. code-block:: yaml
66
67        error:
68          message: error message
69
70:depends: boto3
71
72"""
73# keep lint from choking on _get_conn and _cache_id
74# pylint: disable=E0602
75
76
77import logging
78
79import salt.utils.compat
80import salt.utils.json
81import salt.utils.versions
82from salt.exceptions import SaltInvocationError
83
84log = logging.getLogger(__name__)
85
86
87# pylint: disable=import-error
88try:
89    # pylint: disable=unused-import
90    import boto
91    import boto3
92
93    # pylint: enable=unused-import
94    from botocore.exceptions import ClientError
95
96    logging.getLogger("boto").setLevel(logging.CRITICAL)
97    logging.getLogger("boto3").setLevel(logging.CRITICAL)
98    HAS_BOTO = True
99except ImportError:
100    HAS_BOTO = False
101# pylint: enable=import-error
102
103
104def __virtual__():
105    """
106    Only load if boto libraries exist and if boto libraries are greater than
107    a given version.
108    """
109    # the boto_lambda execution module relies on the connect_to_region() method
110    # which was added in boto 2.8.0
111    # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
112    return salt.utils.versions.check_boto_reqs(boto_ver="2.8.0", boto3_ver="1.4.0")
113
114
115def __init__(opts):
116    if HAS_BOTO:
117        __utils__["boto3.assign_funcs"](__name__, "es")
118
119
120def exists(DomainName, region=None, key=None, keyid=None, profile=None):
121    """
122    Given a domain name, check to see if the given domain exists.
123
124    Returns True if the given domain exists and returns False if the given
125    function does not exist.
126
127    CLI Example:
128
129    .. code-block:: bash
130
131        salt myminion boto_elasticsearch_domain.exists mydomain
132
133    """
134
135    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
136    try:
137        domain = conn.describe_elasticsearch_domain(DomainName=DomainName)
138        return {"exists": True}
139    except ClientError as e:
140        if e.response.get("Error", {}).get("Code") == "ResourceNotFoundException":
141            return {"exists": False}
142        return {"error": __utils__["boto3.get_error"](e)}
143
144
145def status(DomainName, region=None, key=None, keyid=None, profile=None):
146    """
147    Given a domain name describe its status.
148
149    Returns a dictionary of interesting properties.
150
151    CLI Example:
152
153    .. code-block:: bash
154
155        salt myminion boto_elasticsearch_domain.status mydomain
156
157    """
158
159    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
160    try:
161        domain = conn.describe_elasticsearch_domain(DomainName=DomainName)
162        if domain and "DomainStatus" in domain:
163            domain = domain.get("DomainStatus", {})
164            keys = (
165                "Endpoint",
166                "Created",
167                "Deleted",
168                "DomainName",
169                "DomainId",
170                "EBSOptions",
171                "SnapshotOptions",
172                "AccessPolicies",
173                "Processing",
174                "AdvancedOptions",
175                "ARN",
176                "ElasticsearchVersion",
177            )
178            return {"domain": {k: domain.get(k) for k in keys if k in domain}}
179        else:
180            return {"domain": None}
181    except ClientError as e:
182        return {"error": __utils__["boto3.get_error"](e)}
183
184
185def describe(DomainName, region=None, key=None, keyid=None, profile=None):
186    """
187    Given a domain name describe its properties.
188
189    Returns a dictionary of interesting properties.
190
191    CLI Example:
192
193    .. code-block:: bash
194
195        salt myminion boto_elasticsearch_domain.describe mydomain
196
197    """
198
199    conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
200    try:
201        domain = conn.describe_elasticsearch_domain_config(DomainName=DomainName)
202        if domain and "DomainConfig" in domain:
203            domain = domain["DomainConfig"]
204            keys = (
205                "ElasticsearchClusterConfig",
206                "EBSOptions",
207                "AccessPolicies",
208                "SnapshotOptions",
209                "AdvancedOptions",
210            )
211            return {
212                "domain": {
213                    k: domain.get(k, {}).get("Options") for k in keys if k in domain
214                }
215            }
216        else:
217            return {"domain": None}
218    except ClientError as e:
219        return {"error": __utils__["boto3.get_error"](e)}
220
221
222def create(
223    DomainName,
224    ElasticsearchClusterConfig=None,
225    EBSOptions=None,
226    AccessPolicies=None,
227    SnapshotOptions=None,
228    AdvancedOptions=None,
229    region=None,
230    key=None,
231    keyid=None,
232    profile=None,
233    ElasticsearchVersion=None,
234):
235    """
236    Given a valid config, create a domain.
237
238    Returns {created: true} if the domain was created and returns
239    {created: False} if the domain was not created.
240
241    CLI Example:
242
243    .. code-block:: bash
244
245        salt myminion boto_elasticsearch_domain.create mydomain \\
246              {'InstanceType': 't2.micro.elasticsearch', 'InstanceCount': 1, \\
247              'DedicatedMasterEnabled': false, 'ZoneAwarenessEnabled': false} \\
248              {'EBSEnabled': true, 'VolumeType': 'gp2', 'VolumeSize': 10, \\
249              'Iops': 0} \\
250              {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Principal": {"AWS": "*"}, "Action": "es:*", \\
251               "Resource": "arn:aws:es:us-east-1:111111111111:domain/mydomain/*", \\
252               "Condition": {"IpAddress": {"aws:SourceIp": ["127.0.0.1"]}}}]} \\
253              {"AutomatedSnapshotStartHour": 0} \\
254              {"rest.action.multi.allow_explicit_index": "true"}
255    """
256
257    try:
258        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
259        kwargs = {}
260        for k in (
261            "ElasticsearchClusterConfig",
262            "EBSOptions",
263            "AccessPolicies",
264            "SnapshotOptions",
265            "AdvancedOptions",
266            "ElasticsearchVersion",
267        ):
268            if locals()[k] is not None:
269                val = locals()[k]
270                if isinstance(val, str):
271                    try:
272                        val = salt.utils.json.loads(val)
273                    except ValueError as e:
274                        return {
275                            "updated": False,
276                            "error": "Error parsing {}: {}".format(k, e.message),
277                        }
278                kwargs[k] = val
279        if "AccessPolicies" in kwargs:
280            kwargs["AccessPolicies"] = salt.utils.json.dumps(kwargs["AccessPolicies"])
281        if "ElasticsearchVersion" in kwargs:
282            kwargs["ElasticsearchVersion"] = str(kwargs["ElasticsearchVersion"])
283        domain = conn.create_elasticsearch_domain(DomainName=DomainName, **kwargs)
284        if domain and "DomainStatus" in domain:
285            return {"created": True}
286        else:
287            log.warning("Domain was not created")
288            return {"created": False}
289    except ClientError as e:
290        return {"created": False, "error": __utils__["boto3.get_error"](e)}
291
292
293def delete(DomainName, region=None, key=None, keyid=None, profile=None):
294    """
295    Given a domain name, delete it.
296
297    Returns {deleted: true} if the domain was deleted and returns
298    {deleted: false} if the domain was not deleted.
299
300    CLI Example:
301
302    .. code-block:: bash
303
304        salt myminion boto_elasticsearch_domain.delete mydomain
305
306    """
307
308    try:
309        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
310        conn.delete_elasticsearch_domain(DomainName=DomainName)
311        return {"deleted": True}
312    except ClientError as e:
313        return {"deleted": False, "error": __utils__["boto3.get_error"](e)}
314
315
316def update(
317    DomainName,
318    ElasticsearchClusterConfig=None,
319    EBSOptions=None,
320    AccessPolicies=None,
321    SnapshotOptions=None,
322    AdvancedOptions=None,
323    region=None,
324    key=None,
325    keyid=None,
326    profile=None,
327):
328    """
329    Update the named domain to the configuration.
330
331    Returns {updated: true} if the domain was updated and returns
332    {updated: False} if the domain was not updated.
333
334    CLI Example:
335
336    .. code-block:: bash
337
338        salt myminion boto_elasticsearch_domain.update mydomain \\
339              {'InstanceType': 't2.micro.elasticsearch', 'InstanceCount': 1, \\
340              'DedicatedMasterEnabled': false, 'ZoneAwarenessEnabled': false} \\
341              {'EBSEnabled': true, 'VolumeType': 'gp2', 'VolumeSize': 10, \\
342              'Iops': 0} \\
343              {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Principal": {"AWS": "*"}, "Action": "es:*", \\
344               "Resource": "arn:aws:es:us-east-1:111111111111:domain/mydomain/*", \\
345               "Condition": {"IpAddress": {"aws:SourceIp": ["127.0.0.1"]}}}]} \\
346              {"AutomatedSnapshotStartHour": 0} \\
347              {"rest.action.multi.allow_explicit_index": "true"}
348
349    """
350
351    call_args = {}
352    for k in (
353        "ElasticsearchClusterConfig",
354        "EBSOptions",
355        "AccessPolicies",
356        "SnapshotOptions",
357        "AdvancedOptions",
358    ):
359        if locals()[k] is not None:
360            val = locals()[k]
361            if isinstance(val, str):
362                try:
363                    val = salt.utils.json.loads(val)
364                except ValueError as e:
365                    return {
366                        "updated": False,
367                        "error": "Error parsing {}: {}".format(k, e.message),
368                    }
369            call_args[k] = val
370    if "AccessPolicies" in call_args:
371        call_args["AccessPolicies"] = salt.utils.json.dumps(call_args["AccessPolicies"])
372    try:
373        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
374        domain = conn.update_elasticsearch_domain_config(
375            DomainName=DomainName, **call_args
376        )
377        if not domain or "DomainConfig" not in domain:
378            log.warning("Domain was not updated")
379            return {"updated": False}
380        return {"updated": True}
381    except ClientError as e:
382        return {"updated": False, "error": __utils__["boto3.get_error"](e)}
383
384
385def add_tags(
386    DomainName=None, ARN=None, region=None, key=None, keyid=None, profile=None, **kwargs
387):
388    """
389    Add tags to a domain
390
391    Returns {tagged: true} if the domain was tagged and returns
392    {tagged: False} if the domain was not tagged.
393
394    CLI Example:
395
396    .. code-block:: bash
397
398        salt myminion boto_elasticsearch_domain.add_tags mydomain tag_a=tag_value tag_b=tag_value
399
400    """
401
402    try:
403        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
404        tagslist = []
405        for k, v in kwargs.items():
406            if str(k).startswith("__"):
407                continue
408            tagslist.append({"Key": str(k), "Value": str(v)})
409        if ARN is None:
410            if DomainName is None:
411                raise SaltInvocationError(
412                    "One (but not both) of ARN or domain must be specified."
413                )
414            domaindata = status(
415                DomainName=DomainName,
416                region=region,
417                key=key,
418                keyid=keyid,
419                profile=profile,
420            )
421            if not domaindata or "domain" not in domaindata:
422                log.warning("Domain tags not updated")
423                return {"tagged": False}
424            ARN = domaindata.get("domain", {}).get("ARN")
425        elif DomainName is not None:
426            raise SaltInvocationError(
427                "One (but not both) of ARN or domain must be specified."
428            )
429        conn.add_tags(ARN=ARN, TagList=tagslist)
430        return {"tagged": True}
431    except ClientError as e:
432        return {"tagged": False, "error": __utils__["boto3.get_error"](e)}
433
434
435def remove_tags(
436    TagKeys, DomainName=None, ARN=None, region=None, key=None, keyid=None, profile=None
437):
438    """
439    Remove tags from a trail
440
441    Returns {tagged: true} if the trail was tagged and returns
442    {tagged: False} if the trail was not tagged.
443
444    CLI Example:
445
446    .. code-block:: bash
447
448        salt myminion boto_cloudtrail.remove_tags my_trail tag_a=tag_value tag_b=tag_value
449
450    """
451
452    try:
453        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
454        if ARN is None:
455            if DomainName is None:
456                raise SaltInvocationError(
457                    "One (but not both) of ARN or domain must be specified."
458                )
459            domaindata = status(
460                DomainName=DomainName,
461                region=region,
462                key=key,
463                keyid=keyid,
464                profile=profile,
465            )
466            if not domaindata or "domain" not in domaindata:
467                log.warning("Domain tags not updated")
468                return {"tagged": False}
469            ARN = domaindata.get("domain", {}).get("ARN")
470        elif DomainName is not None:
471            raise SaltInvocationError(
472                "One (but not both) of ARN or domain must be specified."
473            )
474        conn.remove_tags(ARN=domaindata.get("domain", {}).get("ARN"), TagKeys=TagKeys)
475        return {"tagged": True}
476    except ClientError as e:
477        return {"tagged": False, "error": __utils__["boto3.get_error"](e)}
478
479
480def list_tags(
481    DomainName=None, ARN=None, region=None, key=None, keyid=None, profile=None
482):
483    """
484    List tags of a trail
485
486    Returns:
487        tags:
488          - {...}
489          - {...}
490
491    CLI Example:
492
493    .. code-block:: bash
494
495        salt myminion boto_cloudtrail.list_tags my_trail
496
497    """
498
499    try:
500        conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
501        if ARN is None:
502            if DomainName is None:
503                raise SaltInvocationError(
504                    "One (but not both) of ARN or domain must be specified."
505                )
506            domaindata = status(
507                DomainName=DomainName,
508                region=region,
509                key=key,
510                keyid=keyid,
511                profile=profile,
512            )
513            if not domaindata or "domain" not in domaindata:
514                log.warning("Domain tags not updated")
515                return {"tagged": False}
516            ARN = domaindata.get("domain", {}).get("ARN")
517        elif DomainName is not None:
518            raise SaltInvocationError(
519                "One (but not both) of ARN or domain must be specified."
520            )
521        ret = conn.list_tags(ARN=ARN)
522        log.warning(ret)
523        tlist = ret.get("TagList", [])
524        tagdict = {}
525        for tag in tlist:
526            tagdict[tag.get("Key")] = tag.get("Value")
527        return {"tags": tagdict}
528    except ClientError as e:
529        return {"error": __utils__["boto3.get_error"](e)}
530