1""" 2Manage EC2 3 4.. versionadded:: 2015.8.0 5 6This module provides an interface to the Elastic Compute Cloud (EC2) service 7from AWS. 8 9The below code creates a key pair: 10 11.. code-block:: yaml 12 13 create-key-pair: 14 boto_ec2.key_present: 15 - name: mykeypair 16 - save_private: /root/ 17 - region: eu-west-1 18 - keyid: GKTADJGHEIQSXMKKRBJ08H 19 - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs 20 21.. code-block:: yaml 22 23 import-key-pair: 24 boto_ec2.key_present: 25 - name: mykeypair 26 - upload_public: 'ssh-rsa AAAA' 27 - keyid: GKTADJGHEIQSXMKKRBJ08H 28 - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs 29 30You can also use salt:// in order to define the public key. 31 32.. code-block:: yaml 33 34 import-key-pair: 35 boto_ec2.key_present: 36 - name: mykeypair 37 - upload_public: salt://mybase/public_key.pub 38 - keyid: GKTADJGHEIQSXMKKRBJ08H 39 - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs 40 41The below code deletes a key pair: 42 43.. code-block:: yaml 44 45 delete-key-pair: 46 boto_ec2.key_absent: 47 - name: mykeypair 48 - region: eu-west-1 49 - keyid: GKTADJGHEIQSXMKKRBJ08H 50 - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs 51""" 52 53 54import logging 55from time import sleep, time 56 57import salt.utils.data 58import salt.utils.dictupdate as dictupdate 59from salt.exceptions import CommandExecutionError, SaltInvocationError 60 61log = logging.getLogger(__name__) 62 63 64def __virtual__(): 65 """ 66 Only load if boto is available. 67 """ 68 if "boto_ec2.get_key" in __salt__: 69 return "boto_ec2" 70 return (False, "boto_ec2 module could not be loaded") 71 72 73def key_present( 74 name, 75 save_private=None, 76 upload_public=None, 77 region=None, 78 key=None, 79 keyid=None, 80 profile=None, 81): 82 """ 83 Ensure key pair is present. 84 """ 85 ret = {"name": name, "result": True, "comment": "", "changes": {}} 86 exists = __salt__["boto_ec2.get_key"](name, region, key, keyid, profile) 87 log.debug("exists is %s", exists) 88 if upload_public is not None and "salt://" in upload_public: 89 try: 90 upload_public = __salt__["cp.get_file_str"](upload_public) 91 except OSError as e: 92 log.debug(e) 93 ret["comment"] = "File {} not found.".format(upload_public) 94 ret["result"] = False 95 return ret 96 if not exists: 97 if __opts__["test"]: 98 ret["comment"] = "The key {} is set to be created.".format(name) 99 ret["result"] = None 100 return ret 101 if save_private and not upload_public: 102 created = __salt__["boto_ec2.create_key"]( 103 name, save_private, region, key, keyid, profile 104 ) 105 if created: 106 ret["result"] = True 107 ret["comment"] = "The key {} is created.".format(name) 108 ret["changes"]["new"] = created 109 else: 110 ret["result"] = False 111 ret["comment"] = "Could not create key {} ".format(name) 112 elif not save_private and upload_public: 113 imported = __salt__["boto_ec2.import_key"]( 114 name, upload_public, region, key, keyid, profile 115 ) 116 if imported: 117 ret["result"] = True 118 ret["comment"] = "The key {} is created.".format(name) 119 ret["changes"]["old"] = None 120 ret["changes"]["new"] = imported 121 else: 122 ret["result"] = False 123 ret["comment"] = "Could not create key {} ".format(name) 124 else: 125 ret["result"] = False 126 ret["comment"] = "You can either upload or download a private key " 127 else: 128 ret["result"] = True 129 ret["comment"] = "The key name {} already exists".format(name) 130 return ret 131 132 133def key_absent(name, region=None, key=None, keyid=None, profile=None): 134 """ 135 Deletes a key pair 136 """ 137 ret = {"name": name, "result": True, "comment": "", "changes": {}} 138 exists = __salt__["boto_ec2.get_key"](name, region, key, keyid, profile) 139 if exists: 140 if __opts__["test"]: 141 ret["comment"] = "The key {} is set to be deleted.".format(name) 142 ret["result"] = None 143 return ret 144 deleted = __salt__["boto_ec2.delete_key"](name, region, key, keyid, profile) 145 log.debug("exists is %s", deleted) 146 if deleted: 147 ret["result"] = True 148 ret["comment"] = "The key {} is deleted.".format(name) 149 ret["changes"]["old"] = name 150 else: 151 ret["result"] = False 152 ret["comment"] = "Could not delete key {} ".format(name) 153 else: 154 ret["result"] = True 155 ret["comment"] = "The key name {} does not exist".format(name) 156 return ret 157 158 159def eni_present( 160 name, 161 subnet_id=None, 162 subnet_name=None, 163 private_ip_address=None, 164 description=None, 165 groups=None, 166 source_dest_check=True, 167 allocate_eip=None, 168 arecords=None, 169 region=None, 170 key=None, 171 keyid=None, 172 profile=None, 173): 174 """ 175 Ensure the EC2 ENI exists. 176 177 .. versionadded:: 2016.3.0 178 179 name 180 Name tag associated with the ENI. 181 182 subnet_id 183 The VPC subnet ID the ENI will exist within. 184 185 subnet_name 186 The VPC subnet name the ENI will exist within. 187 188 private_ip_address 189 The private ip address to use for this ENI. If this is not specified 190 AWS will automatically assign a private IP address to the ENI. Must be 191 specified at creation time; will be ignored afterward. 192 193 description 194 Description of the key. 195 196 groups 197 A list of security groups to apply to the ENI. 198 199 source_dest_check 200 Boolean specifying whether source/destination checking is enabled on 201 the ENI. 202 203 allocate_eip 204 allocate and associate an EIP to the ENI. Could be 'standard' to 205 allocate Elastic IP to EC2 region or 'vpc' to get it for a 206 particular VPC 207 208 .. versionchanged:: 2016.11.0 209 210 arecords 211 A list of arecord dicts with attributes needed for the DNS add_record state. 212 By default the boto_route53.add_record state will be used, which requires: name, zone, ttl, and identifier. 213 See the boto_route53 state for information about these attributes. 214 Other DNS modules can be called by specifying the provider keyword. 215 By default, the private ENI IP address will be used, set 'public: True' in the arecord dict to use the ENI's public IP address 216 217 .. versionadded:: 2016.3.0 218 219 region 220 Region to connect to. 221 222 key 223 Secret key to be used. 224 225 keyid 226 Access key to be used. 227 228 profile 229 A dict with region, key and keyid, or a pillar key (string) 230 that contains a dict with region, key and keyid. 231 """ 232 if not salt.utils.data.exactly_one((subnet_id, subnet_name)): 233 raise SaltInvocationError( 234 "One (but not both) of subnet_id or subnet_name must be provided." 235 ) 236 if not groups: 237 raise SaltInvocationError("groups is a required argument.") 238 if not isinstance(groups, list): 239 raise SaltInvocationError("groups must be a list.") 240 if not isinstance(source_dest_check, bool): 241 raise SaltInvocationError("source_dest_check must be a bool.") 242 ret = {"name": name, "result": True, "comment": "", "changes": {}} 243 r = __salt__["boto_ec2.get_network_interface"]( 244 name=name, region=region, key=key, keyid=keyid, profile=profile 245 ) 246 if "error" in r: 247 ret["result"] = False 248 ret["comment"] = "Error when attempting to find eni: {}.".format( 249 r["error"]["message"] 250 ) 251 return ret 252 if not r["result"]: 253 if __opts__["test"]: 254 ret["comment"] = "ENI is set to be created." 255 if allocate_eip: 256 ret["comment"] = " ".join( 257 [ 258 ret["comment"], 259 "An EIP is set to be allocated/assocaited to the ENI.", 260 ] 261 ) 262 if arecords: 263 ret["comment"] = " ".join( 264 [ret["comment"], "A records are set to be created."] 265 ) 266 ret["result"] = None 267 return ret 268 result_create = __salt__["boto_ec2.create_network_interface"]( 269 name, 270 subnet_id=subnet_id, 271 subnet_name=subnet_name, 272 private_ip_address=private_ip_address, 273 description=description, 274 groups=groups, 275 region=region, 276 key=key, 277 keyid=keyid, 278 profile=profile, 279 ) 280 if "error" in result_create: 281 ret["result"] = False 282 ret["comment"] = "Failed to create ENI: {}".format( 283 result_create["error"]["message"] 284 ) 285 return ret 286 r["result"] = result_create["result"] 287 ret["comment"] = "Created ENI {}".format(name) 288 ret["changes"]["id"] = r["result"]["id"] 289 else: 290 _ret = _eni_attribute( 291 r["result"], "description", description, region, key, keyid, profile 292 ) 293 ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"]) 294 ret["comment"] = _ret["comment"] 295 if not _ret["result"]: 296 ret["result"] = _ret["result"] 297 if ret["result"] is False: 298 return ret 299 _ret = _eni_groups(r["result"], groups, region, key, keyid, profile) 300 ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"]) 301 ret["comment"] = " ".join([ret["comment"], _ret["comment"]]) 302 if not _ret["result"]: 303 ret["result"] = _ret["result"] 304 if ret["result"] is False: 305 return ret 306 # Actions that need to occur whether creating or updating 307 _ret = _eni_attribute( 308 r["result"], "source_dest_check", source_dest_check, region, key, keyid, profile 309 ) 310 ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"]) 311 ret["comment"] = " ".join([ret["comment"], _ret["comment"]]) 312 if not _ret["result"]: 313 ret["result"] = _ret["result"] 314 return ret 315 if allocate_eip: 316 if "allocationId" not in r["result"]: 317 if __opts__["test"]: 318 ret["comment"] = " ".join( 319 [ 320 ret["comment"], 321 "An EIP is set to be allocated and assocaited to the ENI.", 322 ] 323 ) 324 else: 325 domain = "vpc" if allocate_eip == "vpc" else None 326 eip_alloc = __salt__["boto_ec2.allocate_eip_address"]( 327 domain=domain, region=region, key=key, keyid=keyid, profile=profile 328 ) 329 if eip_alloc: 330 _ret = __salt__["boto_ec2.associate_eip_address"]( 331 instance_id=None, 332 instance_name=None, 333 public_ip=None, 334 allocation_id=eip_alloc["allocation_id"], 335 network_interface_id=r["result"]["id"], 336 private_ip_address=None, 337 allow_reassociation=False, 338 region=region, 339 key=key, 340 keyid=keyid, 341 profile=profile, 342 ) 343 if not _ret: 344 _ret = __salt__["boto_ec2.release_eip_address"]( 345 public_ip=None, 346 allocation_id=eip_alloc["allocation_id"], 347 region=region, 348 key=key, 349 keyid=keyid, 350 profile=profile, 351 ) 352 ret["result"] = False 353 msg = ( 354 "Failed to assocaite the allocated EIP address with the" 355 " ENI. The EIP {}".format( 356 "was successfully released." 357 if _ret 358 else "was NOT RELEASED." 359 ) 360 ) 361 ret["comment"] = " ".join([ret["comment"], msg]) 362 return ret 363 else: 364 ret["result"] = False 365 ret["comment"] = " ".join( 366 [ret["comment"], "Failed to allocate an EIP address"] 367 ) 368 return ret 369 else: 370 ret["comment"] = " ".join( 371 [ret["comment"], "An EIP is already allocated/assocaited to the ENI"] 372 ) 373 if arecords: 374 for arecord in arecords: 375 if "name" not in arecord: 376 msg = 'The arecord must contain a "name" property.' 377 raise SaltInvocationError(msg) 378 log.debug("processing arecord %s", arecord) 379 _ret = None 380 dns_provider = "boto_route53" 381 arecord["record_type"] = "A" 382 public_ip_arecord = False 383 if "public" in arecord: 384 public_ip_arecord = arecord.pop("public") 385 if public_ip_arecord: 386 if "publicIp" in r["result"]: 387 arecord["value"] = r["result"]["publicIp"] 388 elif "public_ip" in eip_alloc: 389 arecord["value"] = eip_alloc["public_ip"] 390 else: 391 msg = ( 392 "Unable to add an A record for the public IP address, a public" 393 " IP address does not seem to be allocated to this ENI." 394 ) 395 raise CommandExecutionError(msg) 396 else: 397 arecord["value"] = r["result"]["private_ip_address"] 398 if "provider" in arecord: 399 dns_provider = arecord.pop("provider") 400 if dns_provider == "boto_route53": 401 if "profile" not in arecord: 402 arecord["profile"] = profile 403 if "key" not in arecord: 404 arecord["key"] = key 405 if "keyid" not in arecord: 406 arecord["keyid"] = keyid 407 if "region" not in arecord: 408 arecord["region"] = region 409 _ret = __states__[".".join([dns_provider, "present"])](**arecord) 410 log.debug("ret from dns_provider.present = %s", _ret) 411 ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"]) 412 ret["comment"] = " ".join([ret["comment"], _ret["comment"]]) 413 if not _ret["result"]: 414 ret["result"] = _ret["result"] 415 if ret["result"] is False: 416 return ret 417 return ret 418 419 420def _eni_attribute(metadata, attr, value, region, key, keyid, profile): 421 ret = {"result": True, "comment": "", "changes": {}} 422 if metadata[attr] == value: 423 return ret 424 if __opts__["test"]: 425 ret["comment"] = "ENI set to have {} updated.".format(attr) 426 ret["result"] = None 427 return ret 428 result_update = __salt__["boto_ec2.modify_network_interface_attribute"]( 429 network_interface_id=metadata["id"], 430 attr=attr, 431 value=value, 432 region=region, 433 key=key, 434 keyid=keyid, 435 profile=profile, 436 ) 437 if "error" in result_update: 438 msg = "Failed to update ENI {0}: {1}." 439 ret["result"] = False 440 ret["comment"] = msg.format(attr, result_update["error"]["message"]) 441 else: 442 ret["comment"] = "Updated ENI {}.".format(attr) 443 ret["changes"][attr] = {"old": metadata[attr], "new": value} 444 return ret 445 446 447def _eni_groups(metadata, groups, region, key, keyid, profile): 448 ret = {"result": True, "comment": "", "changes": {}} 449 group_ids = [g["id"] for g in metadata["groups"]] 450 group_ids.sort() 451 _groups = __salt__["boto_secgroup.convert_to_group_ids"]( 452 groups, 453 vpc_id=metadata["vpc_id"], 454 region=region, 455 key=key, 456 keyid=keyid, 457 profile=profile, 458 ) 459 if not _groups: 460 ret["comment"] = "Could not find secgroup ids for provided groups." 461 ret["result"] = False 462 _groups.sort() 463 if group_ids == _groups: 464 return ret 465 if __opts__["test"]: 466 ret["comment"] = "ENI set to have groups updated." 467 ret["result"] = None 468 return ret 469 result_update = __salt__["boto_ec2.modify_network_interface_attribute"]( 470 network_interface_id=metadata["id"], 471 attr="groups", 472 value=_groups, 473 region=region, 474 key=key, 475 keyid=keyid, 476 profile=profile, 477 ) 478 if "error" in result_update: 479 msg = "Failed to update ENI groups: {1}." 480 ret["result"] = False 481 ret["comment"] = msg.format(result_update["error"]["message"]) 482 else: 483 ret["comment"] = "Updated ENI groups." 484 ret["changes"]["groups"] = {"old": group_ids, "new": _groups} 485 return ret 486 487 488def eni_absent( 489 name, release_eip=False, region=None, key=None, keyid=None, profile=None 490): 491 """ 492 Ensure the EC2 ENI is absent. 493 494 .. versionadded:: 2016.3.0 495 496 name 497 Name tag associated with the ENI. 498 499 release_eip 500 True/False - release any EIP associated with the ENI 501 502 region 503 Region to connect to. 504 505 key 506 Secret key to be used. 507 508 keyid 509 Access key to be used. 510 511 profile 512 A dict with region, key and keyid, or a pillar key (string) 513 that contains a dict with region, key and keyid. 514 """ 515 ret = {"name": name, "result": True, "comment": "", "changes": {}} 516 r = __salt__["boto_ec2.get_network_interface"]( 517 name=name, region=region, key=key, keyid=keyid, profile=profile 518 ) 519 if "error" in r: 520 ret["result"] = False 521 ret["comment"] = "Error when attempting to find eni: {}.".format( 522 r["error"]["message"] 523 ) 524 return ret 525 if not r["result"]: 526 if __opts__["test"]: 527 ret["comment"] = "ENI is set to be deleted." 528 ret["result"] = None 529 return ret 530 else: 531 if __opts__["test"]: 532 ret["comment"] = "ENI is set to be deleted." 533 if release_eip and "allocationId" in r["result"]: 534 ret["comment"] = " ".join( 535 [ret["comment"], "Allocated/associated EIP is set to be released"] 536 ) 537 ret["result"] = None 538 return ret 539 if "id" in r["result"]["attachment"]: 540 result_detach = __salt__["boto_ec2.detach_network_interface"]( 541 name=name, 542 force=True, 543 region=region, 544 key=key, 545 keyid=keyid, 546 profile=profile, 547 ) 548 if "error" in result_detach: 549 ret["result"] = False 550 ret["comment"] = "Failed to detach ENI: {}".format( 551 result_detach["error"]["message"] 552 ) 553 return ret 554 # TODO: Ensure the detach occurs before continuing 555 result_delete = __salt__["boto_ec2.delete_network_interface"]( 556 name=name, region=region, key=key, keyid=keyid, profile=profile 557 ) 558 if "error" in result_delete: 559 ret["result"] = False 560 ret["comment"] = "Failed to delete ENI: {}".format( 561 result_delete["error"]["message"] 562 ) 563 return ret 564 ret["comment"] = "Deleted ENI {}".format(name) 565 ret["changes"]["id"] = None 566 if release_eip and "allocationId" in r["result"]: 567 _ret = __salt__["boto_ec2.release_eip_address"]( 568 public_ip=None, 569 allocation_id=r["result"]["allocationId"], 570 region=region, 571 key=key, 572 keyid=keyid, 573 profile=profile, 574 ) 575 if not _ret: 576 ret["comment"] = " ".join( 577 [ret["comment"], "Failed to release EIP allocated to the ENI."] 578 ) 579 ret["result"] = False 580 return ret 581 else: 582 ret["comment"] = " ".join([ret["comment"], "EIP released."]) 583 ret["changes"]["eip released"] = True 584 return ret 585 586 587def snapshot_created( 588 name, 589 ami_name, 590 instance_name, 591 wait_until_available=True, 592 wait_timeout_seconds=300, 593 **kwargs 594): 595 """ 596 Create a snapshot from the given instance 597 598 .. versionadded:: 2016.3.0 599 """ 600 ret = {"name": name, "result": True, "comment": "", "changes": {}} 601 602 if not __salt__["boto_ec2.create_image"]( 603 ami_name=ami_name, instance_name=instance_name, **kwargs 604 ): 605 ret["comment"] = "Failed to create new AMI {ami_name}".format(ami_name=ami_name) 606 ret["result"] = False 607 return ret 608 609 ret["comment"] = "Created new AMI {ami_name}".format(ami_name=ami_name) 610 ret["changes"]["new"] = {ami_name: ami_name} 611 if not wait_until_available: 612 return ret 613 614 starttime = time() 615 while True: 616 images = __salt__["boto_ec2.find_images"]( 617 ami_name=ami_name, return_objs=True, **kwargs 618 ) 619 if images and images[0].state == "available": 620 break 621 if time() - starttime > wait_timeout_seconds: 622 if images: 623 ret["comment"] = "AMI still in state {state} after timeout".format( 624 state=images[0].state 625 ) 626 else: 627 ret[ 628 "comment" 629 ] = "AMI with name {ami_name} not found after timeout.".format( 630 ami_name=ami_name 631 ) 632 ret["result"] = False 633 return ret 634 sleep(5) 635 636 return ret 637 638 639def instance_present( 640 name, 641 instance_name=None, 642 instance_id=None, 643 image_id=None, 644 image_name=None, 645 tags=None, 646 key_name=None, 647 security_groups=None, 648 user_data=None, 649 instance_type=None, 650 placement=None, 651 kernel_id=None, 652 ramdisk_id=None, 653 vpc_id=None, 654 vpc_name=None, 655 monitoring_enabled=None, 656 subnet_id=None, 657 subnet_name=None, 658 private_ip_address=None, 659 block_device_map=None, 660 disable_api_termination=None, 661 instance_initiated_shutdown_behavior=None, 662 placement_group=None, 663 client_token=None, 664 security_group_ids=None, 665 security_group_names=None, 666 additional_info=None, 667 tenancy=None, 668 instance_profile_arn=None, 669 instance_profile_name=None, 670 ebs_optimized=None, 671 network_interfaces=None, 672 network_interface_name=None, 673 network_interface_id=None, 674 attributes=None, 675 target_state=None, 676 public_ip=None, 677 allocation_id=None, 678 allocate_eip=False, 679 region=None, 680 key=None, 681 keyid=None, 682 profile=None, 683): 684 ### TODO - implement 'target_state={running, stopped}' 685 """ 686 Ensure an EC2 instance is running with the given attributes and state. 687 688 name 689 (string) - The name of the state definition. Recommended that this 690 match the instance_name attribute (generally the FQDN of the instance). 691 instance_name 692 (string) - The name of the instance, generally its FQDN. Exclusive with 693 'instance_id'. 694 instance_id 695 (string) - The ID of the instance (if known). Exclusive with 696 'instance_name'. 697 image_id 698 (string) – The ID of the AMI image to run. 699 image_name 700 (string) – The name of the AMI image to run. 701 tags 702 (dict) - Tags to apply to the instance. 703 key_name 704 (string) – The name of the key pair with which to launch instances. 705 security_groups 706 (list of strings) – The names of the EC2 classic security groups with 707 which to associate instances 708 user_data 709 (string) – The Base64-encoded MIME user data to be made available to the 710 instance(s) in this reservation. 711 instance_type 712 (string) – The EC2 instance size/type. Note that only certain types are 713 compatible with HVM based AMIs. 714 placement 715 (string) – The Availability Zone to launch the instance into. 716 kernel_id 717 (string) – The ID of the kernel with which to launch the instances. 718 ramdisk_id 719 (string) – The ID of the RAM disk with which to launch the instances. 720 vpc_id 721 (string) - The ID of a VPC to attach the instance to. 722 vpc_name 723 (string) - The name of a VPC to attach the instance to. 724 monitoring_enabled 725 (bool) – Enable detailed CloudWatch monitoring on the instance. 726 subnet_id 727 (string) – The ID of the subnet within which to launch the instances for 728 VPC. 729 subnet_name 730 (string) – The name of the subnet within which to launch the instances 731 for VPC. 732 private_ip_address 733 (string) – If you’re using VPC, you can optionally use this parameter to 734 assign the instance a specific available IP address from the subnet 735 (e.g., 10.0.0.25). 736 block_device_map 737 (boto.ec2.blockdevicemapping.BlockDeviceMapping) – A BlockDeviceMapping 738 data structure describing the EBS volumes associated with the Image. 739 disable_api_termination 740 (bool) – If True, the instances will be locked and will not be able to 741 be terminated via the API. 742 instance_initiated_shutdown_behavior 743 (string) – Specifies whether the instance stops or terminates on 744 instance-initiated shutdown. Valid values are: 745 746 - 'stop' 747 - 'terminate' 748 749 placement_group 750 (string) – If specified, this is the name of the placement group in 751 which the instance(s) will be launched. 752 client_token 753 (string) – Unique, case-sensitive identifier you provide to ensure 754 idempotency of the request. Maximum 64 ASCII characters. 755 security_group_ids 756 (list of strings) – The IDs of the VPC security groups with which to 757 associate instances. 758 security_group_names 759 (list of strings) – The names of the VPC security groups with which to 760 associate instances. 761 additional_info 762 (string) – Specifies additional information to make available to the 763 instance(s). 764 tenancy 765 (string) – The tenancy of the instance you want to launch. An instance 766 with a tenancy of ‘dedicated’ runs on single-tenant hardware and can 767 only be launched into a VPC. Valid values are:”default” or “dedicated”. 768 NOTE: To use dedicated tenancy you MUST specify a VPC subnet-ID as well. 769 instance_profile_arn 770 (string) – The Amazon resource name (ARN) of the IAM Instance Profile 771 (IIP) to associate with the instances. 772 instance_profile_name 773 (string) – The name of the IAM Instance Profile (IIP) to associate with 774 the instances. 775 ebs_optimized 776 (bool) – Whether the instance is optimized for EBS I/O. This 777 optimization provides dedicated throughput to Amazon EBS and a tuned 778 configuration stack to provide optimal EBS I/O performance. This 779 optimization isn’t available with all instance types. 780 network_interfaces 781 (boto.ec2.networkinterface.NetworkInterfaceCollection) – A 782 NetworkInterfaceCollection data structure containing the ENI 783 specifications for the instance. 784 network_interface_name 785 (string) - The name of Elastic Network Interface to attach 786 787 .. versionadded:: 2016.11.0 788 789 network_interface_id 790 (string) - The id of Elastic Network Interface to attach 791 792 .. versionadded:: 2016.11.0 793 794 attributes 795 (dict) - Instance attributes and value to be applied to the instance. 796 Available options are: 797 798 - instanceType - A valid instance type (m1.small) 799 - kernel - Kernel ID (None) 800 - ramdisk - Ramdisk ID (None) 801 - userData - Base64 encoded String (None) 802 - disableApiTermination - Boolean (true) 803 - instanceInitiatedShutdownBehavior - stop|terminate 804 - blockDeviceMapping - List of strings - ie: [‘/dev/sda=false’] 805 - sourceDestCheck - Boolean (true) 806 - groupSet - Set of Security Groups or IDs 807 - ebsOptimized - Boolean (false) 808 - sriovNetSupport - String - ie: ‘simple’ 809 810 target_state 811 (string) - The desired target state of the instance. Available options 812 are: 813 814 - running 815 - stopped 816 817 Note that this option is currently UNIMPLEMENTED. 818 public_ip: 819 (string) - The IP of a previously allocated EIP address, which will be 820 attached to the instance. EC2 Classic instances ONLY - for VCP pass in 821 an allocation_id instead. 822 allocation_id: 823 (string) - The ID of a previously allocated EIP address, which will be 824 attached to the instance. VPC instances ONLY - for Classic pass in 825 a public_ip instead. 826 allocate_eip: 827 (bool) - Allocate and attach an EIP on-the-fly for this instance. Note 828 you'll want to release this address when terminating the instance, 829 either manually or via the 'release_eip' flag to 'instance_absent'. 830 region 831 (string) - Region to connect to. 832 key 833 (string) - Secret key to be used. 834 keyid 835 (string) - Access key to be used. 836 profile 837 (variable) - A dict with region, key and keyid, or a pillar key (string) 838 that contains a dict with region, key and keyid. 839 840 .. versionadded:: 2016.3.0 841 """ 842 ret = {"name": name, "result": True, "comment": "", "changes": {}} 843 _create = False 844 running_states = ("pending", "rebooting", "running", "stopping", "stopped") 845 changed_attrs = {} 846 847 if not salt.utils.data.exactly_one((image_id, image_name)): 848 raise SaltInvocationError( 849 "Exactly one of image_id OR image_name must be provided." 850 ) 851 if (public_ip or allocation_id or allocate_eip) and not salt.utils.data.exactly_one( 852 (public_ip, allocation_id, allocate_eip) 853 ): 854 raise SaltInvocationError( 855 "At most one of public_ip, allocation_id OR allocate_eip may be provided." 856 ) 857 858 if instance_id: 859 exists = __salt__["boto_ec2.exists"]( 860 instance_id=instance_id, 861 region=region, 862 key=key, 863 keyid=keyid, 864 profile=profile, 865 in_states=running_states, 866 ) 867 if not exists: 868 _create = True 869 else: 870 instances = __salt__["boto_ec2.find_instances"]( 871 name=instance_name if instance_name else name, 872 region=region, 873 key=key, 874 keyid=keyid, 875 profile=profile, 876 in_states=running_states, 877 ) 878 if not instances: 879 _create = True 880 elif len(instances) > 1: 881 log.debug( 882 "Multiple instances matching criteria found - cannot determine a" 883 " singular instance-id" 884 ) 885 instance_id = None # No way to know, we'll just have to bail later.... 886 else: 887 instance_id = instances[0] 888 889 if _create: 890 if __opts__["test"]: 891 ret["comment"] = "The instance {} is set to be created.".format(name) 892 ret["result"] = None 893 return ret 894 if image_name: 895 args = { 896 "ami_name": image_name, 897 "region": region, 898 "key": key, 899 "keyid": keyid, 900 "profile": profile, 901 } 902 image_ids = __salt__["boto_ec2.find_images"](**args) 903 if image_ids: 904 image_id = image_ids[0] 905 else: 906 image_id = image_name 907 r = __salt__["boto_ec2.run"]( 908 image_id, 909 instance_name if instance_name else name, 910 tags=tags, 911 key_name=key_name, 912 security_groups=security_groups, 913 user_data=user_data, 914 instance_type=instance_type, 915 placement=placement, 916 kernel_id=kernel_id, 917 ramdisk_id=ramdisk_id, 918 vpc_id=vpc_id, 919 vpc_name=vpc_name, 920 monitoring_enabled=monitoring_enabled, 921 subnet_id=subnet_id, 922 subnet_name=subnet_name, 923 private_ip_address=private_ip_address, 924 block_device_map=block_device_map, 925 disable_api_termination=disable_api_termination, 926 instance_initiated_shutdown_behavior=instance_initiated_shutdown_behavior, 927 placement_group=placement_group, 928 client_token=client_token, 929 security_group_ids=security_group_ids, 930 security_group_names=security_group_names, 931 additional_info=additional_info, 932 tenancy=tenancy, 933 instance_profile_arn=instance_profile_arn, 934 instance_profile_name=instance_profile_name, 935 ebs_optimized=ebs_optimized, 936 network_interfaces=network_interfaces, 937 network_interface_name=network_interface_name, 938 network_interface_id=network_interface_id, 939 region=region, 940 key=key, 941 keyid=keyid, 942 profile=profile, 943 ) 944 if not r or "instance_id" not in r: 945 ret["result"] = False 946 ret["comment"] = "Failed to create instance {}.".format( 947 instance_name if instance_name else name 948 ) 949 return ret 950 951 instance_id = r["instance_id"] 952 ret["changes"] = {"old": {}, "new": {}} 953 ret["changes"]["old"]["instance_id"] = None 954 ret["changes"]["new"]["instance_id"] = instance_id 955 956 # To avoid issues we only allocate new EIPs at instance creation. 957 # This might miss situations where an instance is initially created 958 # created without and one is added later, but the alternative is the 959 # risk of EIPs allocated at every state run. 960 if allocate_eip: 961 if __opts__["test"]: 962 ret["comment"] = "New EIP would be allocated." 963 ret["result"] = None 964 return ret 965 domain = "vpc" if vpc_id or vpc_name else None 966 r = __salt__["boto_ec2.allocate_eip_address"]( 967 domain=domain, region=region, key=key, keyid=keyid, profile=profile 968 ) 969 if not r: 970 ret["result"] = False 971 ret["comment"] = "Failed to allocate new EIP." 972 return ret 973 allocation_id = r["allocation_id"] 974 log.info("New EIP with address %s allocated.", r["public_ip"]) 975 else: 976 log.info("EIP not requested.") 977 978 if public_ip or allocation_id: 979 # This can take a bit to show up, give it a chance to... 980 tries = 10 981 secs = 3 982 for t in range(tries): 983 r = __salt__["boto_ec2.get_eip_address_info"]( 984 addresses=public_ip, 985 allocation_ids=allocation_id, 986 region=region, 987 key=key, 988 keyid=keyid, 989 profile=profile, 990 ) 991 if r: 992 break 993 else: 994 log.info( 995 "Waiting up to %s secs for new EIP %s to become available", 996 tries * secs, 997 public_ip or allocation_id, 998 ) 999 time.sleep(secs) 1000 if not r: 1001 ret["result"] = False 1002 ret["comment"] = "Failed to lookup EIP {}.".format( 1003 public_ip or allocation_id 1004 ) 1005 return ret 1006 ip = r[0]["public_ip"] 1007 if r[0].get("instance_id"): 1008 if r[0]["instance_id"] != instance_id: 1009 ret["result"] = False 1010 ret[ 1011 "comment" 1012 ] = "EIP {} is already associated with instance {}.".format( 1013 public_ip if public_ip else allocation_id, r[0]["instance_id"] 1014 ) 1015 return ret 1016 else: 1017 if __opts__["test"]: 1018 ret["comment"] = "Instance {} to be updated.".format(name) 1019 ret["result"] = None 1020 return ret 1021 r = __salt__["boto_ec2.associate_eip_address"]( 1022 instance_id=instance_id, 1023 public_ip=public_ip, 1024 allocation_id=allocation_id, 1025 region=region, 1026 key=key, 1027 keyid=keyid, 1028 profile=profile, 1029 ) 1030 if r: 1031 if "new" not in ret["changes"]: 1032 ret["changes"]["new"] = {} 1033 ret["changes"]["new"]["public_ip"] = ip 1034 else: 1035 ret["result"] = False 1036 ret["comment"] = "Failed to attach EIP to instance {}.".format( 1037 instance_name if instance_name else name 1038 ) 1039 return ret 1040 1041 if attributes: 1042 for k, v in attributes.items(): 1043 curr = __salt__["boto_ec2.get_attribute"]( 1044 k, 1045 instance_id=instance_id, 1046 region=region, 1047 key=key, 1048 keyid=keyid, 1049 profile=profile, 1050 ) 1051 curr = {} if not isinstance(curr, dict) else curr 1052 if curr.get(k) == v: 1053 continue 1054 else: 1055 if __opts__["test"]: 1056 changed_attrs[k] = ( 1057 "The instance attribute {} is set to be changed from '{}' to" 1058 " '{}'.".format(k, curr.get(k), v) 1059 ) 1060 continue 1061 try: 1062 r = __salt__["boto_ec2.set_attribute"]( 1063 attribute=k, 1064 attribute_value=v, 1065 instance_id=instance_id, 1066 region=region, 1067 key=key, 1068 keyid=keyid, 1069 profile=profile, 1070 ) 1071 except SaltInvocationError as e: 1072 ret["result"] = False 1073 ret[ 1074 "comment" 1075 ] = "Failed to set attribute {} to {} on instance {}.".format( 1076 k, v, instance_name 1077 ) 1078 return ret 1079 ret["changes"] = ( 1080 ret["changes"] if ret["changes"] else {"old": {}, "new": {}} 1081 ) 1082 ret["changes"]["old"][k] = curr.get(k) 1083 ret["changes"]["new"][k] = v 1084 1085 if __opts__["test"]: 1086 if changed_attrs: 1087 ret["changes"]["new"] = changed_attrs 1088 ret["result"] = None 1089 else: 1090 ret["comment"] = "Instance {} is in the correct state".format( 1091 instance_name if instance_name else name 1092 ) 1093 ret["result"] = True 1094 1095 if tags and instance_id is not None: 1096 tags = dict(tags) 1097 curr_tags = dict( 1098 __salt__["boto_ec2.get_all_tags"]( 1099 filters={"resource-id": instance_id}, 1100 region=region, 1101 key=key, 1102 keyid=keyid, 1103 profile=profile, 1104 ).get(instance_id, {}) 1105 ) 1106 current = set(curr_tags.keys()) 1107 desired = set(tags.keys()) 1108 remove = list( 1109 current - desired 1110 ) # Boto explicitly requires a list here and can't cope with a set... 1111 add = {t: tags[t] for t in desired - current} 1112 replace = {t: tags[t] for t in tags if tags.get(t) != curr_tags.get(t)} 1113 # Tag keys are unique despite the bizarre semantics uses which make it LOOK like they could be duplicative. 1114 add.update(replace) 1115 if add or remove: 1116 if __opts__["test"]: 1117 ret["changes"]["old"] = ( 1118 ret["changes"]["old"] if "old" in ret["changes"] else {} 1119 ) 1120 ret["changes"]["new"] = ( 1121 ret["changes"]["new"] if "new" in ret["changes"] else {} 1122 ) 1123 ret["changes"]["old"]["tags"] = curr_tags 1124 ret["changes"]["new"]["tags"] = tags 1125 ret["comment"] += " Tags would be updated on instance {}.".format( 1126 instance_name if instance_name else name 1127 ) 1128 else: 1129 if remove: 1130 if not __salt__["boto_ec2.delete_tags"]( 1131 resource_ids=instance_id, 1132 tags=remove, 1133 region=region, 1134 key=key, 1135 keyid=keyid, 1136 profile=profile, 1137 ): 1138 msg = "Error while deleting tags on instance {}".format( 1139 instance_name if instance_name else name 1140 ) 1141 log.error(msg) 1142 ret["comment"] += " " + msg 1143 ret["result"] = False 1144 return ret 1145 if add: 1146 if not __salt__["boto_ec2.create_tags"]( 1147 resource_ids=instance_id, 1148 tags=add, 1149 region=region, 1150 key=key, 1151 keyid=keyid, 1152 profile=profile, 1153 ): 1154 msg = "Error while creating tags on instance {}".format( 1155 instance_name if instance_name else name 1156 ) 1157 log.error(msg) 1158 ret["comment"] += " " + msg 1159 ret["result"] = False 1160 return ret 1161 ret["changes"]["old"] = ( 1162 ret["changes"]["old"] if "old" in ret["changes"] else {} 1163 ) 1164 ret["changes"]["new"] = ( 1165 ret["changes"]["new"] if "new" in ret["changes"] else {} 1166 ) 1167 ret["changes"]["old"]["tags"] = curr_tags 1168 ret["changes"]["new"]["tags"] = tags 1169 1170 return ret 1171 1172 1173def instance_absent( 1174 name, 1175 instance_name=None, 1176 instance_id=None, 1177 release_eip=False, 1178 region=None, 1179 key=None, 1180 keyid=None, 1181 profile=None, 1182 filters=None, 1183): 1184 """ 1185 Ensure an EC2 instance does not exist (is stopped and removed). 1186 1187 .. versionchanged:: 2016.11.0 1188 1189 name 1190 (string) - The name of the state definition. 1191 instance_name 1192 (string) - The name of the instance. 1193 instance_id 1194 (string) - The ID of the instance. 1195 release_eip 1196 (bool) - Release any associated EIPs during termination. 1197 region 1198 (string) - Region to connect to. 1199 key 1200 (string) - Secret key to be used. 1201 keyid 1202 (string) - Access key to be used. 1203 profile 1204 (variable) - A dict with region, key and keyid, or a pillar key (string) 1205 that contains a dict with region, key and keyid. 1206 filters 1207 (dict) - A dict of additional filters to use in matching the instance to 1208 delete. 1209 1210 YAML example fragment: 1211 1212 .. code-block:: yaml 1213 1214 - filters: 1215 vpc-id: vpc-abcdef12 1216 """ 1217 ### TODO - Implement 'force' option?? Would automagically turn off 1218 ### 'disableApiTermination', as needed, before trying to delete. 1219 ret = {"name": name, "result": True, "comment": "", "changes": {}} 1220 running_states = ("pending", "rebooting", "running", "stopping", "stopped") 1221 1222 if not instance_id: 1223 try: 1224 instance_id = __salt__["boto_ec2.get_id"]( 1225 name=instance_name if instance_name else name, 1226 region=region, 1227 key=key, 1228 keyid=keyid, 1229 profile=profile, 1230 in_states=running_states, 1231 filters=filters, 1232 ) 1233 except CommandExecutionError as e: 1234 ret["result"] = None 1235 ret["comment"] = "Couldn't determine current status of instance {}.".format( 1236 instance_name or name 1237 ) 1238 return ret 1239 1240 instances = __salt__["boto_ec2.find_instances"]( 1241 instance_id=instance_id, 1242 region=region, 1243 key=key, 1244 keyid=keyid, 1245 profile=profile, 1246 return_objs=True, 1247 filters=filters, 1248 ) 1249 if not instances: 1250 ret["result"] = True 1251 ret["comment"] = "Instance {} is already gone.".format(instance_id) 1252 return ret 1253 instance = instances[0] 1254 1255 ### Honor 'disableApiTermination' - if you want to override it, first use set_attribute() to turn it off 1256 no_can_do = __salt__["boto_ec2.get_attribute"]( 1257 "disableApiTermination", 1258 instance_id=instance_id, 1259 region=region, 1260 key=key, 1261 keyid=keyid, 1262 profile=profile, 1263 ) 1264 if no_can_do.get("disableApiTermination") is True: 1265 ret["result"] = False 1266 ret["comment"] = "Termination of instance {} via the API is disabled.".format( 1267 instance_id 1268 ) 1269 return ret 1270 1271 if __opts__["test"]: 1272 ret["comment"] = "The instance {} is set to be deleted.".format(name) 1273 ret["result"] = None 1274 return ret 1275 1276 r = __salt__["boto_ec2.terminate"]( 1277 instance_id=instance_id, 1278 name=instance_name, 1279 region=region, 1280 key=key, 1281 keyid=keyid, 1282 profile=profile, 1283 ) 1284 if not r: 1285 ret["result"] = False 1286 ret["comment"] = "Failed to terminate instance {}.".format(instance_id) 1287 return ret 1288 1289 ret["changes"]["old"] = {"instance_id": instance_id} 1290 ret["changes"]["new"] = None 1291 1292 if release_eip: 1293 ip = getattr(instance, "ip_address", None) 1294 if ip: 1295 base_args = { 1296 "region": region, 1297 "key": key, 1298 "keyid": keyid, 1299 "profile": profile, 1300 } 1301 public_ip = None 1302 alloc_id = None 1303 assoc_id = None 1304 if getattr(instance, "vpc_id", None): 1305 r = __salt__["boto_ec2.get_eip_address_info"](addresses=ip, **base_args) 1306 if r and "allocation_id" in r[0]: 1307 alloc_id = r[0]["allocation_id"] 1308 assoc_id = r[0].get("association_id") 1309 else: 1310 # I /believe/ this situation is impossible but let's hedge our bets... 1311 ret["result"] = False 1312 ret[ 1313 "comment" 1314 ] = "Can't determine AllocationId for address {}.".format(ip) 1315 return ret 1316 else: 1317 public_ip = instance.ip_address 1318 1319 if assoc_id: 1320 # Race here - sometimes the terminate above will already have dropped this 1321 if not __salt__["boto_ec2.disassociate_eip_address"]( 1322 association_id=assoc_id, **base_args 1323 ): 1324 log.warning("Failed to disassociate EIP %s.", ip) 1325 1326 if __salt__["boto_ec2.release_eip_address"]( 1327 allocation_id=alloc_id, public_ip=public_ip, **base_args 1328 ): 1329 log.info("Released EIP address %s", public_ip or r[0]["public_ip"]) 1330 ret["changes"]["old"]["public_ip"] = public_ip or r[0]["public_ip"] 1331 else: 1332 ret["result"] = False 1333 ret["comment"] = "Failed to release EIP {}.".format(ip) 1334 return ret 1335 1336 return ret 1337 1338 1339def volume_absent( 1340 name, 1341 volume_name=None, 1342 volume_id=None, 1343 instance_name=None, 1344 instance_id=None, 1345 device=None, 1346 region=None, 1347 key=None, 1348 keyid=None, 1349 profile=None, 1350): 1351 """ 1352 Ensure the EC2 volume is detached and absent. 1353 1354 .. versionadded:: 2016.11.0 1355 1356 name 1357 State definition name. 1358 1359 volume_name 1360 Name tag associated with the volume. For safety, if this matches more than 1361 one volume, the state will refuse to apply. 1362 1363 volume_id 1364 Resource ID of the volume. 1365 1366 instance_name 1367 Only remove volume if it is attached to instance with this Name tag. 1368 Exclusive with 'instance_id'. Requires 'device'. 1369 1370 instance_id 1371 Only remove volume if it is attached to this instance. 1372 Exclusive with 'instance_name'. Requires 'device'. 1373 1374 device 1375 Match by device rather than ID. Requires one of 'instance_name' or 1376 'instance_id'. 1377 1378 region 1379 Region to connect to. 1380 1381 key 1382 Secret key to be used. 1383 1384 keyid 1385 Access key to be used. 1386 1387 profile 1388 A dict with region, key and keyid, or a pillar key (string) 1389 that contains a dict with region, key and keyid. 1390 1391 """ 1392 1393 ret = {"name": name, "result": True, "comment": "", "changes": {}} 1394 filters = {} 1395 running_states = ("pending", "rebooting", "running", "stopping", "stopped") 1396 1397 if not salt.utils.data.exactly_one( 1398 (volume_name, volume_id, instance_name, instance_id) 1399 ): 1400 raise SaltInvocationError( 1401 "Exactly one of 'volume_name', 'volume_id', " 1402 "'instance_name', or 'instance_id' must be provided." 1403 ) 1404 if (instance_name or instance_id) and not device: 1405 raise SaltInvocationError( 1406 "Parameter 'device' is required when either " 1407 "'instance_name' or 'instance_id' is specified." 1408 ) 1409 if volume_id: 1410 filters.update({"volume-id": volume_id}) 1411 if volume_name: 1412 filters.update({"tag:Name": volume_name}) 1413 if instance_name: 1414 instance_id = __salt__["boto_ec2.get_id"]( 1415 name=instance_name, 1416 region=region, 1417 key=key, 1418 keyid=keyid, 1419 profile=profile, 1420 in_states=running_states, 1421 ) 1422 if not instance_id: 1423 ret["comment"] = ( 1424 "Instance with Name {} not found. Assuming " 1425 "associated volumes gone.".format(instance_name) 1426 ) 1427 return ret 1428 if instance_id: 1429 filters.update({"attachment.instance-id": instance_id}) 1430 if device: 1431 filters.update({"attachment.device": device}) 1432 1433 args = {"region": region, "key": key, "keyid": keyid, "profile": profile} 1434 1435 vols = __salt__["boto_ec2.get_all_volumes"](filters=filters, **args) 1436 if len(vols) < 1: 1437 ret["comment"] = "Volume matching criteria not found, assuming already absent" 1438 return ret 1439 if len(vols) > 1: 1440 msg = ( 1441 "More than one volume matched criteria, can't continue in state {}".format( 1442 name 1443 ) 1444 ) 1445 log.error(msg) 1446 ret["comment"] = msg 1447 ret["result"] = False 1448 return ret 1449 vol = vols[0] 1450 log.info("Matched Volume ID %s", vol) 1451 1452 if __opts__["test"]: 1453 ret["comment"] = "The volume {} is set to be deleted.".format(vol) 1454 ret["result"] = None 1455 return ret 1456 if __salt__["boto_ec2.delete_volume"](volume_id=vol, force=True, **args): 1457 ret["comment"] = "Volume {} deleted.".format(vol) 1458 ret["changes"] = {"old": {"volume_id": vol}, "new": {"volume_id": None}} 1459 else: 1460 ret["comment"] = "Error deleting volume {}.".format(vol) 1461 ret["result"] = False 1462 return ret 1463 1464 1465def volumes_tagged( 1466 name, tag_maps, authoritative=False, region=None, key=None, keyid=None, profile=None 1467): 1468 """ 1469 Ensure EC2 volume(s) matching the given filters have the defined tags. 1470 1471 .. versionadded:: 2016.11.0 1472 1473 name 1474 State definition name. 1475 1476 tag_maps 1477 List of dicts of filters and tags, where 'filters' is a dict suitable for passing 1478 to the 'filters' argument of boto_ec2.get_all_volumes(), and 'tags' is a dict of 1479 tags to be set on volumes as matched by the given filters. The filter syntax is 1480 extended to permit passing either a list of volume_ids or an instance_name (with 1481 instance_name being the Name tag of the instance to which the desired volumes are 1482 mapped). Each mapping in the list is applied separately, so multiple sets of 1483 volumes can be all tagged differently with one call to this function. 1484 1485 YAML example fragment: 1486 1487 .. code-block:: yaml 1488 1489 - filters: 1490 attachment.instance_id: i-abcdef12 1491 tags: 1492 Name: dev-int-abcdef12.aws-foo.com 1493 - filters: 1494 attachment.device: /dev/sdf 1495 tags: 1496 ManagedSnapshots: true 1497 BillingGroup: bubba.hotep@aws-foo.com 1498 - filters: 1499 instance_name: prd-foo-01.aws-foo.com 1500 tags: 1501 Name: prd-foo-01.aws-foo.com 1502 BillingGroup: infra-team@aws-foo.com 1503 - filters: 1504 volume_ids: [ vol-12345689, vol-abcdef12 ] 1505 tags: 1506 BillingGroup: infra-team@aws-foo.com 1507 1508 authoritative 1509 Should un-declared tags currently set on matched volumes be deleted? Boolean. 1510 1511 region 1512 Region to connect to. 1513 1514 key 1515 Secret key to be used. 1516 1517 keyid 1518 Access key to be used. 1519 1520 profile 1521 A dict with region, key and keyid, or a pillar key (string) 1522 that contains a dict with region, key and keyid. 1523 1524 """ 1525 1526 ret = {"name": name, "result": True, "comment": "", "changes": {}} 1527 args = { 1528 "tag_maps": tag_maps, 1529 "authoritative": authoritative, 1530 "region": region, 1531 "key": key, 1532 "keyid": keyid, 1533 "profile": profile, 1534 } 1535 1536 if __opts__["test"]: 1537 args["dry_run"] = True 1538 r = __salt__["boto_ec2.set_volumes_tags"](**args) 1539 if r["success"]: 1540 if r.get("changes"): 1541 ret["comment"] = "Tags would be updated." 1542 ret["changes"] = r["changes"] 1543 ret["result"] = None 1544 else: 1545 ret["comment"] = "Error validating requested volume tags." 1546 ret["result"] = False 1547 return ret 1548 r = __salt__["boto_ec2.set_volumes_tags"](**args) 1549 if r["success"]: 1550 if r.get("changes"): 1551 ret["comment"] = "Tags applied." 1552 ret["changes"] = r["changes"] 1553 else: 1554 ret["comment"] = "Error updating requested volume tags." 1555 ret["result"] = False 1556 return ret 1557 1558 1559def volume_present( 1560 name, 1561 volume_name=None, 1562 volume_id=None, 1563 instance_name=None, 1564 instance_id=None, 1565 device=None, 1566 size=None, 1567 snapshot_id=None, 1568 volume_type=None, 1569 iops=None, 1570 encrypted=False, 1571 kms_key_id=None, 1572 region=None, 1573 key=None, 1574 keyid=None, 1575 profile=None, 1576): 1577 """ 1578 Ensure the EC2 volume is present and attached. 1579 1580 .. 1581 1582 name 1583 State definition name. 1584 1585 volume_name 1586 The Name tag value for the volume. If no volume with that matching name tag is found, 1587 a new volume will be created. If multiple volumes are matched, the state will fail. 1588 1589 volume_id 1590 Resource ID of the volume. Exclusive with 'volume_name'. 1591 1592 instance_name 1593 Attach volume to instance with this Name tag. 1594 Exclusive with 'instance_id'. 1595 1596 instance_id 1597 Attach volume to instance with this ID. 1598 Exclusive with 'instance_name'. 1599 1600 device 1601 The device on the instance through which the volume is exposed (e.g. /dev/sdh) 1602 1603 size 1604 The size of the new volume, in GiB. If you're creating the volume from a snapshot 1605 and don't specify a volume size, the default is the snapshot size. Optionally specified 1606 at volume creation time; will be ignored afterward. Requires 'volume_name'. 1607 1608 snapshot_id 1609 The snapshot ID from which the new Volume will be created. Optionally specified 1610 at volume creation time; will be ignored afterward. Requires 'volume_name'. 1611 1612 volume_type 1613 The type of the volume. Optionally specified at volume creation time; will be ignored afterward. 1614 Requires 'volume_name'. 1615 Valid volume types for AWS can be found here: 1616 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html 1617 1618 iops 1619 The provisioned IOPS you want to associate with this volume. Optionally specified 1620 at volume creation time; will be ignored afterward. Requires 'volume_name'. 1621 1622 encrypted 1623 Specifies whether the volume should be encrypted. Optionally specified 1624 at volume creation time; will be ignored afterward. Requires 'volume_name'. 1625 1626 kms_key_id 1627 If encrypted is True, this KMS Key ID may be specified to encrypt volume with this key. 1628 Optionally specified at volume creation time; will be ignored afterward. 1629 Requires 'volume_name'. 1630 e.g.: arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef 1631 1632 region 1633 Region to connect to. 1634 1635 key 1636 Secret key to be used. 1637 1638 keyid 1639 Access key to be used. 1640 1641 profile 1642 A dict with region, key and keyid, or a pillar key (string) 1643 that contains a dict with region, key and keyid. 1644 1645 """ 1646 1647 ret = {"name": name, "result": True, "comment": "", "changes": {}} 1648 old_dict = {} 1649 new_dict = {} 1650 running_states = ("running", "stopped") 1651 1652 if not salt.utils.data.exactly_one((volume_name, volume_id)): 1653 raise SaltInvocationError( 1654 "Exactly one of 'volume_name', 'volume_id', must be provided." 1655 ) 1656 if not salt.utils.data.exactly_one((instance_name, instance_id)): 1657 raise SaltInvocationError( 1658 "Exactly one of 'instance_name', or 'instance_id' must be provided." 1659 ) 1660 if device is None: 1661 raise SaltInvocationError("Parameter 'device' is required.") 1662 args = {"region": region, "key": key, "keyid": keyid, "profile": profile} 1663 if instance_name: 1664 instance_id = __salt__["boto_ec2.get_id"]( 1665 name=instance_name, in_states=running_states, **args 1666 ) 1667 if not instance_id: 1668 raise SaltInvocationError( 1669 "Instance with Name {} not found.".format(instance_name) 1670 ) 1671 1672 instances = __salt__["boto_ec2.find_instances"]( 1673 instance_id=instance_id, return_objs=True, **args 1674 ) 1675 instance = instances[0] 1676 if volume_name: 1677 filters = {} 1678 filters.update({"tag:Name": volume_name}) 1679 vols = __salt__["boto_ec2.get_all_volumes"](filters=filters, **args) 1680 if len(vols) > 1: 1681 msg = ( 1682 "More than one volume matched volume name {}, can't continue in" 1683 " state {}".format(volume_name, name) 1684 ) 1685 raise SaltInvocationError(msg) 1686 if len(vols) < 1: 1687 if __opts__["test"]: 1688 ret["comment"] = ( 1689 "The volume with name {} is set to be created and attached" 1690 " on {}({}).".format(volume_name, instance_id, device) 1691 ) 1692 ret["result"] = None 1693 return ret 1694 _rt = __salt__["boto_ec2.create_volume"]( 1695 zone_name=instance.placement, 1696 size=size, 1697 snapshot_id=snapshot_id, 1698 volume_type=volume_type, 1699 iops=iops, 1700 encrypted=encrypted, 1701 kms_key_id=kms_key_id, 1702 wait_for_creation=True, 1703 **args 1704 ) 1705 if "result" in _rt: 1706 volume_id = _rt["result"] 1707 else: 1708 raise SaltInvocationError( 1709 "Error creating volume with name {}.".format(volume_name) 1710 ) 1711 _rt = __salt__["boto_ec2.set_volumes_tags"]( 1712 tag_maps=[ 1713 { 1714 "filters": {"volume_ids": [volume_id]}, 1715 "tags": {"Name": volume_name}, 1716 } 1717 ], 1718 **args 1719 ) 1720 if _rt["success"] is False: 1721 raise SaltInvocationError( 1722 "Error updating requested volume {} with name {}. {}".format( 1723 volume_id, volume_name, _rt["comment"] 1724 ) 1725 ) 1726 old_dict["volume_id"] = None 1727 new_dict["volume_id"] = volume_id 1728 else: 1729 volume_id = vols[0] 1730 vols = __salt__["boto_ec2.get_all_volumes"]( 1731 volume_ids=[volume_id], return_objs=True, **args 1732 ) 1733 if len(vols) < 1: 1734 raise SaltInvocationError("Volume {} do not exist".format(volume_id)) 1735 vol = vols[0] 1736 if vol.zone != instance.placement: 1737 raise SaltInvocationError( 1738 "Volume {} in {} cannot attach to instance {} in {}.".format( 1739 volume_id, vol.zone, instance_id, instance.placement 1740 ) 1741 ) 1742 attach_data = vol.attach_data 1743 if attach_data is not None and attach_data.instance_id is not None: 1744 if instance_id == attach_data.instance_id and device == attach_data.device: 1745 ret["comment"] = "The volume {} is attached on {}({}).".format( 1746 volume_id, instance_id, device 1747 ) 1748 return ret 1749 else: 1750 if __opts__["test"]: 1751 ret[ 1752 "comment" 1753 ] = "The volume {} is set to be detached from {}({} and attached on {}({}).".format( 1754 attach_data.instance_id, 1755 attach_data.devic, 1756 volume_id, 1757 instance_id, 1758 device, 1759 ) 1760 ret["result"] = None 1761 return ret 1762 if __salt__["boto_ec2.detach_volume"]( 1763 volume_id=volume_id, wait_for_detachement=True, **args 1764 ): 1765 ret["comment"] = "Volume {} is detached from {}({}).".format( 1766 volume_id, attach_data.instance_id, attach_data.device 1767 ) 1768 old_dict["instance_id"] = attach_data.instance_id 1769 old_dict["device"] = attach_data.device 1770 else: 1771 raise SaltInvocationError( 1772 "The volume {} is already attached on instance {}({})." 1773 " Failed to detach".format( 1774 volume_id, attach_data.instance_id, attach_data.device 1775 ) 1776 ) 1777 else: 1778 old_dict["instance_id"] = instance_id 1779 old_dict["device"] = None 1780 if __opts__["test"]: 1781 ret["comment"] = "The volume {} is set to be attached on {}({}).".format( 1782 volume_id, instance_id, device 1783 ) 1784 ret["result"] = None 1785 return ret 1786 if __salt__["boto_ec2.attach_volume"]( 1787 volume_id=volume_id, instance_id=instance_id, device=device, **args 1788 ): 1789 ret["comment"] = " ".join( 1790 [ 1791 ret["comment"], 1792 "Volume {} is attached on {}({}).".format( 1793 volume_id, instance_id, device 1794 ), 1795 ] 1796 ) 1797 new_dict["instance_id"] = instance_id 1798 new_dict["device"] = device 1799 ret["changes"] = {"old": old_dict, "new": new_dict} 1800 else: 1801 ret["comment"] = "Error attaching volume {} to instance {}({}).".format( 1802 volume_id, instance_id, device 1803 ) 1804 ret["result"] = False 1805 return ret 1806 1807 1808def private_ips_present( 1809 name, 1810 network_interface_name=None, 1811 network_interface_id=None, 1812 private_ip_addresses=None, 1813 allow_reassignment=False, 1814 region=None, 1815 key=None, 1816 keyid=None, 1817 profile=None, 1818): 1819 """ 1820 Ensure an ENI has secondary private ip addresses associated with it 1821 1822 name 1823 (String) - State definition name 1824 network_interface_id 1825 (String) - The EC2 network interface id, example eni-123456789 1826 private_ip_addresses 1827 (List or String) - The secondary private ip address(es) that should be present on the ENI. 1828 allow_reassignment 1829 (Boolean) - If true, will reassign a secondary private ip address associated with another 1830 ENI. If false, state will fail if the secondary private ip address is associated with 1831 another ENI. 1832 region 1833 (string) - Region to connect to. 1834 key 1835 (string) - Secret key to be used. 1836 keyid 1837 (string) - Access key to be used. 1838 profile 1839 (variable) - A dict with region, key and keyid, or a pillar key (string) that contains a 1840 dict with region, key and keyid. 1841 """ 1842 1843 if not salt.utils.data.exactly_one((network_interface_name, network_interface_id)): 1844 raise SaltInvocationError( 1845 "Exactly one of 'network_interface_name', " 1846 "'network_interface_id' must be provided" 1847 ) 1848 1849 if not private_ip_addresses: 1850 raise SaltInvocationError( 1851 "You must provide the private_ip_addresses to associate with the ENI" 1852 ) 1853 1854 ret = { 1855 "name": name, 1856 "result": True, 1857 "comment": "", 1858 "changes": {"old": [], "new": []}, 1859 } 1860 1861 get_eni_args = { 1862 "name": network_interface_name, 1863 "network_interface_id": network_interface_id, 1864 "region": region, 1865 "key": key, 1866 "keyid": keyid, 1867 "profile": profile, 1868 } 1869 1870 eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args) 1871 1872 # Check if there are any new secondary private ips to add to the eni 1873 if eni and eni.get("result", {}).get("private_ip_addresses"): 1874 for eni_pip in eni["result"]["private_ip_addresses"]: 1875 ret["changes"]["old"].append(eni_pip["private_ip_address"]) 1876 1877 ips_to_add = [] 1878 for private_ip in private_ip_addresses: 1879 if private_ip not in ret["changes"]["old"]: 1880 ips_to_add.append(private_ip) 1881 1882 if ips_to_add: 1883 if not __opts__["test"]: 1884 # Assign secondary private ips to ENI 1885 assign_ips_args = { 1886 "network_interface_id": network_interface_id, 1887 "private_ip_addresses": ips_to_add, 1888 "allow_reassignment": allow_reassignment, 1889 "region": region, 1890 "key": key, 1891 "keyid": keyid, 1892 "profile": profile, 1893 } 1894 1895 __salt__["boto_ec2.assign_private_ip_addresses"](**assign_ips_args) 1896 1897 # Verify secondary private ips were properly assigned to ENI 1898 eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args) 1899 if eni and eni.get("result", {}).get("private_ip_addresses", None): 1900 for eni_pip in eni["result"]["private_ip_addresses"]: 1901 ret["changes"]["new"].append(eni_pip["private_ip_address"]) 1902 1903 ips_not_added = [] 1904 for private_ip in private_ip_addresses: 1905 if private_ip not in ret["changes"]["new"]: 1906 ips_not_added.append(private_ip) 1907 1908 # Display results 1909 if ips_not_added: 1910 ret["result"] = False 1911 ret["comment"] = ( 1912 "ips on eni: {}\n" 1913 "attempted to add: {}\n" 1914 "could not add the following ips: {}\n".format( 1915 "\n\t- " + "\n\t- ".join(ret["changes"]["new"]), 1916 "\n\t- " + "\n\t- ".join(ips_to_add), 1917 "\n\t- " + "\n\t- ".join(ips_not_added), 1918 ) 1919 ) 1920 else: 1921 ret["comment"] = "added ips: {}".format( 1922 "\n\t- " + "\n\t- ".join(ips_to_add) 1923 ) 1924 1925 # Verify there were changes 1926 if ret["changes"]["old"] == ret["changes"]["new"]: 1927 ret["changes"] = {} 1928 1929 else: 1930 # Testing mode, show that there were ips to add 1931 ret["comment"] = "ips on eni: {}\nips that would be added: {}\n".format( 1932 "\n\t- " + "\n\t- ".join(ret["changes"]["old"]), 1933 "\n\t- " + "\n\t- ".join(ips_to_add), 1934 ) 1935 ret["changes"] = {} 1936 ret["result"] = None 1937 1938 else: 1939 ret["comment"] = "ips on eni: {}".format( 1940 "\n\t- " + "\n\t- ".join(ret["changes"]["old"]) 1941 ) 1942 1943 # there were no changes since we did not attempt to remove ips 1944 ret["changes"] = {} 1945 1946 return ret 1947 1948 1949def private_ips_absent( 1950 name, 1951 network_interface_name=None, 1952 network_interface_id=None, 1953 private_ip_addresses=None, 1954 region=None, 1955 key=None, 1956 keyid=None, 1957 profile=None, 1958): 1959 1960 """ 1961 Ensure an ENI does not have secondary private ip addresses associated with it 1962 1963 name 1964 (String) - State definition name 1965 network_interface_id 1966 (String) - The EC2 network interface id, example eni-123456789 1967 private_ip_addresses 1968 (List or String) - The secondary private ip address(es) that should be absent on the ENI. 1969 region 1970 (string) - Region to connect to. 1971 key 1972 (string) - Secret key to be used. 1973 keyid 1974 (string) - Access key to be used. 1975 profile 1976 (variable) - A dict with region, key and keyid, or a pillar key (string) that contains a 1977 dict with region, key and keyid. 1978 """ 1979 1980 if not salt.utils.data.exactly_one((network_interface_name, network_interface_id)): 1981 raise SaltInvocationError( 1982 "Exactly one of 'network_interface_name', " 1983 "'network_interface_id' must be provided" 1984 ) 1985 1986 if not private_ip_addresses: 1987 raise SaltInvocationError( 1988 "You must provide the private_ip_addresses to unassociate with the ENI" 1989 ) 1990 if not isinstance(private_ip_addresses, list): 1991 private_ip_addresses = [private_ip_addresses] 1992 1993 ret = { 1994 "name": name, 1995 "result": True, 1996 "comment": "", 1997 "changes": {"new": [], "old": []}, 1998 } 1999 2000 get_eni_args = { 2001 "name": network_interface_name, 2002 "network_interface_id": network_interface_id, 2003 "region": region, 2004 "key": key, 2005 "keyid": keyid, 2006 "profile": profile, 2007 } 2008 2009 eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args) 2010 2011 # Check if there are any old private ips to remove from the eni 2012 primary_private_ip = None 2013 if eni and eni.get("result", {}).get("private_ip_addresses"): 2014 for eni_pip in eni["result"]["private_ip_addresses"]: 2015 ret["changes"]["old"].append(eni_pip["private_ip_address"]) 2016 if eni_pip["primary"]: 2017 primary_private_ip = eni_pip["private_ip_address"] 2018 2019 ips_to_remove = [] 2020 for private_ip in private_ip_addresses: 2021 if private_ip in ret["changes"]["old"]: 2022 ips_to_remove.append(private_ip) 2023 if private_ip == primary_private_ip: 2024 ret["result"] = False 2025 ret["comment"] = ( 2026 "You cannot unassign the primary private ip address ({}) on an " 2027 "eni\n" 2028 "ips on eni: {}\n" 2029 "attempted to remove: {}\n".format( 2030 primary_private_ip, 2031 "\n\t- " + "\n\t- ".join(ret["changes"]["old"]), 2032 "\n\t- " + "\n\t- ".join(private_ip_addresses), 2033 ) 2034 ) 2035 ret["changes"] = {} 2036 return ret 2037 2038 if ips_to_remove: 2039 if not __opts__["test"]: 2040 # Unassign secondary private ips to ENI 2041 assign_ips_args = { 2042 "network_interface_id": network_interface_id, 2043 "private_ip_addresses": ips_to_remove, 2044 "region": region, 2045 "key": key, 2046 "keyid": keyid, 2047 "profile": profile, 2048 } 2049 2050 __salt__["boto_ec2.unassign_private_ip_addresses"](**assign_ips_args) 2051 2052 # Verify secondary private ips were properly unassigned from ENI 2053 eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args) 2054 if eni and eni.get("result", {}).get("private_ip_addresses", None): 2055 for eni_pip in eni["result"]["private_ip_addresses"]: 2056 ret["changes"]["new"].append(eni_pip["private_ip_address"]) 2057 ips_not_removed = [] 2058 for private_ip in private_ip_addresses: 2059 if private_ip in ret["changes"]["new"]: 2060 ips_not_removed.append(private_ip) 2061 2062 if ips_not_removed: 2063 ret["result"] = False 2064 ret["comment"] = ( 2065 "ips on eni: {}\n" 2066 "attempted to remove: {}\n" 2067 "could not remove the following ips: {}\n".format( 2068 "\n\t- " + "\n\t- ".join(ret["changes"]["new"]), 2069 "\n\t- " + "\n\t- ".join(ips_to_remove), 2070 "\n\t- " + "\n\t- ".join(ips_not_removed), 2071 ) 2072 ) 2073 else: 2074 ret["comment"] = "removed ips: {}".format( 2075 "\n\t- " + "\n\t- ".join(ips_to_remove) 2076 ) 2077 2078 # Verify there were changes 2079 if ret["changes"]["old"] == ret["changes"]["new"]: 2080 ret["changes"] = {} 2081 2082 else: 2083 # Testing mode, show that there were ips to remove 2084 ret["comment"] = "ips on eni: {}\nips that would be removed: {}\n".format( 2085 "\n\t- " + "\n\t- ".join(ret["changes"]["old"]), 2086 "\n\t- " + "\n\t- ".join(ips_to_remove), 2087 ) 2088 ret["changes"] = {} 2089 ret["result"] = None 2090 2091 else: 2092 ret["comment"] = "ips on network interface: {}".format( 2093 "\n\t- " + "\n\t- ".join(ret["changes"]["old"]) 2094 ) 2095 2096 # there were no changes since we did not attempt to remove ips 2097 ret["changes"] = {} 2098 2099 return ret 2100