[VVP] adding list support for non-server-name uniqueness
[vvp/validation-scripts.git] / ice_validator / tests / structures.py
1 # -*- coding: utf8 -*-
2 # ============LICENSE_START====================================================
3 # org.onap.vvp/validation-scripts
4 # ===================================================================
5 # Copyright © 2019 AT&T Intellectual Property. All rights reserved.
6 # ===================================================================
7 #
8 # Unless otherwise specified, all software contained herein is licensed
9 # under the Apache License, Version 2.0 (the "License");
10 # you may not use this software except in compliance with the License.
11 # You may obtain a copy of the License at
12 #
13 #             http://www.apache.org/licenses/LICENSE-2.0
14 #
15 # Unless required by applicable law or agreed to in writing, software
16 # distributed under the License is distributed on an "AS IS" BASIS,
17 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 # See the License for the specific language governing permissions and
19 # limitations under the License.
20 #
21 #
22 #
23 # Unless otherwise specified, all documentation contained herein is licensed
24 # under the Creative Commons License, Attribution 4.0 Intl. (the "License");
25 # you may not use this documentation except in compliance with the License.
26 # You may obtain a copy of the License at
27 #
28 #             https://creativecommons.org/licenses/by/4.0/
29 #
30 # Unless required by applicable law or agreed to in writing, documentation
31 # distributed under the License is distributed on an "AS IS" BASIS,
32 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33 # See the License for the specific language governing permissions and
34 # limitations under the License.
35 #
36 # ============LICENSE_END============================================
37 #
38 #
39 """structures
40 """
41 import collections
42 import inspect
43 import os
44 import re
45 import sys
46
47 from tests import cached_yaml as yaml
48 from tests.helpers import load_yaml
49 from .utils import nested_dict
50
51 VERSION = "4.2.0"
52
53 # key = pattern, value = regex compiled from pattern
54 _REGEX_CACHE = {}
55
56
57 def _get_regex(pattern):
58     """Return a compiled version of pattern.
59     Keep result in _REGEX_CACHE to avoid re-compiling.
60     """
61     regex = _REGEX_CACHE.get(pattern, None)
62     if regex is None:
63         regex = re.compile(pattern)
64         _REGEX_CACHE[pattern] = regex
65     return regex
66
67
68 class Hashabledict(dict):
69     """A hashable dict.
70     dicts with the same keys and whose keys have the same values
71     are assigned the same hash.
72     """
73
74     def __hash__(self):
75         return hash((frozenset(self), frozenset(self.values())))
76
77
78 class HeatProcessor(object):
79     """base class for xxxx::xxxx::xxxx processors
80     """
81
82     resource_type = None  # string 'xxxx::xxxx::xxxx'
83     re_rids = collections.OrderedDict()  # OrderedDict of name: regex
84     # name is a string to name the regex.
85     # regex parses the proper resource id format.
86
87     @staticmethod
88     def get_param_value(value, withIndex=False):
89         """Return get_param value of `value`
90         """
91         if isinstance(value, dict) and len(value) == 1:
92             v = value.get("get_param")
93             if isinstance(v, list) and v:
94                 if withIndex and len(v) > 1:
95                     idx = v[1]
96                     if isinstance(idx, dict):
97                         idx = idx.get("get_param", idx)
98                     v = "{}{}".format(v[0], idx)
99                 else:
100                     v = v[0]
101         else:
102             v = None
103         return v
104
105     @classmethod
106     def get_resource_or_param_value(cls, value):
107         """Return the get_resource or get_param value of `value`
108         """
109         if isinstance(value, dict) and len(value) == 1:
110             v = value.get("get_resource") or cls.get_param_value(value)
111         else:
112             v = None
113         return v
114
115     @classmethod
116     def get_rid_match_tuple(cls, rid):
117         """find the first regex matching `rid` and return the tuple
118         (name, match object) or ('', None) if no match.
119         """
120         rid = "" if rid is None else rid
121         for name, regex in cls.re_rids.items():
122             match = regex.match(rid)
123             if match:
124                 return name, match
125         return "", None
126
127     @classmethod
128     def get_rid_patterns(cls):
129         """Return OrderedDict of name: friendly regex.pattern
130         "friendly" means the group notation is replaced with
131         braces, and the trailing "$" is removed.
132
133         NOTE
134         nested parentheses in any rid_pattern will break this parser.
135         The final character is ASSUMED to be a dollar sign.
136         """
137         friendly_pattern = _get_regex(r"\(\?P<(.*?)>.*?\)")
138         rid_patterns = collections.OrderedDict()
139         for name, regex in cls.re_rids.items():
140             rid_patterns[name] = friendly_pattern.sub(
141                 r"{\1}", regex.pattern  # replace groups with braces
142             )[
143                 :-1
144             ]  # remove trailing $
145         return rid_patterns
146
147     @classmethod
148     def get_str_replace_name(cls, resource_dict):
149         """Return the name modified by str_replace of `resource_dict`,
150         a resource (i.e. a value in some template's resources).
151         Return None, if there is no name, str_replace, its template,
152         or any missing parameters.
153         """
154         str_replace = Heat.nested_get(
155             resource_dict, "properties", "name", "str_replace"
156         )
157         if not str_replace:
158             return None
159         template = Heat.nested_get(str_replace, "template")
160         if not isinstance(template, str):
161             return None
162         params = Heat.nested_get(str_replace, "params", default={})
163         if not isinstance(params, dict):
164             return None
165         # WARNING
166         # The user must choose non-overlapping keys for params since they
167         # are replaced in the template in arbitrary order.
168         name = template
169         for key, value in params.items():
170             param = cls.get_param_value(value, withIndex=True)
171             if param is None:
172                 return None
173             name = name.replace(key, str(param))
174         return name
175
176
177 class CinderVolumeAttachmentProcessor(HeatProcessor):
178     """ Cinder VolumeAttachment
179     """
180
181     resource_type = "OS::Cinder::VolumeAttachment"
182
183     @classmethod
184     def get_config(cls, resources):
185         """Return a tuple (va_config, va_count)
186         va_config - Hashabledict of Cinder Volume Attachment config
187                     indexed by rid.
188         va_count - dict of attachment counts indexed by rid.
189         """
190         va_count = collections.defaultdict(int)
191         va_config = Hashabledict()
192         for resource in resources.values():
193             resource_type = nested_dict.get(resource, "type")
194             if resource_type == cls.resource_type:
195                 config, rids = cls.get_volume_attachment_config(resource)
196                 for rid in rids:
197                     va_config[rid] = config
198                     va_count[rid] += 1
199         return va_config, va_count
200
201     @classmethod
202     def get_volume_attachment_config(cls, resource):
203         """Returns the cinder volume attachment configuration
204         of `resource` as a tuple (config, rids)
205         where:
206         - config is a Hashabledict whose keys are the keys of the
207             properties of resource, and whose values are the
208             corresponding property values (nova server resource ids)
209             replaced with the vm-type they reference.
210         - rids is the set of nova server resource ids referenced by
211             the property values.
212         """
213         config = Hashabledict()
214         rids = set()
215         for key, value in (resource.get("properties") or {}).items():
216             rid = cls.get_resource_or_param_value(value)
217             if rid:
218                 name, match = NovaServerProcessor.get_rid_match_tuple(rid)
219                 if name == "server":
220                     vm_type = match.groupdict()["vm_type"]
221                     config[key] = vm_type
222                     rids.add(rid)
223         return config, rids
224
225
226 class ContrailV2NetworkFlavorBaseProcessor(HeatProcessor):
227     """ContrailV2 objects which have network_flavor
228     """
229
230     network_flavor_external = "external"
231     network_flavor_internal = "internal"
232     network_flavor_subint = "subint"
233
234     @classmethod
235     def get_network_flavor(cls, resource):
236         """Return the network flavor of resource, one of
237         "internal" - get_resource, or get_param contains _int_
238         "subint" - get_param contains _subint_
239         "external" - otherwise
240         None - no parameters found to decide the flavor.
241
242         resource.properties.virtual_network_refs should be a list.
243         All the parameters in the list should have the same "flavor"
244         so the flavor is determined from the first item.
245         """
246         network_flavor = None
247         network_refs = nested_dict.get(resource, "properties", "virtual_network_refs")
248         if network_refs and isinstance(network_refs, list):
249             param = network_refs[0]
250             if isinstance(param, dict):
251                 if "get_resource" in param:
252                     network_flavor = cls.network_flavor_internal
253                 else:
254                     p = param.get("get_param")
255                     if isinstance(p, str):
256                         if "_int_" in p or p.startswith("int_"):
257                             network_flavor = cls.network_flavor_internal
258                         elif "_subint_" in p:
259                             network_flavor = cls.network_flavor_subint
260                         else:
261                             network_flavor = cls.network_flavor_external
262         return network_flavor
263
264
265 class ContrailV2InstanceIpProcessor(ContrailV2NetworkFlavorBaseProcessor):
266     """ ContrailV2 InstanceIp
267     """
268
269     resource_type = "OS::ContrailV2::InstanceIp"
270     re_rids = collections.OrderedDict(
271         [
272             (
273                 "int_ip",
274                 _get_regex(
275                     r"(?P<vm_type>.+)"
276                     r"_(?P<vm_type_index>\d+)"
277                     r"_int"
278                     r"_(?P<network_role>.+)"
279                     r"_vmi"
280                     r"_(?P<vmi_index>\d+)"
281                     r"_IP"
282                     r"_(?P<index>\d+)"
283                     r"$"
284                 ),
285             ),
286             (
287                 "int_v6_ip",
288                 _get_regex(
289                     r"(?P<vm_type>.+)"
290                     r"_(?P<vm_type_index>\d+)"
291                     r"_int"
292                     r"_(?P<network_role>.+)"
293                     r"_vmi"
294                     r"_(?P<vmi_index>\d+)"
295                     r"_v6_IP"
296                     r"_(?P<index>\d+)"
297                     r"$"
298                 ),
299             ),
300             (
301                 "subint_ip",
302                 _get_regex(
303                     r"(?P<vm_type>.+)"
304                     r"_(?P<vm_type_index>\d+)"
305                     r"_subint"
306                     r"_(?P<network_role>.+)"
307                     r"_vmi"
308                     r"_(?P<vmi_index>\d+)"
309                     r"_IP"
310                     r"_(?P<index>\d+)"
311                     r"$"
312                 ),
313             ),
314             (
315                 "subint_v6_ip",
316                 _get_regex(
317                     r"(?P<vm_type>.+)"
318                     r"_(?P<vm_type_index>\d+)"
319                     r"_subint"
320                     r"_(?P<network_role>.+)"
321                     r"_vmi"
322                     r"_(?P<vmi_index>\d+)"
323                     r"_v6_IP"
324                     r"_(?P<index>\d+)"
325                     r"$"
326                 ),
327             ),
328             (
329                 "ip",
330                 _get_regex(
331                     r"(?P<vm_type>.+)"
332                     r"_(?P<vm_type_index>\d+)"
333                     r"_(?P<network_role>.+)"
334                     r"_vmi"
335                     r"_(?P<vmi_index>\d+)"
336                     r"_IP"
337                     r"_(?P<index>\d+)"
338                     r"$"
339                 ),
340             ),
341             (
342                 "v6_ip",
343                 _get_regex(
344                     r"(?P<vm_type>.+)"
345                     r"_(?P<vm_type_index>\d+)"
346                     r"_(?P<network_role>.+)"
347                     r"_vmi"
348                     r"_(?P<vmi_index>\d+)"
349                     r"_v6_IP"
350                     r"_(?P<index>\d+)"
351                     r"$"
352                 ),
353             ),
354         ]
355     )
356
357
358 class ContrailV2InterfaceRouteTableProcessor(HeatProcessor):
359     """ ContrailV2 InterfaceRouteTable
360     """
361
362     resource_type = "OS::ContrailV2::InterfaceRouteTable"
363
364
365 class ContrailV2NetworkIpamProcessor(HeatProcessor):
366     """ ContrailV2 NetworkIpam
367     """
368
369     resource_type = "OS::ContrailV2::NetworkIpam"
370
371
372 class ContrailV2PortTupleProcessor(HeatProcessor):
373     """ ContrailV2 PortTuple
374     """
375
376     resource_type = "OS::ContrailV2::PortTuple"
377
378
379 class ContrailV2ServiceHealthCheckProcessor(HeatProcessor):
380     """ ContrailV2 ServiceHealthCheck
381     """
382
383     resource_type = "OS::ContrailV2::ServiceHealthCheck"
384
385
386 class ContrailV2ServiceInstanceProcessor(HeatProcessor):
387     """ ContrailV2 ServiceInstance
388     """
389
390     resource_type = "OS::ContrailV2::ServiceInstance"
391
392
393 class ContrailV2ServiceInstanceIpProcessor(HeatProcessor):
394     """ ContrailV2 ServiceInstanceIp
395     """
396
397     resource_type = "OS::ContrailV2::ServiceInstanceIp"
398
399
400 class ContrailV2ServiceTemplateProcessor(HeatProcessor):
401     """ ContrailV2 ServiceTemplate
402     """
403
404     resource_type = "OS::ContrailV2::ServiceTemplate"
405
406
407 class ContrailV2VirtualMachineInterfaceProcessor(ContrailV2NetworkFlavorBaseProcessor):
408     """ ContrailV2 Virtual Machine Interface resource
409     """
410
411     resource_type = "OS::ContrailV2::VirtualMachineInterface"
412     re_rids = collections.OrderedDict(
413         [
414             (
415                 "vmi_internal",
416                 _get_regex(
417                     r"(?P<vm_type>.+)"
418                     r"_(?P<vm_type_index>\d+)"
419                     r"_int"
420                     r"_(?P<network_role>.+)"
421                     r"_vmi"
422                     r"_(?P<vmi_index>\d+)"
423                     r"$"
424                 ),
425             ),
426             (
427                 "vmi_subint",
428                 _get_regex(
429                     r"(?P<vm_type>.+)"
430                     r"_(?P<vm_type_index>\d+)"
431                     r"_subint"
432                     r"_(?P<network_role>.+)"
433                     r"_vmi"
434                     r"_(?P<vmi_index>\d+)"
435                     r"$"
436                 ),
437             ),
438             (
439                 "vmi_external",
440                 _get_regex(
441                     r"(?P<vm_type>.+)"
442                     r"_(?P<vm_type_index>\d+)"
443                     r"_(?P<network_role>.+)"
444                     r"_vmi"
445                     r"_(?P<vmi_index>\d+)"
446                     r"$"
447                 ),
448             ),
449         ]
450     )
451
452
453 class ContrailV2VirtualNetworkProcessor(HeatProcessor):
454     """ ContrailV2 VirtualNetwork
455     """
456
457     resource_type = "OS::ContrailV2::VirtualNetwork"
458     re_rids = collections.OrderedDict(
459         [
460             ("network", _get_regex(r"int" r"_(?P<network_role>.+)" r"_network" r"$")),
461             # ("rvn", _get_regex(r"int" r"_(?P<network_role>.+)" r"_RVN" r"$")),
462         ]
463     )
464
465
466 class HeatResourceGroupProcessor(HeatProcessor):
467     """ Heat ResourceGroup
468     """
469
470     resource_type = "OS::Heat::ResourceGroup"
471     re_rids = collections.OrderedDict(
472         [
473             (
474                 "subint",
475                 _get_regex(
476                     r"(?P<vm_type>.+)"
477                     r"_(?P<vm_type_index>\d+)"
478                     r"_subint"
479                     r"_(?P<network_role>.+)"
480                     r"_port_(?P<port_index>\d+)"
481                     r"_subinterfaces"
482                     r"$"
483                 ),
484             )
485         ]
486     )
487
488
489 class NeutronNetProcessor(HeatProcessor):
490     """ Neutron Net resource
491     """
492
493     resource_type = "OS::Neutron::Net"
494     re_rids = collections.OrderedDict(
495         [("network", _get_regex(r"int" r"_(?P<network_role>.+)" r"_network" r"$"))]
496     )
497
498
499 class NeutronPortProcessor(HeatProcessor):
500     """ Neutron Port resource
501     """
502
503     resource_type = "OS::Neutron::Port"
504     re_rids = collections.OrderedDict(
505         [
506             (
507                 "internal_port",
508                 _get_regex(
509                     r"(?P<vm_type>.+)"
510                     r"_(?P<vm_type_index>\d+)"
511                     r"_int"
512                     r"_(?P<network_role>.+)"
513                     r"_port_(?P<port_index>\d+)"
514                     r"$"
515                 ),
516             ),
517             (
518                 "port",
519                 _get_regex(
520                     r"(?P<vm_type>.+)"
521                     r"_(?P<vm_type_index>\d+)"
522                     r"_(?P<network_role>.+)"
523                     r"_port_(?P<port_index>\d+)"
524                     r"$"
525                 ),
526             ),
527             (
528                 "floating_ip",
529                 _get_regex(
530                     r"reserve_port"
531                     r"_(?P<vm_type>.+)"
532                     r"_(?P<network_role>.+)"
533                     r"_floating_ip_(?P<index>\d+)"
534                     r"$"
535                 ),
536             ),
537             (
538                 "floating_v6_ip",
539                 _get_regex(
540                     r"reserve_port"
541                     r"_(?P<vm_type>.+)"
542                     r"_(?P<network_role>.+)"
543                     r"_floating_v6_ip_(?P<index>\d+)"
544                     r"$"
545                 ),
546             ),
547         ]
548     )
549
550     @classmethod
551     def uses_sr_iov(cls, resource):
552         """Returns True/False as `resource` is/not
553         An OS::Nova:Port with the property binding:vnic_type
554         """
555         return nested_dict.get(
556             resource, "type"
557         ) == cls.resource_type and "binding:vnic_type" in nested_dict.get(
558             resource, "properties", default={}
559         )
560
561
562 class NovaServerProcessor(HeatProcessor):
563     """ Nova Server resource
564     """
565
566     resource_type = "OS::Nova::Server"
567     re_rids = collections.OrderedDict(
568         [
569             (
570                 "server",
571                 _get_regex(r"(?P<vm_type>.+)" r"_server_(?P<vm_type_index>\d+)" r"$"),
572             )
573         ]
574     )
575
576     @classmethod
577     def get_flavor(cls, resource):
578         """Return the flavor property of `resource`
579         """
580         return cls.get_param_value(nested_dict.get(resource, "properties", "flavor"))
581
582     @classmethod
583     def get_image(cls, resource):
584         """Return the image property of `resource`
585         """
586         return cls.get_param_value(nested_dict.get(resource, "properties", "image"))
587
588     @classmethod
589     def get_network(cls, resource):
590         """Return the network configuration of `resource` as a
591         frozenset of network-roles.
592         """
593         network = set()
594         networks = nested_dict.get(resource, "properties", "networks")
595         if isinstance(networks, list):
596             for port in networks:
597                 value = cls.get_resource_or_param_value(nested_dict.get(port, "port"))
598                 name, match = NeutronPortProcessor.get_rid_match_tuple(value)
599                 if name:
600                     network_role = match.groupdict().get("network_role")
601                     if network_role:
602                         network.add(network_role)
603         return frozenset(network)
604
605     @classmethod
606     def get_vm_class(cls, resource):
607         """Return the vm_class of `resource`, a Hashabledict (of
608         hashable values) whose keys are only the required keys.
609         Return empty Hashabledict if `resource` is not a NovaServer.
610         """
611         vm_class = Hashabledict()
612         resource_type = nested_dict.get(resource, "type")
613         if resource_type == cls.resource_type:
614             d = dict(
615                 flavor=cls.get_flavor(resource),
616                 image=cls.get_image(resource),
617                 networks=cls.get_network(resource),
618             )
619             if all(d.values()):
620                 vm_class.update(d)
621         return vm_class
622
623
624 class Heat(object):
625     """A Heat template.
626     filepath - absolute path to template file.
627     envpath - absolute path to environmnt file.
628     """
629
630     type_bool = "boolean"
631     type_boolean = "boolean"
632     type_cdl = "comma_delimited_list"
633     type_comma_delimited_list = "comma_delimited_list"
634     type_json = "json"
635     type_num = "number"
636     type_number = "number"
637     type_str = "string"
638     type_string = "string"
639
640     def __init__(self, filepath=None, envpath=None):
641         self.filepath = None
642         self.basename = None
643         self.dirname = None
644         self.yml = None
645         self.heat_template_version = None
646         self.description = None
647         self.parameter_groups = None
648         self.parameters = None
649         self.resources = None
650         self.outputs = None
651         self.conditions = None
652         if filepath:
653             self.load(filepath)
654         self.env = None
655         if envpath:
656             self.load_env(envpath)
657         self.heat_processors = self.get_heat_processors()
658
659     @property
660     def contrail_resources(self):
661         """This attribute is a dict of Contrail resources.
662         """
663         return self.get_resource_by_type(
664             resource_type=ContrailV2VirtualMachineInterfaceProcessor.resource_type
665         )
666
667     def get_all_resources(self, base_dir):
668         """
669         Like ``resources``,
670         but this returns all the resources definitions
671         defined in the template, resource groups, and nested YAML files.
672         """
673         resources = {}
674         for r_id, r_data in self.resources.items():
675             resources[r_id] = r_data
676             resource = Resource(r_id, r_data)
677             if resource.is_nested():
678                 nested = Heat(os.path.join(base_dir, resource.get_nested_filename()))
679                 resources.update(nested.get_all_resources(base_dir))
680         return resources
681
682     @staticmethod
683     def get_heat_processors():
684         """Return a dict, key is resource_type, value is the
685         HeatProcessor subclass whose resource_type is the key.
686         """
687         return _HEAT_PROCESSORS
688
689     def get_resource_by_type(self, resource_type):
690         """Return dict of resources whose type is `resource_type`.
691         key is resource_id, value is resource.
692         """
693         return {
694             rid: resource
695             for rid, resource in self.resources.items()
696             if self.nested_get(resource, "type") == resource_type
697         }
698
699     def get_rid_match_tuple(self, rid, resource_type):
700         """return get_rid_match_tuple(rid) called on the class
701         corresponding to the given resource_type.
702         """
703         processor = self.heat_processors.get(resource_type, HeatProcessor)
704         return processor.get_rid_match_tuple(rid)
705
706     def get_vm_type(self, rid, resource=None):
707         """return the vm_type
708         """
709         if resource is None:
710             resource = self
711         resource_type = self.nested_get(resource, "type")
712         match = self.get_rid_match_tuple(rid, resource_type)[1]
713         vm_type = match.groupdict().get("vm_type") if match else None
714         return vm_type
715
716     def load(self, filepath):
717         """Load the Heat template given a filepath.
718         """
719         self.filepath = filepath
720         self.basename = os.path.basename(self.filepath)
721         self.dirname = os.path.dirname(self.filepath)
722         with open(self.filepath) as fi:
723             self.yml = yaml.load(fi)
724         self.heat_template_version = self.yml.get("heat_template_version", None)
725         self.description = self.yml.get("description", "")
726         self.parameter_groups = self.yml.get("parameter_groups") or {}
727         self.parameters = self.yml.get("parameters") or {}
728         self.resources = self.yml.get("resources") or {}
729         self.outputs = self.yml.get("outputs") or {}
730         self.conditions = self.yml.get("conditions") or {}
731
732     def load_env(self, envpath):
733         """Load the Environment template given a envpath.
734         """
735         self.env = Env(filepath=envpath)
736
737     @staticmethod
738     def nested_get(dic, *keys, **kwargs):
739         """make utils.nested_dict.get available as a class method.
740         """
741         return nested_dict.get(dic, *keys, **kwargs)
742
743     @property
744     def neutron_port_resources(self):
745         """This attribute is a dict of Neutron Ports
746         """
747         return self.get_resource_by_type(
748             resource_type=NeutronPortProcessor.resource_type
749         )
750
751     @property
752     def nova_server_resources(self):
753         """This attribute is a dict of Nova Servers
754         """
755         return self.get_resource_by_type(
756             resource_type=NovaServerProcessor.resource_type
757         )
758
759     @staticmethod
760     def part_is_in_name(part, name):
761         """
762         Return True if any of
763         - name starts with part + '_'
764         - name contains '_' + part + '_'
765         - name ends with '_' + part
766         False otherwise
767         """
768         return bool(
769             re.search("(^(%(x)s)_)|(_(%(x)s)_)|(_(%(x)s)$)" % dict(x=part), name)
770         )
771
772
773 class Env(Heat):
774     """An Environment file
775     """
776
777     pass
778
779
780 class Resource(object):
781     """A Resource
782     """
783
784     def __init__(self, resource_id=None, resource=None):
785         self.resource_id = resource_id or ""
786         self.resource = resource or {}
787         self.properties = self.resource.get("properties", {})
788         self.resource_type = self.resource.get("type", "")
789
790     @staticmethod
791     def get_index_var(resource):
792         """Return the index_var for this resource.
793         """
794         index_var = nested_dict.get(resource, "properties", "index_var") or "index"
795         return index_var
796
797     def get_nested_filename(self):
798         """Returns the filename of the nested YAML file if the
799         resource is a nested YAML or ResourceDef, returns '' otherwise."""
800         typ = self.resource.get("type", "")
801         if typ == "OS::Heat::ResourceGroup":
802             rd = nested_dict.get(self.resource, "properties", "resource_def")
803             typ = rd.get("type", "") if rd else ""
804         ext = os.path.splitext(typ)[1]
805         ext = ext.lower()
806         if ext == ".yml" or ext == ".yaml":
807             return typ
808         else:
809             return ""
810
811     def get_nested_properties(self):
812         """
813         Returns {} if not nested
814         Returns resource: properties if nested
815         Returns resource: properties: resource_def: properties if RG
816         """
817         if not bool(self.get_nested_filename()):
818             return {}
819         elif self.resource_type == "OS::Heat::ResourceGroup":
820             return nested_dict.get(
821                 self.properties, "resource_def", "properties", default={}
822             )
823         else:
824             return self.properties
825
826     @property
827     def depends_on(self):
828         """
829         Returns the list of resources this resource depends on.  Always
830         returns a list.
831
832         :return: list of all resource IDs this resource depends on.  If none,
833                  then returns an empty list
834         """
835         parents = self.resource.get("depends_on", [])
836         return parents if isinstance(parents, list) else [parents]
837
838     def is_nested(self):
839         """Returns True if the resource represents a Nested YAML resource
840         using either type: {filename} or ResourceGroup -> resource_def"""
841         return bool(self.get_nested_filename())
842
843     def get_nested_yaml(self, base_dir):
844         """If the resource represents a Nested YAML resource, then it
845         returns the loaded YAML.  If the resource is not nested or the
846         file cannot be found, then an empty dict is returned"""
847         filename = self.get_nested_filename()
848         if filename:
849             file_path = os.path.join(base_dir, filename)
850             return load_yaml(file_path) if os.path.exists(file_path) else {}
851         else:
852             return {}
853
854
855 def get_all_resources(yaml_files):
856     """Return a dict, resource id: resource
857     of the union of resources across all files.
858     """
859     resources = {}
860     for heat_template in yaml_files:
861         heat = Heat(filepath=heat_template)
862         dirname = os.path.dirname(heat_template)
863         resources.update(heat.get_all_resources(dirname))
864     return resources
865
866
867 def _get_heat_processors():
868     """Introspect this module and return a
869     dict of all HeatProcessor sub-classes with a (True) resource_type.
870     Key is the resource_type, value is the corrresponding class.
871     """
872     mod_classes = inspect.getmembers(sys.modules[__name__], inspect.isclass)
873     heat_processors = {
874         c.resource_type: c
875         for _, c in mod_classes
876         if issubclass(c, HeatProcessor) and c.resource_type
877     }
878     return heat_processors
879
880
881 _HEAT_PROCESSORS = _get_heat_processors()