[KUBEV] Deploy a dedicated node for test usage (tsrv).

Related: KUBEV-521
Change-Id: Ide3fe1c4560440b23df38ceb2e76a77da0b8fd0d
diff --git a/hco/env/ctrl3-wrkr3.yaml b/hco/env/ctrl3-wrkr3.yaml
index 61bf6c2..c69ee9a 100644
--- a/hco/env/ctrl3-wrkr3.yaml
+++ b/hco/env/ctrl3-wrkr3.yaml
@@ -5,6 +5,7 @@
 parameters:
   controllers_size: 3
   workers_size: 3
+  tsrv_enable: true
   image: ubuntu-24.04-server-cloudimg-amd64-20250805
   public_net_id: public
   cluster_public_key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCp0evjOaK8c8SKYK4r2+0BN7g+8YSvQ2n8nFgOURCyvkJqOHi1qPGZmuN0CclYVdVuZiXbWw3VxRbSW3EH736VzgY1U0JmoTiSamzLHaWsXvEIW8VCi7boli539QJP0ikJiBaNAgZILyCrVPN+A6mfqtacs1KXdZ0zlMq1BPtFciR1JTCRcVs5vP2Wwz5QtY2jMIh3aiwkePjMTQPcfmh1TkOlxYu5IbQyZ3G1ahA0mNKI9a0dtF282av/F6pwB/N1R1nEZ/9VtcN2I1mf1NW/tTHEEcTzXYo1R/8K9vlqAN8QvvGLZtZduGviNVNoNWvoxaXxDt8CPv2B2NCdQFZp
diff --git a/hco/fragments/VMCompute.yaml b/hco/fragments/VMCompute.yaml
index e7f44c8..a3b41c0 100644
--- a/hco/fragments/VMCompute.yaml
+++ b/hco/fragments/VMCompute.yaml
@@ -28,12 +28,6 @@
   key_name:
     type: string
     description: Name of keypair to assign to servers
-  k8s_vip:
-    type: string
-    description: VIP of kubernetes (child cluster)
-  k8s_svc_network_cidr:
-    type: string
-    description: CIDR of kubernetes service network
 
 resources:
 
@@ -99,7 +93,6 @@
           - virtinst
           - python3-virtualbmc
           - ipmitool
-          - docker.io
         write_files:
           - path: /etc/systemd/system/virtualbmc.service
             content: |
@@ -124,14 +117,9 @@
                       br_pxe:
                         interfaces: [ens4]
                         addresses: [ip_addr/ip_mask]
-                        routes:
-                          - to: k8s_svc_network_cidr
-                            via: k8s_vip
                 params:
                   ip_addr: { get_attr: [ ip_addr_pxe, value ] }
                   ip_mask: { get_attr: [ ip_mask_pxe, value ] }
-                  k8s_vip: { get_param: k8s_vip }
-                  k8s_svc_network_cidr: { get_param: k8s_svc_network_cidr }
         runcmd:
           - str_replace:
               template: |
diff --git a/hco/fragments/VMInstance.yaml b/hco/fragments/VMInstance.yaml
index 53e0660..40be33b 100644
--- a/hco/fragments/VMInstance.yaml
+++ b/hco/fragments/VMInstance.yaml
@@ -101,3 +101,6 @@
   server_public_ip:
     description: Floating IP address of server in public network
     value: { get_attr: [ floating_ip_k8s_net, floating_ip_address ] }
+  server_k8s_ipv4:
+    description: List of assigned network addresses
+    value: { get_attr: [ k8s_network_port, fixed_ips, 0, ip_address ] }
diff --git a/hco/fragments/VMInstanceCeph.yaml b/hco/fragments/VMInstanceCeph.yaml
index 61cbd9e..48d1860 100644
--- a/hco/fragments/VMInstanceCeph.yaml
+++ b/hco/fragments/VMInstanceCeph.yaml
@@ -211,3 +211,6 @@
   wc_data:
     description: Metadata from instance
     value: { get_attr: [wait_condition, data]}
+  server_k8s_ipv4:
+    description: List of assigned network addresses
+    value: { get_attr: [ k8s_network_port, fixed_ips, 0, ip_address ] }
diff --git a/hco/fragments/VMTestSrv.yaml b/hco/fragments/VMTestSrv.yaml
new file mode 100644
index 0000000..a056231
--- /dev/null
+++ b/hco/fragments/VMTestSrv.yaml
@@ -0,0 +1,112 @@
+heat_template_version: queens
+
+parameters:
+
+  k8s_network:
+    type: string
+  k8s_subnet_id:
+    type: string
+  public_net_id:
+    type: string
+  availability_zone:
+    type: string
+    default: nova
+  boot_timeout:
+    type: number
+    description: Boot timeout for instance
+    default: 450
+  image:
+    type: string
+    description: Name of image to use for servers
+  flavor:
+    type: string
+    description: Flavor to use for servers
+  key_name:
+    type: string
+    description: Name of keypair to assign to servers
+  k8s_vip:
+    type: string
+    description: VIP of kubernetes (child cluster)
+  k8s_svc_network_cidr:
+    type: string
+    description: CIDR of kubernetes service network
+
+resources:
+
+  k8s_network_port:
+    type: OS::Neutron::Port
+    properties:
+      network: { get_param: k8s_network }
+      port_security_enabled: false
+      fixed_ips:
+        - subnet: { get_param: k8s_subnet_id }
+
+  floating_ip_k8s_net:
+    type: OS::Neutron::FloatingIP
+    properties:
+      floating_network_id: { get_param: public_net_id }
+      port_id: { get_resource: k8s_network_port }
+
+  wait_handle:
+    type: OS::Heat::WaitConditionHandle
+
+  wait_condition:
+    type: OS::Heat::WaitCondition
+    properties:
+      handle: { get_resource: wait_handle }
+      timeout: { get_param: boot_timeout }
+
+  server_init:
+    type: OS::Heat::CloudConfig
+    properties:
+      cloud_config:
+        password: 'r00tme'
+        chpasswd:
+          expire: false
+        ssh_pwauth: true
+        packages:
+          - python3
+          - docker.io
+        write_files:
+          - path: /etc/netplan/98-custom.yaml
+            permissions: '0600'
+            content:
+              str_replace:
+                template: |
+                  network:
+                    version: 2
+                    ethernets:
+                      ens3:
+                        routes:
+                          - to: k8s_svc_network_cidr
+                            via: k8s_vip
+                params:
+                  k8s_vip: { get_param: k8s_vip }
+                  k8s_svc_network_cidr: { get_param: k8s_svc_network_cidr }
+        runcmd:
+          - str_replace:
+              template: |
+                #!/bin/bash
+                set +x
+                netplan apply
+                # Simple success signal
+                wc_notify --data-binary '{"status": "SUCCESS"}'
+              params:
+                wc_notify: { get_attr: [ wait_handle, curl_cli ] }
+
+  server:
+    type: OS::Nova::Server
+    properties:
+      availability_zone: { get_param: availability_zone }
+      image: { get_param: image }
+      flavor: { get_param: flavor }
+      key_name: { get_param: key_name }
+      networks:
+        - port: { get_resource: k8s_network_port }
+      user_data_format: RAW
+      user_data: { get_resource: server_init }
+
+outputs:
+  server_public_ip:
+    description: Floating IP address of server in public network
+    value: { get_attr: [ floating_ip_k8s_net, floating_ip_address ] }
diff --git a/hco/top.yaml b/hco/top.yaml
index 876fd9a..9060e25 100644
--- a/hco/top.yaml
+++ b/hco/top.yaml
@@ -67,6 +67,20 @@
     type: number
     description: Boot timeout for instance
     default: 600
+  tsrv_enable:
+    type: boolean
+    description: Deploy node for test server
+    default: false
+  # Test node parameters
+  tsrv_flavor:
+    type: string
+    default: 'system.compact.openstack.control'
+  k8s_vip:
+    type: string
+    default: ''
+  k8s_svc_network_cidr:
+    type: string
+    default: '10.96.0.0/12'
   # Hybrid lab parameters
   hybrid_lab:
     type: boolean
@@ -82,17 +96,14 @@
   pxe_subnet:
     type: string
     default: ''
-  k8s_vip:
-    type: string
-    default: ''
-  k8s_svc_network_cidr:
-    type: string
-    default: ''
 
 conditions:
 
+  deploy_test_server:
+    get_param: tsrv_enable
   deploy_vm_compute:
     get_param: hybrid_lab
+  is_k8s_vip_empty: { equals: [ { get_param: k8s_vip }, '' ] }
 
 resources:
 
@@ -224,6 +235,30 @@
           pxe_network: { get_param: pxe_network }
           pxe_subnet: { get_param: pxe_subnet }
 
+  tsrv:
+    type: ./fragments/VMTestSrv.yaml
+    condition: deploy_test_server
+    depends_on:
+      - masters
+      - k8s_network
+      - public_router_iface
+    properties:
+      k8s_network: { get_resource: k8s_network }
+      k8s_subnet_id: { get_resource: k8s_subnet }
+      public_net_id: { get_param: public_net_id }
+      image: { get_param: image }
+      flavor: { get_param: tsrv_flavor }
+      key_name: { get_attr: [ keypair_name, value ] }
+      k8s_vip:
+        if:
+          - is_k8s_vip_empty
+          # WA for https://bugs.launchpad.net/heat/+bug/1640488
+          - yaql:
+              expression: coalesce($.data, []).first(null)
+              data: { get_attr: [ masters, server_k8s_ipv4 ] }
+          - { get_param: k8s_vip }
+      k8s_svc_network_cidr: { get_param: k8s_svc_network_cidr }
+
   vm_compute:
     type: ./fragments/VMCompute.yaml
     condition: deploy_vm_compute
@@ -240,8 +275,6 @@
       image: { get_param: image }
       flavor: { get_param: vm_compute_flavor }
       key_name: { get_attr: [ keypair_name, value ] }
-      k8s_vip: { get_param: k8s_vip }
-      k8s_svc_network_cidr: { get_param: k8s_svc_network_cidr }
 
 outputs:
   masters_ips:
@@ -262,6 +295,10 @@
   public_router_gw_ipv6:
     description: Public gateway IPv6 address (used for kubevirt tests)
     value: { get_param: k8s_network_ipv6_gw_ip }
+  tsrv_ip:
+    condition: deploy_test_server
+    description: Public IP address of the deployed test server instance
+    value: { get_attr: [ tsrv, server_public_ip ] }
   vm_compute_ip:
     condition: deploy_vm_compute
     description: Public IP address of the deployed compute instance