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