1 {%- set primary_role = [roles[0]] -%}
2 {%- for role in roles -%}
3 {%- if 'primary' in role.tags and 'controller' in role.tags -%}
4 {%- set _ = primary_role.pop() -%}
5 {%- set _ = primary_role.append(role) -%}
8 {%- set primary_role_name = primary_role[0].name -%}
9 # primary role is: {{primary_role_name}}
10 heat_template_version: pike
13 Deploy an OpenStack environment, consisting of several node types (roles),
14 Controller, Compute, BlockStorage, SwiftStorage and CephStorage. The Storage
15 roles enable independent scaling of the storage components, but the minimal
16 deployment is one Controller and one Compute node.
19 # TODO(shadower): we should probably use the parameter groups to put
23 # Common parameters (not specific to a role)
25 default: overcloud.localdomain
26 description: The DNS name of this cloud. E.g. ci-overcloud.tripleo.org
29 default: overcloud.internalapi.localdomain
31 The DNS name of this cloud's internal API endpoint. E.g.
32 'ci-overcloud.internalapi.tripleo.org'.
35 default: overcloud.storage.localdomain
37 The DNS name of this cloud's storage endpoint. E.g.
38 'ci-overcloud.storage.tripleo.org'.
40 CloudNameStorageManagement:
41 default: overcloud.storagemgmt.localdomain
43 The DNS name of this cloud's storage management endpoint. E.g.
44 'ci-overcloud.storagemgmt.tripleo.org'.
47 default: overcloud.ctlplane.localdomain
49 The DNS name of this cloud's storage management endpoint. E.g.
50 'ci-overcloud.management.tripleo.org'.
55 Control the IP allocation for the ControlVirtualIP port. E.g.
56 [{'ip_address':'1.2.3.4'}]
58 InternalApiVirtualFixedIPs:
61 Control the IP allocation for the InternalApiVirtualInterface port. E.g.
62 [{'ip_address':'1.2.3.4'}]
64 NeutronControlPlaneID:
67 description: Neutron ID or name for ctlplane network.
68 NeutronPublicInterface:
70 description: What interface to bridge onto br-ex for network nodes.
72 PublicVirtualFixedIPs:
75 Control the IP allocation for the PublicVirtualInterface port. E.g.
76 [{'ip_address':'1.2.3.4'}]
81 description: Salt for the rabbit cookie, change this to force the randomly generated rabbit cookie to change.
82 StorageVirtualFixedIPs:
85 Control the IP allocation for the StorageVirtualInterface port. E.g.
86 [{'ip_address':'1.2.3.4'}]
88 StorageMgmtVirtualFixedIPs:
91 Control the IP allocation for the StorageMgmgVirtualInterface port. E.g.
92 [{'ip_address':'1.2.3.4'}]
97 Control the IP allocation for the virtual IP used by Redis. E.g.
98 [{'ip_address':'1.2.3.4'}]
101 default: 'localdomain'
104 The DNS domain used for the hosts. This should match the dhcp_domain
105 configured in the Undercloud neutron. Defaults to localdomain.
109 Extra properties or metadata passed to Nova for the created nodes in
110 the overcloud. It's accessible via the Nova metadata API.
113 # Compute-specific params
114 # FIXME(shardy) handle these deprecated names as they don't match compute.yaml
115 HypervisorNeutronPhysicalBridge:
118 An OVS bridge to create on each hypervisor. This defaults to br-ex the
119 same as the control plane nodes, as we have a uniform configuration of
120 the openvswitch agent. Typically should not need to be changed.
122 HypervisorNeutronPublicInterface:
124 description: What interface to add to the HypervisorNeutronPhysicalBridge.
129 description: Maxiumum batch size for creating nodes
132 # Jinja loop for Role in role_data.yaml
133 {% for role in roles %}
134 # Parameters generated for {{role.name}} Role
135 {{role.name}}Services:
136 description: A list of service resources (configured in the Heat
137 resource_registry) which represent nested stacks
138 for each service that should get installed on the {{role.name}} role.
139 type: comma_delimited_list
142 description: Number of {{role.name}} nodes to deploy
144 default: {{role.CountDefault|default(0)}}
146 {{role.name}}HostnameFormat:
149 Format for {{role.name}} node hostnames
150 Note %index% is translated into the index of the node, e.g 0/1/2 etc
151 and %stackname% is replaced with the stack name e.g overcloud
152 {% if role.HostnameFormatDefault %}
153 default: "{{role.HostnameFormatDefault}}"
155 default: "%stackname%-{{role.name.lower()}}-%index%"
158 {{role.name}}RemovalPolicies:
162 List of resources to be removed from {{role.name}} ResourceGroup when
163 doing an update which requires removal of specific resources.
164 Example format ComputeRemovalPolicies: [{'resource_list': ['0']}]
166 {% if role.name != 'Compute' %}
167 {{role.name}}SchedulerHints:
169 NovaComputeSchedulerHints:
172 description: Optional scheduler hints to pass to nova
175 {{role.name}}Parameters:
177 description: Optional Role Specific parameters to be provided to service
181 # Identifiers to trigger tasks on nodes
186 Setting to a previously unused value during stack-update will trigger
187 package update on all nodes
192 Setting this to a unique value will re-run any deployment tasks which
193 perform configuration on a Heat stack-update.
198 Set to true to append per network Vips to /etc/hosts on each node.
200 DeploymentServerBlacklist:
202 type: comma_delimited_list
204 List of server hostnames to blacklist from any triggered deployments.
207 add_vips_to_etc_hosts: {equals : [{get_param: AddVipsToEtcHosts}, True]}
212 type: OS::Heat::Value
221 IP: {get_attr: [VipMap, net_ip_map, external]}
222 HOST: {get_param: CloudName}
226 IP: {get_attr: [VipMap, net_ip_map, ctlplane]}
227 HOST: {get_param: CloudNameCtlplane}
231 IP: {get_attr: [VipMap, net_ip_map, internal_api]}
232 HOST: {get_param: CloudNameInternal}
236 IP: {get_attr: [VipMap, net_ip_map, storage]}
237 HOST: {get_param: CloudNameStorage}
241 IP: {get_attr: [VipMap, net_ip_map, storage_mgmt]}
242 HOST: {get_param: CloudNameStorageManagement}
244 HeatAuthEncryptionKey:
245 type: OS::Heat::RandomString
248 type: OS::Heat::RandomString
253 type: OS::Heat::RandomString
258 type: OS::TripleO::ServiceNetMap
261 type: OS::TripleO::EndpointMap
264 external: {get_param: CloudName}
265 internal_api: {get_param: CloudNameInternal}
266 storage: {get_param: CloudNameStorage}
267 storage_mgmt: {get_param: CloudNameStorageManagement}
268 ctlplane: {get_param: CloudNameCtlplane}
269 NetIpMap: {get_attr: [VipMap, net_ip_map]}
270 ServiceNetMap: {get_attr: [ServiceNetMap, service_net_map]}
273 type: OS::Heat::Value
276 value: {get_attr: [EndpointMap, endpoint_map]}
279 type: OS::TripleO::Ssh::KnownHostsConfig
284 {% for role in roles %}
285 - {get_attr: [{{role.name}}, known_hosts_entry]}
288 # Jinja loop for Role in roles_data.yaml
289 {% for role in roles %}
290 # Resources generated for {{role.name}} Role
291 {{role.name}}ServiceChain:
292 type: OS::TripleO::Services
295 get_param: {{role.name}}Services
296 ServiceNetMap: {get_attr: [ServiceNetMap, service_net_map]}
297 EndpointMap: {get_attr: [EndpointMap, endpoint_map]}
298 DefaultPasswords: {get_attr: [DefaultPasswords, passwords]}
299 RoleName: {{role.name}}
300 RoleParameters: {get_param: {{role.name}}Parameters}
302 # Lookup of role_data via heat outputs is slow, so workaround this by caching
303 # the value in an OS::Heat::Value resource
304 {{role.name}}ServiceChainRoleData:
305 type: OS::Heat::Value
308 value: {get_attr: [{{role.name}}ServiceChain, role_data]}
310 # Filter any null/None service_names which may be present due to mapping
311 # of services to OS::Heat::None
312 {{role.name}}ServiceNames:
313 type: OS::Heat::Value
314 depends_on: {{role.name}}ServiceChain
316 type: comma_delimited_list
319 expression: coalesce($.data, []).where($ != null)
320 data: {get_attr: [{{role.name}}ServiceChainRoleData, value, service_names]}
322 {{role.name}}HostsDeployment:
323 type: OS::Heat::StructuredDeployments
325 name: {{role.name}}HostsDeployment
326 config: {get_attr: [hostsConfig, config_id]}
327 servers: {get_attr: [{{role.name}}Servers, value]}
329 {{role.name}}SshKnownHostsDeployment:
330 type: OS::Heat::StructuredDeployments
332 name: {{role.name}}SshKnownHostsDeployment
333 config: {get_resource: SshKnownHostsConfig}
334 servers: {get_attr: [{{role.name}}Servers, value]}
336 {{role.name}}AllNodesDeployment:
337 type: OS::Heat::StructuredDeployments
339 {% for role_inner in roles %}
340 - {{role_inner.name}}HostsDeployment
343 name: {{role.name}}AllNodesDeployment
344 config: {get_attr: [allNodesConfig, config_id]}
345 servers: {get_attr: [{{role.name}}Servers, value]}
347 # Note we have to use yaql to look up the first hostname/ip in the
348 # list because heat path based attributes operate on the attribute
349 # inside the ResourceGroup, not the exposed list ref discussion in
350 # https://bugs.launchpad.net/heat/+bug/1640488
351 # The coalesce is needed because $.data is None during heat validation
354 expression: coalesce($.data, []).first(null)
355 data: {get_attr: [{{role.name}}, hostname]}
358 expression: coalesce($.data, []).first(null)
359 data: {get_attr: [{{role.name}}, ip_address]}
361 {{role.name}}AllNodesValidationDeployment:
362 type: OS::Heat::StructuredDeployments
363 depends_on: {{role.name}}AllNodesDeployment
365 name: {{role.name}}AllNodesValidationDeployment
366 config: {get_resource: AllNodesValidationConfig}
367 servers: {get_attr: [{{role.name}}Servers, value]}
369 {{role.name}}IpListMap:
370 type: OS::TripleO::Network::Ports::NetIpListMap
372 ControlPlaneIpList: {get_attr: [{{role.name}}, ip_address]}
373 ExternalIpList: {get_attr: [{{role.name}}, external_ip_address]}
374 InternalApiIpList: {get_attr: [{{role.name}}, internal_api_ip_address]}
375 StorageIpList: {get_attr: [{{role.name}}, storage_ip_address]}
376 StorageMgmtIpList: {get_attr: [{{role.name}}, storage_mgmt_ip_address]}
377 TenantIpList: {get_attr: [{{role.name}}, tenant_ip_address]}
378 ManagementIpList: {get_attr: [{{role.name}}, management_ip_address]}
379 EnabledServices: {get_attr: [{{role.name}}ServiceNames, value]}
380 ServiceNetMap: {get_attr: [ServiceNetMap, service_net_map_lower]}
381 ServiceHostnameList: {get_attr: [{{role.name}}, hostname]}
382 NetworkHostnameMap: {get_attr: [{{role.name}}NetworkHostnameMap, value]}
384 {{role.name}}NetworkHostnameMap:
385 type: OS::Heat::Value
389 # Note (shardy) this somewhat complex yaql may be replaced
390 # with a map_deep_merge function in ocata. It merges the
391 # list of maps, but appends to colliding lists so we can
392 # create a map of lists for all nodes for each network
394 expression: dict($.data.where($ != null).flatten().selectMany($.items()).groupBy($[0], $[1], [$[0], $[1].flatten()]))
396 - {get_attr: [{{role.name}}, hostname_map]}
399 type: OS::Heat::ResourceGroup
403 max_batch_size: {get_param: NodeCreateBatchSize}
405 count: {get_param: {{role.name}}Count}
406 removal_policies: {get_param: {{role.name}}RemovalPolicies}
408 type: OS::TripleO::{{role.name}}
410 CloudDomain: {get_param: CloudDomain}
411 ServiceNetMap: {get_attr: [ServiceNetMap, service_net_map]}
412 EndpointMap: {get_attr: [EndpointMap, endpoint_map]}
415 template: {get_param: {{role.name}}HostnameFormat}
417 '%stackname%': {get_param: 'OS::stack_name'}
419 {% if role.name != 'Compute' %}
420 {{role.name}}SchedulerHints: {get_param: {{role.name}}SchedulerHints}
422 NovaComputeSchedulerHints: {get_param: NovaComputeSchedulerHints}
424 ServiceConfigSettings:
426 - get_attr: [{{role.name}}ServiceChainRoleData, value, config_settings]
428 - get_attr: [{{r.name}}ServiceChain, role_data, global_config_settings]
430 # This next step combines two yaql passes:
431 # - The inner one does a deep merge on the service_config_settings for all roles
432 # - The outer one filters the map based on the services enabled for the role
433 # then merges the result into one map.
435 expression: let(root => $) -> $.data.map.items().where($[0] in coalesce($root.data.services, [])).select($[1]).reduce($1.mergeWith($2), {})
439 expression: $.data.where($ != null).reduce($1.mergeWith($2), {})
442 - get_attr: [{{r.name}}ServiceChain, role_data, service_config_settings]
444 services: {get_attr: [{{role.name}}ServiceNames, value]}
445 ServiceNames: {get_attr: [{{role.name}}ServiceNames, value]}
446 MonitoringSubscriptions: {get_attr: [{{role.name}}ServiceChainRoleData, value, monitoring_subscriptions]}
447 ServiceMetadataSettings: {get_attr: [{{role.name}}ServiceChainRoleData, value, service_metadata_settings]}
448 DeploymentServerBlacklistDict: {get_attr: [DeploymentServerBlacklistDict, value]}
451 {% for role in roles %}
452 {{role.name}}Servers:
453 type: OS::Heat::Value
454 depends_on: {{role.name}}
459 expression: let(servers=>switch(isDict($.data.servers) => $.data.servers, true => {})) -> $servers.deleteAll($servers.keys().where($servers[$] = null))
461 servers: {get_attr: [{{role.name}}, attributes, nova_server_resource]}
464 # This resource just creates a dict out of the DeploymentServerBlacklist,
465 # which is a list. The dict is used in the role templates to set a condition
466 # on whether to create the deployment resources. We can't use the list
467 # directly because there is no way to ask Heat if a list contains a specific
469 DeploymentServerBlacklistDict:
470 type: OS::Heat::Value
479 hostname: {get_param: DeploymentServerBlacklist}
482 type: OS::TripleO::Hosts::SoftwareConfig
488 - add_vips_to_etc_hosts
489 - {get_attr: [VipHosts, value]}
492 {% for role in roles %}
495 - {get_attr: [{{role.name}}, hosts_entry]}
499 type: OS::TripleO::AllNodes::SoftwareConfig
501 cloud_name_external: {get_param: CloudName}
502 cloud_name_internal_api: {get_param: CloudNameInternal}
503 cloud_name_storage: {get_param: CloudNameStorage}
504 cloud_name_storage_mgmt: {get_param: CloudNameStorageManagement}
505 cloud_name_ctlplane: {get_param: CloudNameCtlplane}
509 {% for role in roles %}
510 - {get_attr: [{{role.name}}ServiceNames, value]}
515 $.data.groups.flatten()
518 {% for role in roles %}
519 - {get_attr: [{{role.name}}ServiceChainRoleData, value, logging_groups]}
524 $.data.sources.flatten()
527 {% for role in roles %}
528 - {get_attr: [{{role.name}}ServiceChainRoleData, value, logging_sources]}
530 controller_ips: {get_attr: [{{primary_role_name}}, ip_address]}
531 controller_names: {get_attr: [{{primary_role_name}}, hostname]}
533 # Note (shardy) this somewhat complex yaql may be replaced
534 # with a map_deep_merge function in ocata. It merges the
535 # list of maps, but appends to colliding lists when a service
536 # is deployed on more than one role
538 expression: dict($.data.l.where($ != null).selectMany($.items()).groupBy($[0], $[1], [$[0], $[1].flatten()]))
541 {% for role in roles %}
542 - {get_attr: [{{role.name}}IpListMap, service_ips]}
546 expression: dict($.data.l.where($ != null).selectMany($.items()).groupBy($[0], $[1], [$[0], $[1].flatten()]))
549 {% for role in roles %}
550 - {get_attr: [{{role.name}}IpListMap, service_hostnames]}
552 short_service_node_names:
554 expression: dict($.data.l.where($ != null).selectMany($.items()).groupBy($[0], $[1], [$[0], $[1].flatten()]))
557 {% for role in roles %}
558 - {get_attr: [{{role.name}}IpListMap, short_service_hostnames]}
560 short_service_bootstrap_node:
562 expression: dict($.data.l.where($ != null).selectMany($.items()).groupBy($[0], $[1], [$[0], $[1].flatten().first()]))
565 {% for role in roles %}
566 - {get_attr: [{{role.name}}IpListMap, short_service_bootstrap_hostnames]}
568 # FIXME(shardy): These require further work to move into service_ips
569 memcache_node_ips: {get_attr: [{{primary_role_name}}IpListMap, net_ip_map, {get_attr: [ServiceNetMap, service_net_map, MemcachedNetwork]}]}
570 NetVipMap: {get_attr: [VipMap, net_ip_map]}
571 RedisVirtualIP: {get_attr: [RedisVirtualIP, ip_address]}
572 ServiceNetMap: {get_attr: [ServiceNetMap, service_net_map_lower]}
573 DeployIdentifier: {get_param: DeployIdentifier}
574 UpdateIdentifier: {get_param: UpdateIdentifier}
577 type: OS::Heat::RandomString
582 type: OS::Heat::RandomString
585 salt: {get_param: RabbitCookieSalt}
588 type: OS::TripleO::DefaultPasswords
590 DefaultMysqlRootPassword: {get_attr: [MysqlRootPassword, value]}
591 DefaultRabbitCookie: {get_attr: [RabbitCookie, value]}
592 DefaultHeatAuthEncryptionKey: {get_attr: [HeatAuthEncryptionKey, value]}
593 DefaultPcsdPassword: {get_attr: [PcsdPassword, value]}
594 DefaultHorizonSecret: {get_attr: [HorizonSecret, value]}
596 # creates the network architecture
598 type: OS::TripleO::Network
601 type: OS::TripleO::Network::Ports::ControlPlaneVipPort
604 name: control_virtual_ip
605 network: {get_param: NeutronControlPlaneID}
606 fixed_ips: {get_param: ControlFixedIPs}
607 replacement_policy: AUTO
611 type: OS::TripleO::Network::Ports::RedisVipPort
613 ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
614 ControlPlaneNetwork: {get_param: NeutronControlPlaneID}
615 PortName: redis_virtual_ip
616 NetworkName: {get_attr: [ServiceNetMap, service_net_map, RedisNetwork]}
618 FixedIPs: {get_param: RedisVirtualFixedIPs}
620 # The public VIP is on the External net, falls back to ctlplane
623 type: OS::TripleO::Network::Ports::ExternalVipPort
625 ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
626 ControlPlaneNetwork: {get_param: NeutronControlPlaneID}
627 PortName: public_virtual_ip
628 FixedIPs: {get_param: PublicVirtualFixedIPs}
630 InternalApiVirtualIP:
632 type: OS::TripleO::Network::Ports::InternalApiVipPort
634 ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
635 PortName: internal_api_virtual_ip
636 FixedIPs: {get_param: InternalApiVirtualFixedIPs}
640 type: OS::TripleO::Network::Ports::StorageVipPort
642 ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
643 PortName: storage_virtual_ip
644 FixedIPs: {get_param: StorageVirtualFixedIPs}
646 StorageMgmtVirtualIP:
648 type: OS::TripleO::Network::Ports::StorageMgmtVipPort
650 ControlPlaneIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
651 PortName: storage_management_virtual_ip
652 FixedIPs: {get_param: StorageMgmtVirtualFixedIPs}
655 type: OS::TripleO::Network::Ports::NetVipMap
657 ControlPlaneIp: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
658 ExternalIp: {get_attr: [PublicVirtualIP, ip_address]}
659 ExternalIpUri: {get_attr: [PublicVirtualIP, ip_address_uri]}
660 InternalApiIp: {get_attr: [InternalApiVirtualIP, ip_address]}
661 InternalApiIpUri: {get_attr: [InternalApiVirtualIP, ip_address_uri]}
662 StorageIp: {get_attr: [StorageVirtualIP, ip_address]}
663 StorageIpUri: {get_attr: [StorageVirtualIP, ip_address_uri]}
664 StorageMgmtIp: {get_attr: [StorageMgmtVirtualIP, ip_address]}
665 StorageMgmtIpUri: {get_attr: [StorageMgmtVirtualIP, ip_address_uri]}
666 # No tenant or management VIP required
668 # All Nodes Validations
669 AllNodesValidationConfig:
670 type: OS::TripleO::AllNodes::Validation
676 expression: coalesce($.data, []).first(null)
677 data: {get_attr: [{{primary_role_name}}, external_ip_address]}
679 expression: coalesce($.data, []).first(null)
680 data: {get_attr: [{{primary_role_name}}, internal_api_ip_address]}
682 expression: coalesce($.data, []).first(null)
683 data: {get_attr: [{{primary_role_name}}, storage_ip_address]}
685 expression: coalesce($.data, []).first(null)
686 data: {get_attr: [{{primary_role_name}}, storage_mgmt_ip_address]}
688 expression: coalesce($.data, []).first(null)
689 data: {get_attr: [{{primary_role_name}}, tenant_ip_address]}
691 expression: coalesce($.data, []).first(null)
692 data: {get_attr: [{{primary_role_name}}, management_ip_address]}
695 type: OS::TripleO::Tasks::UpdateWorkflow
697 {% for role in roles %}
698 - {{role.name}}AllNodesDeployment
702 {% for role in roles %}
703 {{role.name}}: {get_attr: [{{role.name}}Servers, value]}
706 deploy_identifier: {get_param: DeployIdentifier}
707 update_identifier: {get_param: UpdateIdentifier}
709 # Optional ExtraConfig for all nodes - all roles are passed in here, but
710 # the nested template may configure each role differently (or not at all)
712 type: OS::TripleO::AllNodesExtraConfig
715 {% for role in roles %}
716 - {{role.name}}AllNodesValidationDeployment
720 {% for role in roles %}
721 {{role.name}}: {get_attr: [{{role.name}}Servers, value]}
724 # Post deployment steps for all roles
726 type: OS::TripleO::PostDeploySteps
728 - AllNodesExtraConfig
729 {% for role in roles %}
730 - {{role.name}}AllNodesDeployment
734 {% for role in roles %}
735 {{role.name}}: {get_attr: [{{role.name}}Servers, value]}
737 EndpointMap: {get_attr: [EndpointMap, endpoint_map]}
739 {% for role in roles %}
740 {{role.name}}: {get_attr: [{{role.name}}ServiceChainRoleData, value]}
745 description: Asserts that the keystone endpoints have been provisioned.
748 description: URL for the Overcloud Keystone service
749 value: {get_attr: [EndpointMapData, value, KeystonePublic, uri]}
751 description: Keystone Admin VIP endpoint
752 value: {get_attr: [VipMap, net_ip_map, {get_attr: [ServiceNetMap, service_net_map, KeystoneAdminApiNetwork]}]}
755 Mapping of the resources with the needed info for their endpoints.
756 This includes the protocol used, the IP, port and also a full
757 representation of the URI.
758 value: {get_attr: [EndpointMapData, value]}
761 The content that should be appended to your /etc/hosts if you want to get
762 hostname-based access to the deployed nodes (useful for testing without
767 - - {get_attr: [hostsConfig, hosts_entries]}
768 - - {get_attr: [VipHosts, value]}
770 description: The services enabled on each role
772 {% for role in roles %}
773 {{role.name}}: {get_attr: [{{role.name}}ServiceNames, value]}
776 description: The configuration data associated with each role
778 {% for role in roles %}
779 {{role.name}}: {get_attr: [{{role.name}}ServiceChainRoleData, value]}
782 description: Mapping of each network to a list of IPs for each role
784 {% for role in roles %}
785 {{role.name}}: {get_attr: [{{role.name}}IpListMap, net_ip_map]}
788 description: Mapping of each network to a list of hostnames for each role
790 {% for role in roles %}
791 {{role.name}}: {get_attr: [{{role.name}}NetworkHostnameMap, value]}