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