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