1"""
2Manage Elasticache
3==================
4
5.. versionadded:: 2014.7.0
6
7Create, destroy and update Elasticache clusters. Be aware that this interacts
8with Amazon's services, and so may incur charges.
9
10Note: This module currently only supports creation and deletion of
11elasticache resources and will not modify clusters when their configuration
12changes in your state files.
13
14This module uses ``boto``, which can be installed via package, or pip.
15
16This module accepts explicit elasticache credentials but can also utilize
17IAM roles assigned to the instance through Instance Profiles. Dynamic
18credentials are then automatically obtained from AWS API and no further
19configuration is necessary. More information available `here
20<http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_.
21
22If IAM roles are not used you need to specify them either in a pillar file or
23in the minion's config file:
24
25.. code-block:: yaml
26
27    elasticache.keyid: GKTADJGHEIQSXMKKRBJ08H
28    elasticache.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
29
30It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile, either
31passed in as a dict, or as a string to pull from pillars or minion config:
32
33.. code-block:: yaml
34
35    myprofile:
36      keyid: GKTADJGHEIQSXMKKRBJ08H
37      key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
38        region: us-east-1
39
40.. code-block:: yaml
41
42    Ensure myelasticache exists:
43      boto_elasticache.present:
44        - name: myelasticache
45        - engine: redis
46        - cache_node_type: cache.t1.micro
47        - num_cache_nodes: 1
48        - notification_topic_arn: arn:aws:sns:us-east-1:879879:my-sns-topic
49        - region: us-east-1
50        - keyid: GKTADJGHEIQSXMKKRBJ08H
51        - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
52
53    # Using a profile from pillars
54    Ensure myelasticache exists:
55      boto_elasticache.present:
56        - name: myelasticache
57        - engine: redis
58        - cache_node_type: cache.t1.micro
59        - num_cache_nodes: 1
60        - notification_topic_arn: arn:aws:sns:us-east-1:879879:my-sns-topic
61        - region: us-east-1
62        - profile: myprofile
63
64    # Passing in a profile
65    Ensure myelasticache exists:
66      boto_elasticache.present:
67        - name: myelasticache
68        - engine: redis
69        - cache_node_type: cache.t1.micro
70        - num_cache_nodes: 1
71        - notification_topic_arn: arn:aws:sns:us-east-1:879879:my-sns-topic
72        - region: us-east-1
73        - profile:
74            keyid: GKTADJGHEIQSXMKKRBJ08H
75            key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
76"""
77
78
79import logging
80
81log = logging.getLogger(__name__)
82
83
84def __virtual__():
85    """
86    Only load if boto is available.
87    """
88    if "boto_elasticache.exists" in __salt__:
89        return "boto_elasticache"
90    return (False, "boto_elasticache module could not be loaded")
91
92
93def cache_cluster_present(*args, **kwargs):
94    return present(*args, **kwargs)
95
96
97def present(
98    name,
99    engine=None,
100    cache_node_type=None,
101    num_cache_nodes=None,
102    preferred_availability_zone=None,
103    port=None,
104    cache_parameter_group_name=None,
105    cache_security_group_names=None,
106    replication_group_id=None,
107    auto_minor_version_upgrade=True,
108    security_group_ids=None,
109    cache_subnet_group_name=None,
110    engine_version=None,
111    notification_topic_arn=None,
112    preferred_maintenance_window=None,
113    wait=None,
114    region=None,
115    key=None,
116    keyid=None,
117    profile=None,
118):
119    """
120    Ensure the cache cluster exists.
121
122    name
123        Name of the cache cluster (cache cluster id).
124
125    engine
126        The name of the cache engine to be used for this cache cluster. Valid
127        values are memcached or redis.
128
129    cache_node_type
130        The compute and memory capacity of the nodes in the cache cluster.
131        cache.t1.micro, cache.m1.small, etc. See: https://boto.readthedocs.io/en/latest/ref/elasticache.html#boto.elasticache.layer1.ElastiCacheConnection.create_cache_cluster
132
133    num_cache_nodes
134        The number of cache nodes that the cache cluster will have.
135
136    preferred_availability_zone
137        The EC2 Availability Zone in which the cache cluster will be created.
138        All cache nodes belonging to a cache cluster are placed in the
139        preferred availability zone.
140
141    port
142        The port number on which each of the cache nodes will accept
143        connections.
144
145    cache_parameter_group_name
146        The name of the cache parameter group to associate with this cache
147        cluster. If this argument is omitted, the default cache parameter group
148        for the specified engine will be used.
149
150    cache_security_group_names
151        A list of cache security group names to associate with this cache
152        cluster. Use this parameter only when you are creating a cluster
153        outside of a VPC.
154
155    replication_group_id
156        The replication group to which this cache cluster should belong. If
157        this parameter is specified, the cache cluster will be added to the
158        specified replication group as a read replica; otherwise, the cache
159        cluster will be a standalone primary that is not part of any
160        replication group.
161
162    auto_minor_version_upgrade
163        Determines whether minor engine upgrades will be applied automatically
164        to the cache cluster during the maintenance window. A value of True
165        allows these upgrades to occur; False disables automatic upgrades.
166
167    security_group_ids
168        One or more VPC security groups associated with the cache cluster. Use
169        this parameter only when you are creating a cluster in a VPC.
170
171    cache_subnet_group_name
172        The name of the cache subnet group to be used for the cache cluster.
173        Use this parameter only when you are creating a cluster in a VPC.
174
175    engine_version
176        The version number of the cache engine to be used for this cluster.
177
178    notification_topic_arn
179        The Amazon Resource Name (ARN) of the Amazon Simple Notification
180        Service (SNS) topic to which notifications will be sent. The Amazon SNS
181        topic owner must be the same as the cache cluster owner.
182
183    preferred_maintenance_window
184        The weekly time range (in UTC) during which system maintenance can
185        occur. Example: sun:05:00-sun:09:00
186
187    wait
188        Boolean. Wait for confirmation from boto that the cluster is in the
189        available state.
190
191    region
192        Region to connect to.
193
194    key
195        Secret key to be used.
196
197    keyid
198        Access key to be used.
199
200    profile
201        A dict with region, key and keyid, or a pillar key (string)
202        that contains a dict with region, key and keyid.
203    """
204    ret = {"name": name, "result": True, "comment": "", "changes": {}}
205    if cache_security_group_names and cache_subnet_group_name:
206        _subnet_group = __salt__["boto_elasticache.get_cache_subnet_group"](
207            cache_subnet_group_name, region, key, keyid, profile
208        )
209        vpc_id = _subnet_group["vpc_id"]
210        if not security_group_ids:
211            security_group_ids = []
212        _security_group_ids = __salt__["boto_secgroup.convert_to_group_ids"](
213            groups=cache_security_group_names,
214            vpc_id=vpc_id,
215            region=region,
216            key=key,
217            keyid=keyid,
218            profile=profile,
219        )
220        security_group_ids.extend(_security_group_ids)
221        cache_security_group_names = None
222    config = __salt__["boto_elasticache.get_config"](name, region, key, keyid, profile)
223    if config is None:
224        msg = "Failed to retrieve cache cluster info from AWS."
225        ret["comment"] = msg
226        ret["result"] = None
227        return ret
228    elif not config:
229        if __opts__["test"]:
230            msg = "Cache cluster {} is set to be created.".format(name)
231            ret["comment"] = msg
232            ret["result"] = None
233            return ret
234        created = __salt__["boto_elasticache.create"](
235            name=name,
236            num_cache_nodes=num_cache_nodes,
237            cache_node_type=cache_node_type,
238            engine=engine,
239            replication_group_id=replication_group_id,
240            engine_version=engine_version,
241            cache_parameter_group_name=cache_parameter_group_name,
242            cache_subnet_group_name=cache_subnet_group_name,
243            cache_security_group_names=cache_security_group_names,
244            security_group_ids=security_group_ids,
245            preferred_availability_zone=preferred_availability_zone,
246            preferred_maintenance_window=preferred_maintenance_window,
247            port=port,
248            notification_topic_arn=notification_topic_arn,
249            auto_minor_version_upgrade=auto_minor_version_upgrade,
250            wait=wait,
251            region=region,
252            key=key,
253            keyid=keyid,
254            profile=profile,
255        )
256        if created:
257            ret["changes"]["old"] = None
258            config = __salt__["boto_elasticache.get_config"](
259                name, region, key, keyid, profile
260            )
261            ret["changes"]["new"] = config
262        else:
263            ret["result"] = False
264            ret["comment"] = "Failed to create {} cache cluster.".format(name)
265            return ret
266    # TODO: support modification of existing elasticache clusters
267    else:
268        ret["comment"] = "Cache cluster {} is present.".format(name)
269    return ret
270
271
272def subnet_group_present(
273    name,
274    subnet_ids=None,
275    subnet_names=None,
276    description=None,
277    tags=None,
278    region=None,
279    key=None,
280    keyid=None,
281    profile=None,
282):
283    """
284    Ensure ElastiCache subnet group exists.
285
286    .. versionadded:: 2015.8.0
287
288    name
289        The name for the ElastiCache subnet group. This value is stored as a lowercase string.
290
291    subnet_ids
292        A list of VPC subnet IDs for the cache subnet group.  Exclusive with subnet_names.
293
294    subnet_names
295        A list of VPC subnet names for the cache subnet group.  Exclusive with subnet_ids.
296
297    description
298        Subnet group description.
299
300    tags
301        A list of tags.
302
303    region
304        Region to connect to.
305
306    key
307        Secret key to be used.
308
309    keyid
310        Access key to be used.
311
312    profile
313        A dict with region, key and keyid, or a pillar key (string) that
314        contains a dict with region, key and keyid.
315    """
316    ret = {"name": name, "result": True, "comment": "", "changes": {}}
317
318    exists = __salt__["boto_elasticache.subnet_group_exists"](
319        name=name, tags=tags, region=region, key=key, keyid=keyid, profile=profile
320    )
321    if not exists:
322        if __opts__["test"]:
323            ret["comment"] = "Subnet group {} is set to be created.".format(name)
324            ret["result"] = None
325            return ret
326        created = __salt__["boto_elasticache.create_subnet_group"](
327            name=name,
328            subnet_ids=subnet_ids,
329            subnet_names=subnet_names,
330            description=description,
331            tags=tags,
332            region=region,
333            key=key,
334            keyid=keyid,
335            profile=profile,
336        )
337        if not created:
338            ret["result"] = False
339            ret["comment"] = "Failed to create {} subnet group.".format(name)
340            return ret
341        ret["changes"]["old"] = None
342        ret["changes"]["new"] = name
343        ret["comment"] = "Subnet group {} created.".format(name)
344        return ret
345    ret["comment"] = "Subnet group present."
346    return ret
347
348
349def cache_cluster_absent(*args, **kwargs):
350    return absent(*args, **kwargs)
351
352
353def absent(name, wait=True, region=None, key=None, keyid=None, profile=None):
354    """
355    Ensure the named elasticache cluster is deleted.
356
357    name
358        Name of the cache cluster.
359
360    wait
361        Boolean. Wait for confirmation from boto that the cluster is in the
362        deleting state.
363
364    region
365        Region to connect to.
366
367    key
368        Secret key to be used.
369
370    keyid
371        Access key to be used.
372
373    profile
374        A dict with region, key and keyid, or a pillar key (string)
375        that contains a dict with region, key and keyid.
376    """
377    ret = {"name": name, "result": True, "comment": "", "changes": {}}
378
379    is_present = __salt__["boto_elasticache.exists"](name, region, key, keyid, profile)
380
381    if is_present:
382        if __opts__["test"]:
383            ret["comment"] = "Cache cluster {} is set to be removed.".format(name)
384            ret["result"] = None
385            return ret
386        deleted = __salt__["boto_elasticache.delete"](
387            name, wait, region, key, keyid, profile
388        )
389        if deleted:
390            ret["changes"]["old"] = name
391            ret["changes"]["new"] = None
392        else:
393            ret["result"] = False
394            ret["comment"] = "Failed to delete {} cache cluster.".format(name)
395    else:
396        ret["comment"] = "{} does not exist in {}.".format(name, region)
397    return ret
398
399
400def replication_group_present(*args, **kwargs):
401    return creategroup(*args, **kwargs)
402
403
404def creategroup(
405    name,
406    primary_cluster_id,
407    replication_group_description,
408    wait=None,
409    region=None,
410    key=None,
411    keyid=None,
412    profile=None,
413):
414    """
415    Ensure the a replication group is create.
416
417    name
418        Name of replication group
419
420    wait
421        Waits for the group to be available
422
423    primary_cluster_id
424        Name of the master cache node
425
426    replication_group_description
427        Description for the group
428
429    region
430        Region to connect to.
431
432    key
433        Secret key to be used.
434
435    keyid
436        Access key to be used.
437
438    profile
439        A dict with region, key and keyid, or a pillar key (string)
440        that contains a dict with region, key and keyid.
441    """
442    ret = {"name": name, "result": None, "comment": "", "changes": {}}
443    is_present = __salt__["boto_elasticache.group_exists"](
444        name, region, key, keyid, profile
445    )
446    if not is_present:
447        if __opts__["test"]:
448            ret["comment"] = "Replication {} is set to be created.".format(name)
449            ret["result"] = None
450        created = __salt__["boto_elasticache.create_replication_group"](
451            name,
452            primary_cluster_id,
453            replication_group_description,
454            wait,
455            region,
456            key,
457            keyid,
458            profile,
459        )
460        if created:
461            config = __salt__["boto_elasticache.describe_replication_group"](
462                name, region, key, keyid, profile
463            )
464            ret["changes"]["old"] = None
465            ret["changes"]["new"] = config
466            ret["result"] = True
467        else:
468            ret["result"] = False
469            ret["comment"] = "Failed to create {} replication group.".format(name)
470    else:
471        ret["comment"] = "{} replication group exists .".format(name)
472        ret["result"] = True
473    return ret
474
475
476def subnet_group_absent(
477    name, tags=None, region=None, key=None, keyid=None, profile=None
478):
479    ret = {"name": name, "result": True, "comment": "", "changes": {}}
480
481    exists = __salt__["boto_elasticache.subnet_group_exists"](
482        name=name, tags=tags, region=region, key=key, keyid=keyid, profile=profile
483    )
484    if not exists:
485        ret["result"] = True
486        ret["comment"] = "{} ElastiCache subnet group does not exist.".format(name)
487        return ret
488
489    if __opts__["test"]:
490        ret["comment"] = "ElastiCache subnet group {} is set to be removed.".format(
491            name
492        )
493        ret["result"] = None
494        return ret
495    deleted = __salt__["boto_elasticache.delete_subnet_group"](
496        name, region, key, keyid, profile
497    )
498    if not deleted:
499        ret["result"] = False
500        ret["comment"] = "Failed to delete {} ElastiCache subnet group.".format(name)
501        return ret
502    ret["changes"]["old"] = name
503    ret["changes"]["new"] = None
504    ret["comment"] = "ElastiCache subnet group {} deleted.".format(name)
505    return ret
506
507
508def replication_group_absent(
509    name, tags=None, region=None, key=None, keyid=None, profile=None
510):
511    ret = {"name": name, "result": True, "comment": "", "changes": {}}
512
513    exists = __salt__["boto_elasticache.group_exists"](
514        name=name, region=region, key=key, keyid=keyid, profile=profile
515    )
516    if not exists:
517        ret["result"] = True
518        ret["comment"] = "{} ElastiCache replication group does not exist.".format(name)
519        log.info(ret["comment"])
520        return ret
521
522    if __opts__["test"]:
523        ret[
524            "comment"
525        ] = "ElastiCache replication group {} is set to be removed.".format(name)
526        ret["result"] = True
527        return ret
528    deleted = __salt__["boto_elasticache.delete_replication_group"](
529        name, region, key, keyid, profile
530    )
531    if not deleted:
532        ret["result"] = False
533        log.error(ret["comment"])
534        ret["comment"] = "Failed to delete {} ElastiCache replication group.".format(
535            name
536        )
537        return ret
538    ret["changes"]["old"] = name
539    ret["changes"]["new"] = None
540    ret["comment"] = "ElastiCache replication group {} deleted.".format(name)
541    log.info(ret["comment"])
542    return ret
543