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