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 """Caution: `stop` in GCP is closer to terminate in terms of effect.
148
149 The `discard_local_ssd` specifies if local SSD should be discarded
150 or not while stopping the instance. The default behavior from
151 Google Cloud console is to keep the local SSD. Default
152 `discard_local_ssd` is False.
153 https://cloud.google.com/compute/docs/instances/stop-start-instance#stop-vm-local-ssd
154
155 `suspend` is closer to stop in other providers.
156
157 See https://cloud.google.com/compute/docs/instances/instance-life-cycle
158
159 """
160
161 schema = type_schema('stop', discard_local_ssd={'type': 'boolean'})
162 method_spec = {'op': 'stop'}
163 attr_filter = ('status', ('RUNNING',))
164
165 def get_resource_params(self, model, resource):
166 params = super().get_resource_params(model, resource)
167
168 # support stopping instance with local SSD, it requires to pass an additional param to
169 # the stop request to discard local SSD (true/false)
170 discard_local_ssd = self.data.get('discard_local_ssd', False)
171 params['discardLocalSsd'] = discard_local_ssd
172
173 return params
174
175
176@Instance.action_registry.register('suspend')
177class Suspend(InstanceAction):
178
179 schema = type_schema('suspend')
180 method_spec = {'op': 'suspend'}
181 attr_filter = ('status', ('RUNNING',))
182
183
184@Instance.action_registry.register('resume')
185class Resume(InstanceAction):
186
187 schema = type_schema('resume')
188 method_spec = {'op': 'resume'}
189 attr_filter = ('status', ('SUSPENDED',))
190
191
192@Instance.action_registry.register('delete')
193class Delete(InstanceAction):
194
195 schema = type_schema('delete')
196 method_spec = {'op': 'delete'}
197
198
199@Instance.action_registry.register('detach-disks')
200class DetachDisks(MethodAction):
201 """
202 `Detaches <https://cloud.google.com/compute/docs/reference/rest/v1/instances/detachDisk>`_
203 all disks from instance. The action does not specify any parameters.
204
205 It may be useful to be used before deleting instances to not delete disks
206 that are set to auto delete.
207
208 :Example:
209
210 .. code-block:: yaml
211
212 policies:
213 - name: gcp-instance-detach-disks
214 resource: gcp.instance
215 filters:
216 - type: value
217 key: name
218 value: instance-template-to-detahc
219 actions:
220 - type: detach-disks
221 """
222 schema = type_schema('detach-disks')
223 attr_filter = ('status', ('TERMINATED',))
224 method_spec = {'op': 'detachDisk'}
225 path_param_re = re.compile(
226 '.*?/projects/(.*?)/zones/(.*?)/instances/(.*)')
227
228 def validate(self):
229 pass
230
231 def process_resource_set(self, client, model, resources):
232 for resource in resources:
233 self.process_resource(client, resource)
234
235 def process_resource(self, client, resource):
236 op_name = 'detachDisk'
237
238 project, zone, instance = self.path_param_re.match(
239 resource['selfLink']).groups()
240
241 base_params = {'project': project, 'zone': zone, 'instance': instance}
242 for disk in resource.get('disks', []):
243 params = dict(base_params, deviceName=disk['deviceName'])
244 self.invoke_api(client, op_name, params)
245
246
247@Instance.action_registry.register('create-machine-image')
248class CreateMachineImage(MethodAction):
249 """
250 `Creates <https://cloud.google.com/compute/docs/reference/rest/beta/machineImages/insert>`_
251 Machine Image from instance.
252
253 The `name_format` specifies name of image in python `format string <https://pyformat.info/>`
254
255 Inside format string there are defined variables:
256 - `now`: current time
257 - `instance`: whole instance resource
258
259 Default name format is `{instance[name]}`
260
261 :Example:
262
263 .. code-block:: yaml
264
265 policies:
266 - name: gcp-create-machine-image
267 resource: gcp.instance
268 filters:
269 - type: value
270 key: name
271 value: instance-create-to-make-image
272 actions:
273 - type: create-machine-image
274 name_format: "{instance[name]:.50}-{now:%Y-%m-%d}"
275
276 """
277 schema = type_schema('create-machine-image', name_format={'type': 'string'})
278 method_spec = {'op': 'insert'}
279 permissions = ('compute.machineImages.create',)
280
281 def get_resource_params(self, model, resource):
282 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/instances/(.*)')
283 project, _, _ = path_param_re.match(resource['selfLink']).groups()
284 name_format = self.data.get('name_format', '{instance[name]}')
285 name = name_format.format(instance=resource, now=datetime.now())
286
287 return {'project': project, 'sourceInstance': resource['selfLink'], 'body': {'name': name}}
288
289 def get_client(self, session, model):
290 return session.client(model.service, "beta", "machineImages")
291
292
293@resources.register('image')
294class Image(QueryResourceManager):
295
296 class resource_type(TypeInfo):
297 service = 'compute'
298 version = 'v1'
299 component = 'images'
300 name = id = 'name'
301 default_report_fields = [
302 "name", "description", "sourceType", "status", "creationTimestamp",
303 "diskSizeGb", "family"]
304 asset_type = "compute.googleapis.com/Image"
305 urn_component = "image"
306 labels = True
307
308 @staticmethod
309 def get(client, resource_info):
310 return client.execute_command(
311 'get', {'project': resource_info['project_id'],
312 'image': resource_info['image_id']})
313
314 @staticmethod
315 def get_label_params(resource, all_labels):
316 project, resource_id = re.match(
317 '.*?/projects/(.*?)/global/images/(.*)',
318 resource['selfLink']).groups()
319 return {'project': project, 'resource': resource_id,
320 'body': {
321 'labels': all_labels,
322 'labelFingerprint': resource['labelFingerprint']
323 }}
324
325 @classmethod
326 def refresh(cls, client, resource):
327 project, resource_id = re.match(
328 '.*?/projects/(.*?)/global/images/(.*)',
329 resource['selfLink']).groups()
330 return cls.get(
331 client,
332 {
333 'project_id': project,
334 'image_id': resource_id
335 }
336 )
337
338
339@Image.filter_registry.register('iam-policy')
340class ImageIamPolicyFilter(IamPolicyFilter):
341 """
342 Overrides the base implementation to process images resources correctly.
343 """
344 permissions = ('compute.images.getIamPolicy',)
345
346 def _verb_arguments(self, resource):
347 project, _ = re.match(
348 '.*?/projects/(.*?)/global/images/(.*)',
349 resource['selfLink']).groups()
350 verb_arguments = {'resource': resource[self.manager.resource_type.id], 'project': project}
351 return verb_arguments
352
353 def process_resources(self, resources):
354 value_filter = IamPolicyValueFilter(self.data['doc'], self.manager)
355 value_filter._verb_arguments = self._verb_arguments
356 return value_filter.process(resources)
357
358
359@Image.action_registry.register('delete')
360class DeleteImage(MethodAction):
361
362 schema = type_schema('delete')
363 method_spec = {'op': 'delete'}
364 attr_filter = ('status', ('READY'))
365 path_param_re = re.compile('.*?/projects/(.*?)/global/images/(.*)')
366
367 def get_resource_params(self, m, r):
368 project, image_id = self.path_param_re.match(r['selfLink']).groups()
369 return {'project': project, 'image': image_id}
370
371
372@resources.register('disk')
373class Disk(QueryResourceManager):
374
375 class resource_type(TypeInfo):
376 service = 'compute'
377 version = 'v1'
378 component = 'disks'
379 scope = 'zone'
380 enum_spec = ('aggregatedList', 'items.*.disks[]', None)
381 name = id = 'name'
382 labels = True
383 default_report_fields = ["name", "sizeGb", "status", "zone"]
384 asset_type = "compute.googleapis.com/Disk"
385 urn_component = "disk"
386 urn_zonal = True
387
388 @staticmethod
389 def get(client, resource_info):
390 return client.execute_command(
391 'get', {'project': resource_info['project_id'],
392 'zone': resource_info['zone'],
393 'disk': resource_info['disk_id']})
394
395 @staticmethod
396 def get_label_params(resource, all_labels):
397 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/disks/(.*)')
398 project, zone, instance = path_param_re.match(
399 resource['selfLink']).groups()
400 return {'project': project, 'zone': zone, 'resource': instance,
401 'body': {
402 'labels': all_labels,
403 'labelFingerprint': resource['labelFingerprint']
404 }}
405
406
407@Disk.action_registry.register('snapshot')
408class DiskSnapshot(MethodAction):
409 """
410 `Snapshots <https://cloud.google.com/compute/docs/reference/rest/v1/disks/createSnapshot>`_
411 disk.
412
413 The `name_format` specifies name of snapshot in python `format string <https://pyformat.info/>`
414
415 Inside format string there are defined variables:
416 - `now`: current time
417 - `disk`: whole disk resource
418
419 Default name format is `{disk.name}`
420
421 :Example:
422
423 .. code-block:: yaml
424
425 policies:
426 - name: gcp-disk-snapshot
427 resource: gcp.disk
428 filters:
429 - type: value
430 key: name
431 value: disk-7
432 actions:
433 - type: snapshot
434 name_format: "{disk[name]:.50}-{now:%Y-%m-%d}"
435 """
436 schema = type_schema('snapshot', name_format={'type': 'string'})
437 method_spec = {'op': 'createSnapshot'}
438 path_param_re = re.compile(
439 '.*?/projects/(.*?)/zones/(.*?)/disks/(.*)')
440 attr_filter = ('status', ('RUNNING', 'READY'))
441
442 def get_resource_params(self, model, resource):
443 project, zone, resourceId = self.path_param_re.match(resource['selfLink']).groups()
444 name_format = self.data.get('name_format', '{disk[name]}')
445 name = name_format.format(disk=resource, now=datetime.now())
446
447 return {
448 'project': project,
449 'zone': zone,
450 'disk': resourceId,
451 'body': {
452 'name': name,
453 'labels': resource.get('labels', {}),
454 }
455 }
456
457
458@Disk.action_registry.register('delete')
459class DiskDelete(MethodAction):
460
461 schema = type_schema('delete')
462 method_spec = {'op': 'delete'}
463 path_param_re = re.compile(
464 '.*?/projects/(.*?)/zones/(.*?)/disks/(.*)')
465 attr_filter = ('status', ('RUNNING', 'READY'))
466
467 def get_resource_params(self, m, r):
468 project, zone, resourceId = self.path_param_re.match(r['selfLink']).groups()
469 return {
470 'project': project,
471 'zone': zone,
472 'disk': resourceId,
473 }
474
475
476@resources.register('snapshot')
477class Snapshot(QueryResourceManager):
478
479 class resource_type(TypeInfo):
480 service = 'compute'
481 version = 'v1'
482 component = 'snapshots'
483 enum_spec = ('list', 'items[]', None)
484 name = id = 'name'
485 default_report_fields = ["name", "status", "diskSizeGb", "creationTimestamp"]
486 asset_type = "compute.googleapis.com/Snapshot"
487 urn_component = "snapshot"
488
489 @staticmethod
490 def get(client, resource_info):
491 return client.execute_command(
492 'get', {'project': resource_info['project_id'],
493 'snapshot': resource_info['snapshot_id']})
494
495
496@Snapshot.action_registry.register('delete')
497class DeleteSnapshot(MethodAction):
498
499 schema = type_schema('delete')
500 method_spec = {'op': 'delete'}
501 attr_filter = ('status', ('READY', 'UPLOADING'))
502 path_param_re = re.compile('.*?/projects/(.*?)/global/snapshots/(.*)')
503
504 def get_resource_params(self, m, r):
505 project, snapshot_id = self.path_param_re.match(r['selfLink']).groups()
506 # Docs are wrong :-(
507 # https://cloud.google.com/compute/docs/reference/rest/v1/snapshots/delete
508 return {'project': project, 'snapshot': snapshot_id}
509
510
511@resources.register('instance-template')
512class InstanceTemplate(QueryResourceManager):
513 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates"""
514 class resource_type(TypeInfo):
515 service = 'compute'
516 version = 'v1'
517 component = 'instanceTemplates'
518 scope = 'zone'
519 enum_spec = ('list', 'items[]', None)
520 name = id = 'name'
521 default_report_fields = [
522 name, "description", "creationTimestamp",
523 "properties.machineType", "properties.description"]
524 asset_type = "compute.googleapis.com/InstanceTemplate"
525 urn_component = "instance-template"
526
527 @staticmethod
528 def get(client, resource_info):
529 return client.execute_command(
530 'get', {'project': resource_info['project_id'],
531 'instanceTemplate': resource_info['instance_template_name']})
532
533
534@InstanceTemplate.action_registry.register('delete')
535class InstanceTemplateDelete(MethodAction):
536 """
537 `Deletes <https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/delete>`_
538 an Instance Template. The action does not specify any parameters.
539
540 :Example:
541
542 .. code-block:: yaml
543
544 policies:
545 - name: gcp-instance-template-delete
546 resource: gcp.instance-template
547 filters:
548 - type: value
549 key: name
550 value: instance-template-to-delete
551 actions:
552 - type: delete
553 """
554 schema = type_schema('delete')
555 method_spec = {'op': 'delete'}
556
557 def get_resource_params(self, m, r):
558 project, instance_template = re.match('.*/projects/(.*?)/.*/instanceTemplates/(.*)',
559 r['selfLink']).groups()
560 return {'project': project,
561 'instanceTemplate': instance_template}
562
563
564@resources.register('autoscaler')
565class Autoscaler(QueryResourceManager):
566 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/autoscalers"""
567 class resource_type(TypeInfo):
568 service = 'compute'
569 version = 'v1'
570 component = 'autoscalers'
571 name = id = 'name'
572 enum_spec = ('aggregatedList', 'items.*.autoscalers[]', None)
573 default_report_fields = [
574 "name", "description", "status", "target", "recommendedSize"]
575 asset_type = "compute.googleapis.com/Autoscaler"
576 metric_key = "resource.labels.autoscaler_name"
577 urn_component = "autoscaler"
578 urn_zonal = True
579
580 @staticmethod
581 def get(client, resource_info):
582 project, zone, autoscaler = re.match(
583 'projects/(.*?)/zones/(.*?)/autoscalers/(.*)',
584 resource_info['resourceName']).groups()
585
586 return client.execute_command(
587 'get', {'project': project,
588 'zone': zone,
589 'autoscaler': autoscaler})
590
591
592@Autoscaler.action_registry.register('set')
593class AutoscalerSet(MethodAction):
594 """
595 `Patches <https://cloud.google.com/compute/docs/reference/rest/v1/autoscalers/patch>`_
596 configuration parameters for the autoscaling algorithm.
597
598 The `coolDownPeriodSec` specifies the number of seconds that the autoscaler
599 should wait before it starts collecting information from a new instance.
600
601 The `cpuUtilization.utilizationTarget` specifies the target CPU utilization that the
602 autoscaler should maintain.
603
604 The `loadBalancingUtilization.utilizationTarget` specifies fraction of backend capacity
605 utilization (set in HTTP(S) load balancing configuration) that autoscaler should maintain.
606
607 The `minNumReplicas` specifies the minimum number of replicas that the autoscaler can
608 scale down to.
609
610 The `maxNumReplicas` specifies the maximum number of instances that the autoscaler can
611 scale up to.
612
613 :Example:
614
615 .. code-block:: yaml
616
617 policies:
618 - name: gcp-autoscaler-set
619 resource: gcp.autoscaler
620 filters:
621 - type: value
622 key: name
623 value: instance-group-2
624 actions:
625 - type: set
626 coolDownPeriodSec: 20
627 cpuUtilization:
628 utilizationTarget: 0.7
629 loadBalancingUtilization:
630 utilizationTarget: 0.7
631 minNumReplicas: 1
632 maxNumReplicas: 4
633 """
634 schema = type_schema('set',
635 **{
636 'coolDownPeriodSec': {
637 'type': 'integer',
638 'minimum': 15
639 },
640 'cpuUtilization': {
641 'type': 'object',
642 'required': ['utilizationTarget'],
643 'properties': {
644 'utilizationTarget': {
645 'type': 'number',
646 'exclusiveMinimum': 0,
647 'maximum': 1
648 }
649 },
650 },
651 'loadBalancingUtilization': {
652 'type': 'object',
653 'required': ['utilizationTarget'],
654 'properties': {
655 'utilizationTarget': {
656 'type': 'number',
657 'exclusiveMinimum': 0,
658 'maximum': 1
659 }
660 }
661 },
662 'maxNumReplicas': {
663 'type': 'integer',
664 'exclusiveMinimum': 0
665 },
666 'minNumReplicas': {
667 'type': 'integer',
668 'exclusiveMinimum': 0
669 }
670 })
671 method_spec = {'op': 'patch'}
672 path_param_re = re.compile('.*?/projects/(.*?)/zones/(.*?)/autoscalers/(.*)')
673 method_perm = 'update'
674
675 def get_resource_params(self, model, resource):
676 project, zone, autoscaler = self.path_param_re.match(resource['selfLink']).groups()
677 body = {}
678
679 if 'coolDownPeriodSec' in self.data:
680 body['coolDownPeriodSec'] = self.data['coolDownPeriodSec']
681
682 if 'cpuUtilization' in self.data:
683 body['cpuUtilization'] = self.data['cpuUtilization']
684
685 if 'loadBalancingUtilization' in self.data:
686 body['loadBalancingUtilization'] = self.data['loadBalancingUtilization']
687
688 if 'maxNumReplicas' in self.data:
689 body['maxNumReplicas'] = self.data['maxNumReplicas']
690
691 if 'minNumReplicas' in self.data:
692 body['minNumReplicas'] = self.data['minNumReplicas']
693
694 result = {'project': project,
695 'zone': zone,
696 'autoscaler': autoscaler,
697 'body': {
698 'autoscalingPolicy': body
699 }}
700
701 return result
702
703
704@resources.register('zone')
705class Zone(QueryResourceManager):
706 """GC resource: https://cloud.google.com/compute/docs/reference/rest/v1/zones"""
707 class resource_type(TypeInfo):
708 service = 'compute'
709 version = 'v1'
710 component = 'zones'
711 enum_spec = ('list', 'items[]', None)
712 scope = 'project'
713 name = id = 'name'
714 default_report_fields = ['id', 'name', 'dnsName', 'creationTime', 'visibility']
715 asset_type = "compute.googleapis.com/compute"
716 scc_type = "google.cloud.dns.ManagedZone"
717
718
719@resources.register('compute-project')
720class Project(QueryResourceManager):
721 """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/projects"""
722 class resource_type(TypeInfo):
723 service = 'compute'
724 version = 'v1'
725 component = 'projects'
726 enum_spec = ('get', '[@]', None)
727 name = id = 'name'
728 default_report_fields = ["name"]
729 asset_type = 'compute.googleapis.com/Project'
730
731 @staticmethod
732 def get(client, resource_info):
733 return client.execute_command(
734 'get', {'project': resource_info['project_id']})
735
736
737@resources.register('instance-group-manager')
738class InstanceGroupManager(ChildResourceManager):
739
740 class resource_type(ChildTypeInfo):
741 service = 'compute'
742 version = 'v1'
743 component = 'instanceGroupManagers'
744 enum_spec = ('list', 'items[]', None)
745 name = id = 'name'
746 parent_spec = {
747 'resource': 'zone',
748 'child_enum_params': {
749 ('name', 'zone')},
750 'use_child_query': False,
751 }
752 default_report_fields = ['id', 'name', 'dnsName', 'creationTime', 'visibility']