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