1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3
4import re
5
6from datetime import datetime
7
8from c7n.utils import local_session, type_schema
9
10from c7n_gcp.actions import MethodAction
11from c7n_gcp.filters import IamPolicyFilter
12from c7n_gcp.filters.iampolicy import IamPolicyValueFilter
13from c7n_gcp.provider import resources
14from c7n_gcp.query import QueryResourceManager, TypeInfo, ChildResourceManager, ChildTypeInfo
15
16from c7n.filters.core import ValueFilter
17from c7n.filters.offhours import OffHour, OnHour
18
19
20@resources.register('instance')
21class Instance(QueryResourceManager):
22
23 class resource_type(TypeInfo):
24 service = 'compute'
25 version = 'v1'
26 component = 'instances'
27 enum_spec = ('aggregatedList', 'items.*.instances[]', None)
28 scope = 'project'
29 name = id = 'name'
30 labels = True
31 default_report_fields = ['name', 'status', 'creationTimestamp', 'machineType', 'zone']
32 asset_type = "compute.googleapis.com/Instance"
33 scc_type = "google.compute.Instance"
34 metric_key = 'metric.labels.instance_name'
35 urn_component = "instance"
36 urn_zonal = True
37
38 @staticmethod
39 def get(client, resource_info):
40 # The api docs for compute instance get are wrong,
41 # they spell instance as resourceId
42 return client.execute_command(
43 'get', {'project': resource_info['project_id'],
44 'zone': resource_info['zone'],
45 'instance': resource_info[
46 'resourceName'].rsplit('/', 1)[-1]})
47
48 @staticmethod
49 def get_label_params(resource, all_labels):
50 project, zone, instance = re.match(
51 '.*?/projects/(.*?)/zones/(.*?)/instances/(.*)',
52 resource['selfLink']).groups()
53 return {'project': project, 'zone': zone, 'instance': instance,
54 'body': {
55 'labels': all_labels,
56 'labelFingerprint': resource['labelFingerprint']
57 }}
58
59 @classmethod
60 def refresh(cls, client, resource):
61 project, zone, name = re.match(
62 '.*?/projects/(.*?)/zones/(.*?)/instances/(.*)',
63 resource['selfLink']).groups()
64 return cls.get(
65 client,
66 {
67 'project_id': project,
68 'zone': zone,
69 'resourceName': name
70 }
71 )
72
73
74Instance.filter_registry.register('offhour', OffHour)
75Instance.filter_registry.register('onhour', OnHour)
76
77
78@Instance.filter_registry.register('effective-firewall')
79class EffectiveFirewall(ValueFilter):
80 """Filters instances by their effective firewall rules.
81 See `getEffectiveFirewalls
82 <https://cloud.google.com/compute/docs/reference/rest/v1/instances/getEffectiveFirewalls>`_
83 for valid fields.
84
85 :example:
86
87 Filter all instances that have a firewall rule that allows public
88 acess
89
90 .. code-block:: yaml
91
92 policies:
93 - name: find-publicly-accessable-instances
94 resource: gcp.instance
95 filters:
96 - type: effective-firewall
97 key: firewalls[*].sourceRanges[]
98 op: contains
99 value: "0.0.0.0/0"
100 """
101
102 schema = type_schema('effective-firewall', rinherit=ValueFilter.schema)
103 permissions = ('compute.instances.getEffectiveFirewalls',)
104
105 def get_resource_params(self, resource):
106 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/instances/.*')
107 project, zone = path_param_re.match(resource['selfLink']).groups()
108 return {'project': project, 'zone': zone, 'instance': resource["name"]}
109
110 def process_resource(self, client, resource):
111 params = self.get_resource_params(resource)
112 effective_firewalls = []
113 for interface in resource["networkInterfaces"]:
114 effective_firewalls.append(client.execute_command(
115 'getEffectiveFirewalls', {"networkInterface": interface["name"], **params}))
116 return super(EffectiveFirewall, self).process(effective_firewalls, None)
117
118 def get_client(self, session, model):
119 return session.client(
120 model.service, model.version, model.component)
121
122 def process(self, resources, event=None):
123 model = self.manager.get_model()
124 session = local_session(self.manager.session_factory)
125 client = self.get_client(session, model)
126 return [r for r in resources if self.process_resource(client, r)]
127
128
129class InstanceAction(MethodAction):
130
131 def get_resource_params(self, model, resource):
132 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/instances/(.*)')
133 project, zone, instance = path_param_re.match(resource['selfLink']).groups()
134 return {'project': project, 'zone': zone, 'instance': instance}
135
136
137@Instance.action_registry.register('start')
138class Start(InstanceAction):
139
140 schema = type_schema('start')
141 method_spec = {'op': 'start'}
142 attr_filter = ('status', ('TERMINATED',))
143
144
145@Instance.action_registry.register('stop')
146class Stop(InstanceAction):
147 """
148 Caution: `stop` in GCP is closer to terminate in terms of effect.
149
150 `suspend` is closer to stop in other providers.
151
152 See https://cloud.google.com/compute/docs/instances/instance-life-cycle
153 """
154
155 schema = type_schema('stop')
156 method_spec = {'op': 'stop'}
157 attr_filter = ('status', ('RUNNING',))
158
159
160@Instance.action_registry.register('suspend')
161class Suspend(InstanceAction):
162
163 schema = type_schema('suspend')
164 method_spec = {'op': 'suspend'}
165 attr_filter = ('status', ('RUNNING',))
166
167
168@Instance.action_registry.register('resume')
169class Resume(InstanceAction):
170
171 schema = type_schema('resume')
172 method_spec = {'op': 'resume'}
173 attr_filter = ('status', ('SUSPENDED',))
174
175
176@Instance.action_registry.register('delete')
177class Delete(InstanceAction):
178
179 schema = type_schema('delete')
180 method_spec = {'op': 'delete'}
181
182
183@Instance.action_registry.register('detach-disks')
184class DetachDisks(MethodAction):
185 """
186 `Detaches <https://cloud.google.com/compute/docs/reference/rest/v1/instances/detachDisk>`_
187 all disks from instance. The action does not specify any parameters.
188
189 It may be useful to be used before deleting instances to not delete disks
190 that are set to auto delete.
191
192 :Example:
193
194 .. code-block:: yaml
195
196 policies:
197 - name: gcp-instance-detach-disks
198 resource: gcp.instance
199 filters:
200 - type: value
201 key: name
202 value: instance-template-to-detahc
203 actions:
204 - type: detach-disks
205 """
206 schema = type_schema('detach-disks')
207 attr_filter = ('status', ('TERMINATED',))
208 method_spec = {'op': 'detachDisk'}
209 path_param_re = re.compile(
210 '.*?/projects/(.*?)/zones/(.*?)/instances/(.*)')
211
212 def validate(self):
213 pass
214
215 def process_resource_set(self, client, model, resources):
216 for resource in resources:
217 self.process_resource(client, resource)
218
219 def process_resource(self, client, resource):
220 op_name = 'detachDisk'
221
222 project, zone, instance = self.path_param_re.match(
223 resource['selfLink']).groups()
224
225 base_params = {'project': project, 'zone': zone, 'instance': instance}
226 for disk in resource.get('disks', []):
227 params = dict(base_params, deviceName=disk['deviceName'])
228 self.invoke_api(client, op_name, params)
229
230
231@Instance.action_registry.register('create-machine-image')
232class CreateMachineImage(MethodAction):
233 """
234 `Creates <https://cloud.google.com/compute/docs/reference/rest/beta/machineImages/insert>`_
235 Machine Image from instance.
236
237 The `name_format` specifies name of image in python `format string <https://pyformat.info/>`
238
239 Inside format string there are defined variables:
240 - `now`: current time
241 - `instance`: whole instance resource
242
243 Default name format is `{instance[name]}`
244
245 :Example:
246
247 .. code-block:: yaml
248
249 policies:
250 - name: gcp-create-machine-image
251 resource: gcp.instance
252 filters:
253 - type: value
254 key: name
255 value: instance-create-to-make-image
256 actions:
257 - type: create-machine-image
258 name_format: "{instance[name]:.50}-{now:%Y-%m-%d}"
259
260 """
261 schema = type_schema('create-machine-image', name_format={'type': 'string'})
262 method_spec = {'op': 'insert'}
263 permissions = ('compute.machineImages.create',)
264
265 def get_resource_params(self, model, resource):
266 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/instances/(.*)')
267 project, _, _ = path_param_re.match(resource['selfLink']).groups()
268 name_format = self.data.get('name_format', '{instance[name]}')
269 name = name_format.format(instance=resource, now=datetime.now())
270
271 return {'project': project, 'sourceInstance': resource['selfLink'], 'body': {'name': name}}
272
273 def get_client(self, session, model):
274 return session.client(model.service, "beta", "machineImages")
275
276
277@resources.register('image')
278class Image(QueryResourceManager):
279
280 class resource_type(TypeInfo):
281 service = 'compute'
282 version = 'v1'
283 component = 'images'
284 name = id = 'name'
285 default_report_fields = [
286 "name", "description", "sourceType", "status", "creationTimestamp",
287 "diskSizeGb", "family"]
288 asset_type = "compute.googleapis.com/Image"
289 urn_component = "image"
290 labels = True
291
292 @staticmethod
293 def get(client, resource_info):
294 return client.execute_command(
295 'get', {'project': resource_info['project_id'],
296 'image': resource_info['image_id']})
297
298 @staticmethod
299 def get_label_params(resource, all_labels):
300 project, resource_id = re.match(
301 '.*?/projects/(.*?)/global/images/(.*)',
302 resource['selfLink']).groups()
303 return {'project': project, 'resource': resource_id,
304 'body': {
305 'labels': all_labels,
306 'labelFingerprint': resource['labelFingerprint']
307 }}
308
309 @classmethod
310 def refresh(cls, client, resource):
311 project, resource_id = re.match(
312 '.*?/projects/(.*?)/global/images/(.*)',
313 resource['selfLink']).groups()
314 return cls.get(
315 client,
316 {
317 'project_id': project,
318 'image_id': resource_id
319 }
320 )
321
322
323@Image.filter_registry.register('iam-policy')
324class ImageIamPolicyFilter(IamPolicyFilter):
325 """
326 Overrides the base implementation to process images resources correctly.
327 """
328 permissions = ('compute.images.getIamPolicy',)
329
330 def _verb_arguments(self, resource):
331 project, _ = re.match(
332 '.*?/projects/(.*?)/global/images/(.*)',
333 resource['selfLink']).groups()
334 verb_arguments = {'resource': resource[self.manager.resource_type.id], 'project': project}
335 return verb_arguments
336
337 def process_resources(self, resources):
338 value_filter = IamPolicyValueFilter(self.data['doc'], self.manager)
339 value_filter._verb_arguments = self._verb_arguments
340 return value_filter.process(resources)
341
342
343@Image.action_registry.register('delete')
344class DeleteImage(MethodAction):
345
346 schema = type_schema('delete')
347 method_spec = {'op': 'delete'}
348 attr_filter = ('status', ('READY'))
349 path_param_re = re.compile('.*?/projects/(.*?)/global/images/(.*)')
350
351 def get_resource_params(self, m, r):
352 project, image_id = self.path_param_re.match(r['selfLink']).groups()
353 return {'project': project, 'image': image_id}
354
355
356@resources.register('disk')
357class Disk(QueryResourceManager):
358
359 class resource_type(TypeInfo):
360 service = 'compute'
361 version = 'v1'
362 component = 'disks'
363 scope = 'zone'
364 enum_spec = ('aggregatedList', 'items.*.disks[]', None)
365 name = id = 'name'
366 labels = True
367 default_report_fields = ["name", "sizeGb", "status", "zone"]
368 asset_type = "compute.googleapis.com/Disk"
369 urn_component = "disk"
370 urn_zonal = True
371
372 @staticmethod
373 def get(client, resource_info):
374 return client.execute_command(
375 'get', {'project': resource_info['project_id'],
376 'zone': resource_info['zone'],
377 'disk': resource_info['disk_id']})
378
379 @staticmethod
380 def get_label_params(resource, all_labels):
381 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/disks/(.*)')
382 project, zone, instance = path_param_re.match(
383 resource['selfLink']).groups()
384 return {'project': project, 'zone': zone, 'resource': instance,
385 'body': {
386 'labels': all_labels,
387 'labelFingerprint': resource['labelFingerprint']
388 }}
389
390
391@Disk.action_registry.register('snapshot')
392class DiskSnapshot(MethodAction):
393 """
394 `Snapshots <https://cloud.google.com/compute/docs/reference/rest/v1/disks/createSnapshot>`_
395 disk.
396
397 The `name_format` specifies name of snapshot in python `format string <https://pyformat.info/>`
398
399 Inside format string there are defined variables:
400 - `now`: current time
401 - `disk`: whole disk resource
402
403 Default name format is `{disk.name}`
404
405 :Example:
406
407 .. code-block:: yaml
408
409 policies:
410 - name: gcp-disk-snapshot
411 resource: gcp.disk
412 filters:
413 - type: value
414 key: name
415 value: disk-7
416 actions:
417 - type: snapshot
418 name_format: "{disk[name]:.50}-{now:%Y-%m-%d}"
419 """
420 schema = type_schema('snapshot', name_format={'type': 'string'})
421 method_spec = {'op': 'createSnapshot'}
422 path_param_re = re.compile(
423 '.*?/projects/(.*?)/zones/(.*?)/disks/(.*)')
424 attr_filter = ('status', ('RUNNING', 'READY'))
425
426 def get_resource_params(self, model, resource):
427 project, zone, resourceId = self.path_param_re.match(resource['selfLink']).groups()
428 name_format = self.data.get('name_format', '{disk[name]}')
429 name = name_format.format(disk=resource, now=datetime.now())
430
431 return {
432 'project': project,
433 'zone': zone,
434 'disk': resourceId,
435 'body': {
436 'name': name,
437 'labels': resource.get('labels', {}),
438 }
439 }
440
441
442@Disk.action_registry.register('delete')
443class DiskDelete(MethodAction):
444
445 schema = type_schema('delete')
446 method_spec = {'op': 'delete'}
447 path_param_re = re.compile(
448 '.*?/projects/(.*?)/zones/(.*?)/disks/(.*)')
449 attr_filter = ('status', ('RUNNING', 'READY'))
450
451 def get_resource_params(self, m, r):
452 project, zone, resourceId = self.path_param_re.match(r['selfLink']).groups()
453 return {
454 'project': project,
455 'zone': zone,
456 'disk': resourceId,
457 }
458
459
460@resources.register('snapshot')
461class Snapshot(QueryResourceManager):
462
463 class resource_type(TypeInfo):
464 service = 'compute'
465 version = 'v1'
466 component = 'snapshots'
467 enum_spec = ('list', 'items[]', None)
468 name = id = 'name'
469 default_report_fields = ["name", "status", "diskSizeGb", "creationTimestamp"]
470 asset_type = "compute.googleapis.com/Snapshot"
471 urn_component = "snapshot"
472
473 @staticmethod
474 def get(client, resource_info):
475 return client.execute_command(
476 'get', {'project': resource_info['project_id'],
477 'snapshot': resource_info['snapshot_id']})
478
479
480@Snapshot.action_registry.register('delete')
481class DeleteSnapshot(MethodAction):
482
483 schema = type_schema('delete')
484 method_spec = {'op': 'delete'}
485 attr_filter = ('status', ('READY', 'UPLOADING'))
486 path_param_re = re.compile('.*?/projects/(.*?)/global/snapshots/(.*)')
487
488 def get_resource_params(self, m, r):
489 project, snapshot_id = self.path_param_re.match(r['selfLink']).groups()
490 # Docs are wrong :-(
491 # https://cloud.google.com/compute/docs/reference/rest/v1/snapshots/delete
492 return {'project': project, 'snapshot': snapshot_id}
493
494
495@resources.register('instance-template')
496class InstanceTemplate(QueryResourceManager):
497 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates"""
498 class resource_type(TypeInfo):
499 service = 'compute'
500 version = 'v1'
501 component = 'instanceTemplates'
502 scope = 'zone'
503 enum_spec = ('list', 'items[]', None)
504 name = id = 'name'
505 default_report_fields = [
506 name, "description", "creationTimestamp",
507 "properties.machineType", "properties.description"]
508 asset_type = "compute.googleapis.com/InstanceTemplate"
509 urn_component = "instance-template"
510
511 @staticmethod
512 def get(client, resource_info):
513 return client.execute_command(
514 'get', {'project': resource_info['project_id'],
515 'instanceTemplate': resource_info['instance_template_name']})
516
517
518@InstanceTemplate.action_registry.register('delete')
519class InstanceTemplateDelete(MethodAction):
520 """
521 `Deletes <https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/delete>`_
522 an Instance Template. The action does not specify any parameters.
523
524 :Example:
525
526 .. code-block:: yaml
527
528 policies:
529 - name: gcp-instance-template-delete
530 resource: gcp.instance-template
531 filters:
532 - type: value
533 key: name
534 value: instance-template-to-delete
535 actions:
536 - type: delete
537 """
538 schema = type_schema('delete')
539 method_spec = {'op': 'delete'}
540
541 def get_resource_params(self, m, r):
542 project, instance_template = re.match('.*/projects/(.*?)/.*/instanceTemplates/(.*)',
543 r['selfLink']).groups()
544 return {'project': project,
545 'instanceTemplate': instance_template}
546
547
548@resources.register('autoscaler')
549class Autoscaler(QueryResourceManager):
550 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/autoscalers"""
551 class resource_type(TypeInfo):
552 service = 'compute'
553 version = 'v1'
554 component = 'autoscalers'
555 name = id = 'name'
556 enum_spec = ('aggregatedList', 'items.*.autoscalers[]', None)
557 default_report_fields = [
558 "name", "description", "status", "target", "recommendedSize"]
559 asset_type = "compute.googleapis.com/Autoscaler"
560 metric_key = "resource.labels.autoscaler_name"
561 urn_component = "autoscaler"
562 urn_zonal = True
563
564 @staticmethod
565 def get(client, resource_info):
566 project, zone, autoscaler = re.match(
567 'projects/(.*?)/zones/(.*?)/autoscalers/(.*)',
568 resource_info['resourceName']).groups()
569
570 return client.execute_command(
571 'get', {'project': project,
572 'zone': zone,
573 'autoscaler': autoscaler})
574
575
576@Autoscaler.action_registry.register('set')
577class AutoscalerSet(MethodAction):
578 """
579 `Patches <https://cloud.google.com/compute/docs/reference/rest/v1/autoscalers/patch>`_
580 configuration parameters for the autoscaling algorithm.
581
582 The `coolDownPeriodSec` specifies the number of seconds that the autoscaler
583 should wait before it starts collecting information from a new instance.
584
585 The `cpuUtilization.utilizationTarget` specifies the target CPU utilization that the
586 autoscaler should maintain.
587
588 The `loadBalancingUtilization.utilizationTarget` specifies fraction of backend capacity
589 utilization (set in HTTP(S) load balancing configuration) that autoscaler should maintain.
590
591 The `minNumReplicas` specifies the minimum number of replicas that the autoscaler can
592 scale down to.
593
594 The `maxNumReplicas` specifies the maximum number of instances that the autoscaler can
595 scale up to.
596
597 :Example:
598
599 .. code-block:: yaml
600
601 policies:
602 - name: gcp-autoscaler-set
603 resource: gcp.autoscaler
604 filters:
605 - type: value
606 key: name
607 value: instance-group-2
608 actions:
609 - type: set
610 coolDownPeriodSec: 20
611 cpuUtilization:
612 utilizationTarget: 0.7
613 loadBalancingUtilization:
614 utilizationTarget: 0.7
615 minNumReplicas: 1
616 maxNumReplicas: 4
617 """
618 schema = type_schema('set',
619 **{
620 'coolDownPeriodSec': {
621 'type': 'integer',
622 'minimum': 15
623 },
624 'cpuUtilization': {
625 'type': 'object',
626 'required': ['utilizationTarget'],
627 'properties': {
628 'utilizationTarget': {
629 'type': 'number',
630 'exclusiveMinimum': 0,
631 'maximum': 1
632 }
633 },
634 },
635 'loadBalancingUtilization': {
636 'type': 'object',
637 'required': ['utilizationTarget'],
638 'properties': {
639 'utilizationTarget': {
640 'type': 'number',
641 'exclusiveMinimum': 0,
642 'maximum': 1
643 }
644 }
645 },
646 'maxNumReplicas': {
647 'type': 'integer',
648 'exclusiveMinimum': 0
649 },
650 'minNumReplicas': {
651 'type': 'integer',
652 'exclusiveMinimum': 0
653 }
654 })
655 method_spec = {'op': 'patch'}
656 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/autoscalers/(.*)')
657 method_perm = 'update'
658
659 def get_resource_params(self, model, resource):
660 project, zone, autoscaler = self.path_param_re.match(resource['selfLink']).groups()
661 body = {}
662
663 if 'coolDownPeriodSec' in self.data:
664 body['coolDownPeriodSec'] = self.data['coolDownPeriodSec']
665
666 if 'cpuUtilization' in self.data:
667 body['cpuUtilization'] = self.data['cpuUtilization']
668
669 if 'loadBalancingUtilization' in self.data:
670 body['loadBalancingUtilization'] = self.data['loadBalancingUtilization']
671
672 if 'maxNumReplicas' in self.data:
673 body['maxNumReplicas'] = self.data['maxNumReplicas']
674
675 if 'minNumReplicas' in self.data:
676 body['minNumReplicas'] = self.data['minNumReplicas']
677
678 result = {'project': project,
679 'zone': zone,
680 'autoscaler': autoscaler,
681 'body': {
682 'autoscalingPolicy': body
683 }}
684
685 return result
686
687
688@resources.register('zone')
689class Zone(QueryResourceManager):
690 """GC resource: https://cloud.google.com/compute/docs/reference/rest/v1/zones"""
691 class resource_type(TypeInfo):
692 service = 'compute'
693 version = 'v1'
694 component = 'zones'
695 enum_spec = ('list', 'items[]', None)
696 scope = 'project'
697 name = id = 'name'
698 default_report_fields = ['id', 'name', 'dnsName', 'creationTime', 'visibility']
699 asset_type = "compute.googleapis.com/compute"
700 scc_type = "google.cloud.dns.ManagedZone"
701
702
703@resources.register('compute-project')
704class Project(QueryResourceManager):
705 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/projects"""
706 class resource_type(TypeInfo):
707 service = 'compute'
708 version = 'v1'
709 component = 'projects'
710 enum_spec = ('get', '[@]', None)
711 name = id = 'name'
712 default_report_fields = ["name"]
713 asset_type = 'compute.googleapis.com/Project'
714
715 @staticmethod
716 def get(client, resource_info):
717 return client.execute_command(
718 'get', {'project': resource_info['project_id']})
719
720
721@resources.register('instance-group-manager')
722class InstanceGroupManager(ChildResourceManager):
723
724 class resource_type(ChildTypeInfo):
725 service = 'compute'
726 version = 'v1'
727 component = 'instanceGroupManagers'
728 enum_spec = ('list', 'items[]', None)
729 name = id = 'name'
730 parent_spec = {
731 'resource': 'zone',
732 'child_enum_params': {
733 ('name', 'zone')},
734 'use_child_query': False,
735 }
736 default_report_fields = ['id', 'name', 'dnsName', 'creationTime', 'visibility']