summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--playbooks/provisioning/openstack/README.md87
-rw-r--r--playbooks/provisioning/openstack/post-provision-openstack.yml12
-rw-r--r--playbooks/provisioning/openstack/prerequisites.yml10
-rw-r--r--playbooks/provisioning/openstack/provision-openstack.yml11
-rw-r--r--playbooks/provisioning/openstack/sample-inventory/clouds.yaml5
-rw-r--r--playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml55
-rw-r--r--playbooks/provisioning/openstack/stack_params.yaml36
-rw-r--r--roles/dns-records/tasks/main.yml (renamed from playbooks/provisioning/openstack/openstack_dns_records.yml)7
-rw-r--r--roles/dns-views/tasks/main.yml (renamed from playbooks/provisioning/openstack/openstack_dns_views.yml)0
-rw-r--r--roles/openstack-stack/defaults/main.yml6
-rw-r--r--roles/openstack-stack/tasks/main.yml9
-rw-r--r--roles/openstack-stack/templates/heat_stack.yaml.j279
-rw-r--r--roles/openstack-stack/templates/heat_stack_server.yaml.j23
-rw-r--r--roles/openstack-stack/templates/heat_stack_server_nofloating.yaml.j2152
-rw-r--r--roles/static_inventory/defaults/main.yml21
-rw-r--r--roles/static_inventory/tasks/main.yml11
-rw-r--r--roles/static_inventory/tasks/openstack.yml44
-rw-r--r--roles/static_inventory/tasks/sshconfig.yml13
-rw-r--r--roles/static_inventory/tasks/sshtun.yml15
-rw-r--r--roles/static_inventory/templates/inventory.j25
-rw-r--r--roles/static_inventory/templates/openstack_ssh_config.j221
-rw-r--r--roles/static_inventory/templates/ssh-tunnel.service.j220
22 files changed, 553 insertions, 69 deletions
diff --git a/playbooks/provisioning/openstack/README.md b/playbooks/provisioning/openstack/README.md
index 1ff586b49..ae572f9b6 100644
--- a/playbooks/provisioning/openstack/README.md
+++ b/playbooks/provisioning/openstack/README.md
@@ -10,6 +10,7 @@ etc.). The result is an environment ready for openshift-ansible.
* [Ansible-galaxy](https://pypi.python.org/pypi/ansible-galaxy-local-deps)
* [jinja2](http://jinja.pocoo.org/docs/2.9/)
* [shade](https://pypi.python.org/pypi/shade)
+* python-jmespath / [jmespath](https://pypi.python.org/pypi/jmespath)
* python-dns / [dnspython](https://pypi.python.org/pypi/dnspython)
* Become (sudo) is not required.
@@ -40,7 +41,7 @@ Alternatively you can install directly from github:
-p openshift-ansible-contrib/roles
Notes:
-* This assumes we're in the directory that contains the clonned
+* This assumes we're in the directory that contains the clonned
openshift-ansible-contrib repo in its root path.
* When trying to install a different version, the previous one must be removed first
(`infra-ansible` directory from [roles](https://github.com/openshift/openshift-ansible-contrib/tree/master/roles)).
@@ -62,10 +63,6 @@ Otherwise, even if there are differences between the two versions, installation
cp -r openshift-ansible-contrib/playbooks/provisioning/openstack/sample-inventory inventory
-### Copy clouds.yaml
-
- cp openshift-ansible-contrib/playbooks/provisioning/openstack/sample-inventory/clouds.yaml clouds.yaml
-
### Copy ansible config
cp openshift-ansible-contrib/playbooks/provisioning/openstack/sample-inventory/ansible.cfg ansible.cfg
@@ -81,6 +78,10 @@ your servers will be under. With the default values, this will be
That sudomain can be set as well by the `openshift_app_domain` variable in
the inventory.
+The `openstack_<role name>_hostname` is a set of variables used for customising
+hostnames of servers with a given role. When such a variable stays commented,
+default hostname (usually the role name) is used.
+
The `public_dns_nameservers` is a list of DNS servers accessible from all
the created Nova servers. These will be serving as your DNS forwarders for
external FQDNs that do not belong to the cluster's DNS domain and its subdomains.
@@ -101,18 +102,29 @@ as a default nameserver that comes from the NetworkManager and cloud-init.
`openstack keypair list`. This guide assumes that its corresponding private
key is `~/.ssh/openshift`, stored on the ansible admin (control) node.
-`openstack_default_image_name` is the name of the Glance image the
-servers will use. You can
-see your images with `openstack image list`.
+`openstack_default_image_name` is the default name of the Glance image the
+servers will use. You can see your images with `openstack image list`.
+In order to set a different image for a role, uncomment the line with the
+corresponding variable (e.g. `openstack_lb_image_name` for load balancer) and
+set its value to another available image name. `openstack_default_image_name`
+must stay defined as it is used as a default value for the rest of the roles.
-`openstack_default_flavor` is the Nova flavor the servers will use.
+`openstack_default_flavor` is the default Nova flavor the servers will use.
You can see your flavors with `openstack flavor list`.
+In order to set a different flavor for a role, uncomment the line with the
+corresponding variable (e.g. `openstack_lb_flavor` for load balancer) and
+set its value to another available flavor. `openstack_default_flavor` must
+stay defined as it is used as a default value for the rest of the roles.
`openstack_external_network_name` is the name of the Neutron network
providing external connectivity. It is often called `public`,
`external` or `ext-net`. You can see your networks with `openstack
network list`.
+`openstack_private_network_name` is the name of the private Neutron network
+providing admin/control access for ansible. It can be merged with other
+cluster networks, there are no special requirements for networking.
+
The `openstack_num_masters`, `openstack_num_infra` and
`openstack_num_nodes` values specify the number of Master, Infra and
App nodes to create.
@@ -123,6 +135,16 @@ cluster node groups, like app or infra nodes. For example: `{'region': 'infra'}`
The `openstack_nodes_to_remove` allows you to specify the numerical indexes
of App nodes that should be removed; for example, ['0', '2'],
+The `docker_volume_size` is the default Docker volume size the servers will use.
+In order to set a different volume size for a role,
+uncomment the line with the corresponding variable (e. g. `docker_master_volume_size`
+for master) and change its value. `docker_volume_size` must stay defined as it is
+used as a default value for some of the servers (master, infra, app node).
+The rest of the roles (etcd, load balancer, dns) have their defaults hard-coded.
+
+**Note**: If the `ephemeral_volumes` is set to `true`, the `*_volume_size` variables
+will be ignored and the deployment will not create any cinder volumes.
+
The `openstack_flat_secgrp`, controls Neutron security groups creation for Heat
stacks. Set it to true, if you experience issues with sec group rules
quotas. It trades security for number of rules, by sharing the same set
@@ -177,16 +199,30 @@ variables for the `inventory/group_vars/OSEv3.yml`, `all.yml`:
origin_release: 1.5.1
openshift_deployment_type: "{{ deployment_type }}"
-### Configure static inventory
+### Configure static inventory and access via a bastion node
Example inventory variables:
+ openstack_use_bastion: true
+ bastion_ingress_cidr: "{{openstack_subnet_prefix}}.0/24"
openstack_private_ssh_key: ~/.ssh/openshift
openstack_inventory: static
openstack_inventory_path: ../../../../inventory
+ openstack_ssh_config_path: /tmp/ssh.config.openshift.ansible.openshift.example.com
+
+The `openstack_subnet_prefix` is the openstack private network for your cluster.
+And the `bastion_ingress_cidr` defines accepted range for SSH connections to nodes
+additionally to the `ssh_ingress_cidr`` (see the security notes above).
+
+The SSH config will be stored on the ansible control node by the
+gitven path. Ansible uses it automatically. To access the cluster nodes with
+that ssh config, use the `-F` prefix, f.e.:
+ ssh -F /tmp/ssh.config.openshift.ansible.openshift.example.com master-0.openshift.example.com echo OK
-In this guide, the latter points to the current directory, where you run ansible commands
+Note, relative paths will not work for the `openstack_ssh_config_path`, but it
+works for the `openstack_private_ssh_key` and `openstack_inventory_path`. In this
+guide, the latter points to the current directory, where you run ansible commands
from.
To verify nodes connectivity, use the command:
@@ -194,7 +230,7 @@ To verify nodes connectivity, use the command:
ansible -v -i inventory/hosts -m ping all
If something is broken, double-check the inventory variables, paths and the
-generated `<openstack_inventory_path>/hosts` file.
+generated `<openstack_inventory_path>/hosts` and `openstack_ssh_config_path` files.
The `inventory: dynamic` can be used instead to access cluster nodes directly via
floating IPs. In this mode you can not use a bastion node and should specify
@@ -213,6 +249,15 @@ this is how you stat the provisioning process from your ansible control node:
Note, here you start with an empty inventory. The static inventory will be populated
with data so you can omit providing additional arguments for future ansible commands.
+If bastion enabled, the generates SSH config must be applied for ansible.
+Otherwise, it is auto included by the previous step. In order to execute it
+as a separate playbook, use the following command:
+
+ ansible-playbook openshift-ansible-contrib/playbooks/provisioning/openstack/post-provision-openstack.yml
+
+The first infra node then becomes a bastion node as well and proxies access
+for future ansible commands. The post-provision step also configures Satellite,
+if requested, and DNS server, and ensures other OpenShift requirements to be met.
### Install OpenShift
@@ -220,6 +265,24 @@ Once it succeeds, you can install openshift by running:
ansible-playbook openshift-ansible/playbooks/byo/config.yml
+### Access UI
+
+OpenShift UI may be accessed via the 1st master node FQDN, port 8443.
+
+When using a bastion, you may want to make an SSH tunnel from your control node
+to access UI on the `https://localhost:8443`, with this inventory variable:
+
+ openshift_ui_ssh_tunnel: True
+
+Note, this requires sudo rights on the ansible control node and an absolute path
+for the `openstack_private_ssh_key`. You should also update the control node's
+`/etc/hosts`:
+
+ 127.0.0.1 master-0.openshift.example.com
+
+In order to access UI, the ssh-tunnel service will be created and started on the
+control node. Make sure to remove these changes and the service manually, when not
+needed anymore.
## License
diff --git a/playbooks/provisioning/openstack/post-provision-openstack.yml b/playbooks/provisioning/openstack/post-provision-openstack.yml
index a807c4d2f..f683b77be 100644
--- a/playbooks/provisioning/openstack/post-provision-openstack.yml
+++ b/playbooks/provisioning/openstack/post-provision-openstack.yml
@@ -4,7 +4,11 @@
become: False
gather_facts: False
tasks:
- - wait_for_connection:
+ - when: not openstack_use_bastion|default(False)|bool
+ wait_for_connection:
+ - when: openstack_use_bastion|default(False)|bool
+ delegate_to: bastion
+ wait_for_connection:
- hosts: cluster_hosts
gather_facts: True
@@ -48,9 +52,8 @@
become: true
pre_tasks:
- include: pre_tasks.yml
- - name: "Generate dns-server views"
- include: openstack_dns_views.yml
roles:
+ - role: dns-views
- role: infra-ansible/roles/dns-server
- name: Build and process DNS Records
@@ -59,9 +62,8 @@
become: False
pre_tasks:
- include: pre_tasks.yml
- - name: "Generate dns records"
- include: openstack_dns_records.yml
roles:
+ - role: dns-records
- role: infra-ansible/roles/dns
- name: Switch the stack subnet to the configured private DNS server
diff --git a/playbooks/provisioning/openstack/prerequisites.yml b/playbooks/provisioning/openstack/prerequisites.yml
index 71a99fc82..dd4f980b2 100644
--- a/playbooks/provisioning/openstack/prerequisites.yml
+++ b/playbooks/provisioning/openstack/prerequisites.yml
@@ -20,6 +20,16 @@
that: 'shade_result.rc == 0'
msg: "Python module shade is not installed"
+ # Check jmespath
+ - name: Try to import python module shade
+ command: python -c "import jmespath"
+ ignore_errors: yes
+ register: jmespath_result
+ - name: Check if jmespath is installed
+ assert:
+ that: 'jmespath_result.rc == 0'
+ msg: "Python module jmespath is not installed"
+
# Check python-dns
- name: Try to import python DNS module
command: python -c "import dns"
diff --git a/playbooks/provisioning/openstack/provision-openstack.yml b/playbooks/provisioning/openstack/provision-openstack.yml
index 0cac37aaf..6ec944d56 100644
--- a/playbooks/provisioning/openstack/provision-openstack.yml
+++ b/playbooks/provisioning/openstack/provision-openstack.yml
@@ -12,13 +12,20 @@
when: openstack_inventory|default('static') == 'static'
inventory_path: "{{ openstack_inventory_path|default(inventory_dir) }}"
private_ssh_key: "{{ openstack_private_ssh_key|default('~/.ssh/id_rsa') }}"
+ ssh_config_path: "{{ openstack_ssh_config_path|default('/tmp/ssh.config.openshift.ansible' + '.' + stack_name) }}"
+ ssh_user: "{{ ansible_user }}"
-- name: Refresh Server inventory
+- name: Refresh Server inventory or exit to apply SSH config
hosts: localhost
connection: local
become: False
gather_facts: False
tasks:
- - meta: refresh_inventory
+ - name: Exit to apply SSH config for a bastion
+ meta: end_play
+ when: openstack_use_bastion|default(False)|bool
+ - name: Refresh Server inventory
+ meta: refresh_inventory
- include: post-provision-openstack.yml
+ when: not openstack_use_bastion|default(False)|bool
diff --git a/playbooks/provisioning/openstack/sample-inventory/clouds.yaml b/playbooks/provisioning/openstack/sample-inventory/clouds.yaml
deleted file mode 100644
index 8182d2995..000000000
--- a/playbooks/provisioning/openstack/sample-inventory/clouds.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-ansible:
- use_hostnames: True
- expand_hostvars: True
- fail_on_errors: True
diff --git a/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml b/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml
index 9eb36ab13..bdd98d239 100644
--- a/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml
+++ b/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml
@@ -3,18 +3,57 @@ env_id: "openshift"
public_dns_domain: "example.com"
public_dns_nameservers: []
+# # Used Hostnames
+# # - set custom hostnames for roles by uncommenting corresponding lines
+#openstack_master_hostname: "master"
+#openstack_infra_hostname: "infra-node"
+#openstack_node_hostname: "app-node"
+#openstack_lb_hostname: "lb"
+#openstack_etcd_hostname: "etcd"
+#openstack_dns_hostname: "dns"
+
openstack_ssh_public_key: "openshift"
-openstack_default_image_name: "centos7"
-openstack_default_flavor: "m1.medium"
openstack_external_network_name: "public"
+#openstack_private_network_name: "openshift-ansible-{{ stack_name }}-net"
+
+# # Used Images
+# # - set specific images for roles by uncommenting corresponding lines
+# # - note: do not remove openstack_default_image_name definition
+#openstack_master_image_name: "centos7"
+#openstack_infra_image_name: "centos7"
+#openstack_node_image_name: "centos7"
+#openstack_lb_image_name: "centos7"
+#openstack_etcd_image_name: "centos7"
+#openstack_dns_image_name: "centos7"
+openstack_default_image_name: "centos7"
openstack_num_masters: 1
openstack_num_infra: 1
openstack_num_nodes: 2
+# # Used Flavors
+# # - set specific flavors for roles by uncommenting corresponding lines
+# # - note: do note remove openstack_default_flavor definition
+#openstack_master_flavor: "m1.medium"
+#openstack_infra_flavor: "m1.medium"
+#openstack_node_flavor: "m1.medium"
+#openstack_lb_flavor: "m1.medium"
+#openstack_etcd_flavor: "m1.medium"
+#openstack_dns_flavor: "m1.medium"
+openstack_default_flavor: "m1.medium"
+
# # Numerical index of nodes to remove
# openstack_nodes_to_remove: []
+# # Docker volume size
+# # - set specific volume size for roles by uncommenting corresponding lines
+# # - note: do not remove docker_default_volume_size definition
+#docker_master_volume_size: "15"
+#docker_infra_volume_size: "15"
+#docker_node_volume_size: "15"
+#docker_etcd_volume_size: "2"
+#docker_dns_volume_size: "1"
+#docker_lb_volume_size: "5"
docker_volume_size: "15"
openstack_subnet_prefix: "192.168.99"
@@ -69,5 +108,17 @@ ansible_user: openshift
# # The path to checkpoint the static inventory from the in-memory one
#openstack_inventory_path: ../../../../inventory
+# # Use bastion node to access cluster nodes (Defaults to False).
+# # Requires a static inventory.
+#openstack_use_bastion: False
+#bastion_ingress_cidr: "{{openstack_subnet_prefix}}.0/24"
+#
# # The Nova key-pair's private SSH key to access inventory nodes
#openstack_private_ssh_key: ~/.ssh/openshift
+# # The path for the SSH config to access all nodes
+#openstack_ssh_config_path: /tmp/ssh.config.openshift.ansible.{{ env_id }}.{{ public_dns_domain }}
+
+
+# If you want to use the VM storage instead of Cinder volumes, set this to `true`.
+# NOTE: this is for testing only! Your data will be gone once the VM disappears!
+# ephemeral_volumes: false
diff --git a/playbooks/provisioning/openstack/stack_params.yaml b/playbooks/provisioning/openstack/stack_params.yaml
index 9c0b09b45..60e9bcf45 100644
--- a/playbooks/provisioning/openstack/stack_params.yaml
+++ b/playbooks/provisioning/openstack/stack_params.yaml
@@ -3,21 +3,39 @@ stack_name: "{{ env_id }}.{{ public_dns_domain }}"
dns_domain: "{{ public_dns_domain }}"
dns_nameservers: "{{ public_dns_nameservers }}"
subnet_prefix: "{{ openstack_subnet_prefix }}"
+master_hostname: "{{ openstack_master_hostname | default('master') }}"
+infra_hostname: "{{ openstack_infra_hostname | default('infra-node') }}"
+node_hostname: "{{ openstack_node_hostname | default('app-node') }}"
+lb_hostname: "{{ openstack_lb_hostname | default('lb') }}"
+etcd_hostname: "{{ openstack_etcd_hostname | default('etcd') }}"
+dns_hostname: "{{ openstack_dns_hostname | default('dns') }}"
ssh_public_key: "{{ openstack_ssh_public_key }}"
openstack_image: "{{ openstack_default_image_name }}"
-lb_flavor: "{{ openstack_default_flavor | default('m1.small') }}"
-etcd_flavor: "{{ openstack_default_flavor | default('m1.small') }}"
-master_flavor: "{{ openstack_default_flavor | default('m1.medium') }}"
-node_flavor: "{{ openstack_default_flavor | default('m1.medium') }}"
-infra_flavor: "{{ openstack_default_flavor | default('m1.medium') }}"
-dns_flavor: "{{ openstack_default_flavor | default('m1.small') }}"
+lb_flavor: "{{ openstack_lb_flavor | default(openstack_default_flavor) }}"
+etcd_flavor: "{{ openstack_etcd_flavor | default(openstack_default_flavor) }}"
+master_flavor: "{{ openstack_master_flavor | default(openstack_default_flavor) }}"
+node_flavor: "{{ openstack_node_flavor | default(openstack_default_flavor) }}"
+infra_flavor: "{{ openstack_infra_flavor | default(openstack_default_flavor) }}"
+dns_flavor: "{{ openstack_dns_flavor | default(openstack_default_flavor) }}"
+openstack_master_image: "{{ openstack_master_image_name | default(openstack_default_image_name) }}"
+openstack_infra_image: "{{ openstack_infra_image_name | default(openstack_default_image_name) }}"
+openstack_node_image: "{{ openstack_node_image_name | default(openstack_default_image_name) }}"
+openstack_lb_image: "{{ openstack_lb_image_name | default(openstack_default_image_name) }}"
+openstack_etcd_image: "{{ openstack_etcd_image_name | default(openstack_default_image_name) }}"
+openstack_dns_image: "{{ openstack_dns_image_name | default(openstack_default_image_name) }}"
+openstack_private_network: "{{ openstack_private_network_name | default ('openshift-ansible-' + stack_name + '-net') }}"
external_network: "{{ openstack_external_network_name }}"
num_etcd: "{{ openstack_num_etcd | default(0) }}"
num_masters: "{{ openstack_num_masters }}"
num_nodes: "{{ openstack_num_nodes }}"
num_infra: "{{ openstack_num_infra }}"
num_dns: "{{ openstack_num_dns | default(1) }}"
-master_volume_size: "{{ docker_volume_size }}"
-app_volume_size: "{{ docker_volume_size }}"
-infra_volume_size: "{{ docker_volume_size }}"
+master_volume_size: "{{ docker_master_volume_size | default(docker_volume_size) }}"
+infra_volume_size: "{{ docker_infra_volume_size | default(docker_volume_size) }}"
+node_volume_size: "{{ docker_node_volume_size | default(docker_volume_size) }}"
+etcd_volume_size: "{{ docker_etcd_volume_size | default('2') }}"
+dns_volume_size: "{{ docker_dns_volume_size | default('1') }}"
+lb_volume_size: "{{ docker_lb_volume_size | default('5') }}"
nodes_to_remove: "{{ openstack_nodes_to_remove | default([]) | to_yaml }}"
+use_bastion: "{{ openstack_use_bastion|default(False) }}"
+ui_ssh_tunnel: "{{ openshift_ui_ssh_tunnel|default(False) }}"
diff --git a/playbooks/provisioning/openstack/openstack_dns_records.yml b/roles/dns-records/tasks/main.yml
index b5f0840c5..3672a8ea6 100644
--- a/playbooks/provisioning/openstack/openstack_dns_records.yml
+++ b/roles/dns-records/tasks/main.yml
@@ -4,6 +4,11 @@
private_records: "{{ private_records | default([]) + [ { 'type': 'A', 'hostname': hostvars[item]['ansible_hostname'], 'ip': hostvars[item]['private_v4'] } ] }}"
with_items: "{{ groups['cluster_hosts'] }}"
+- name: "Add wildcard records to the private A records for infrahosts"
+ set_fact:
+ private_records: "{{ private_records | default([]) + [ { 'type': 'A', 'hostname': '*.' + openshift_app_domain, 'ip': hostvars[item]['private_v4'] } ] }}"
+ with_items: "{{ groups['infra_hosts'] }}"
+
- name: "Set the private DNS server to use the external value (if provided)"
set_fact:
nsupdate_server_private: "{{ external_nsupdate_keys['private']['server'] }}"
@@ -36,11 +41,13 @@
set_fact:
public_records: "{{ public_records | default([]) + [ { 'type': 'A', 'hostname': hostvars[item]['ansible_hostname'], 'ip': hostvars[item]['public_v4'] } ] }}"
with_items: "{{ groups['cluster_hosts'] }}"
+ when: hostvars[item]['public_v4'] is defined
- name: "Add wildcard records to the public A records"
set_fact:
public_records: "{{ public_records | default([]) + [ { 'type': 'A', 'hostname': '*.' + openshift_app_domain, 'ip': hostvars[item]['public_v4'] } ] }}"
with_items: "{{ groups['infra_hosts'] }}"
+ when: hostvars[item]['public_v4'] is defined
- name: "Set the public DNS server details to use the external value (if provided)"
set_fact:
diff --git a/playbooks/provisioning/openstack/openstack_dns_views.yml b/roles/dns-views/tasks/main.yml
index 7165b4269..7165b4269 100644
--- a/playbooks/provisioning/openstack/openstack_dns_views.yml
+++ b/roles/dns-views/tasks/main.yml
diff --git a/roles/openstack-stack/defaults/main.yml b/roles/openstack-stack/defaults/main.yml
index 4831d6bc4..fbca0bdf6 100644
--- a/roles/openstack-stack/defaults/main.yml
+++ b/roles/openstack-stack/defaults/main.yml
@@ -1,9 +1,9 @@
---
-dns_volume_size: 1
ssh_ingress_cidr: 0.0.0.0/0
node_ingress_cidr: 0.0.0.0/0
master_ingress_cidr: 0.0.0.0/0
lb_ingress_cidr: 0.0.0.0/0
+bastion_ingress_cidr: 0.0.0.0/0
num_etcd: 0
num_masters: 1
num_nodes: 1
@@ -11,3 +11,7 @@ num_dns: 1
num_infra: 1
nodes_to_remove: []
etcd_volume_size: 2
+dns_volume_size: 1
+lb_volume_size: 5
+use_bastion: False
+ui_ssh_tunnel: False
diff --git a/roles/openstack-stack/tasks/main.yml b/roles/openstack-stack/tasks/main.yml
index a53e6350b..9b4855294 100644
--- a/roles/openstack-stack/tasks/main.yml
+++ b/roles/openstack-stack/tasks/main.yml
@@ -8,7 +8,6 @@
- name: set template paths
set_fact:
stack_template_path: "{{ stack_template_pre.path }}/stack.yaml"
- server_template_path: "{{ stack_template_pre.path }}/server.yaml"
user_data_template_path: "{{ stack_template_pre.path }}/user-data"
- name: generate HOT stack template from jinja2 template
@@ -19,7 +18,13 @@
- name: generate HOT server template from jinja2 template
template:
src: heat_stack_server.yaml.j2
- dest: "{{ server_template_path }}"
+ dest: "{{ stack_template_pre.path }}/server.yaml"
+
+- name: generate HOT server w/o floating IPs template from jinja2 template
+ template:
+ src: heat_stack_server_nofloating.yaml.j2
+ dest: "{{ stack_template_pre.path }}/server_nofloating.yaml"
+ when: use_bastion|bool
- name: generate user_data from jinja2 template
template:
diff --git a/roles/openstack-stack/templates/heat_stack.yaml.j2 b/roles/openstack-stack/templates/heat_stack.yaml.j2
index 54941db06..c0da4c184 100644
--- a/roles/openstack-stack/templates/heat_stack.yaml.j2
+++ b/roles/openstack-stack/templates/heat_stack.yaml.j2
@@ -156,6 +156,13 @@ resources:
port_range_min: 22
port_range_max: 22
remote_ip_prefix: {{ ssh_ingress_cidr }}
+{% if use_bastion|bool %}
+ - direction: ingress
+ protocol: tcp
+ port_range_min: 22
+ port_range_max: 22
+ remote_ip_prefix: {{ bastion_ingress_cidr }}
+{% endif %}
- direction: ingress
protocol: icmp
remote_ip_prefix: {{ ssh_ingress_cidr }}
@@ -432,7 +439,7 @@ resources:
port_range_min: 53
port_range_max: 53
remote_ip_prefix: "{{ openstack_subnet_prefix }}.0/24"
-{% if num_masters > 1 %}
+{% if num_masters > 1 or ui_ssh_tunnel|bool %}
lb-secgrp:
type: OS::Neutron::SecurityGroup
properties:
@@ -443,14 +450,21 @@ resources:
protocol: tcp
port_range_min: {{ openshift_master_api_port | default(8443) }}
port_range_max: {{ openshift_master_api_port | default(8443) }}
- remote_ip_prefix: {{ lb_ingress_cidr }}
- {% if openshift_master_console_port is defined and openshift_master_console_port != openshift_master_api_port %}
+ remote_ip_prefix: {{ lb_ingress_cidr | default(bastion_ingress_cidr) }}
+{% if ui_ssh_tunnel|bool %}
+ - direction: ingress
+ protocol: tcp
+ port_range_min: {{ openshift_master_api_port | default(8443) }}
+ port_range_max: {{ openshift_master_api_port | default(8443) }}
+ remote_ip_prefix: {{ ssh_ingress_cidr }}
+{% endif %}
+{% if openshift_master_console_port is defined and openshift_master_console_port != openshift_master_api_port %}
- direction: ingress
protocol: tcp
port_range_min: {{ openshift_master_console_port | default(8443) }}
port_range_max: {{ openshift_master_console_port | default(8443) }}
- remote_ip_prefix: {{ lb_ingress_cidr }}
- {% endif %}
+ remote_ip_prefix: {{ lb_ingress_cidr | default(bastion_ingress_cidr) }}
+{% endif %}
{% endif %}
etcd:
@@ -458,14 +472,18 @@ resources:
properties:
count: {{ num_etcd }}
resource_def:
+{% if use_bastion|bool %}
+ type: server_nofloating.yaml
+{% else %}
type: server.yaml
+{% endif %}
properties:
name:
str_replace:
template: k8s_type-%index%.cluster_id
params:
cluster_id: {{ stack_name }}
- k8s_type: etcd
+ k8s_type: {{ etcd_hostname }}
cluster_env: {{ public_dns_domain }}
cluster_id: {{ stack_name }}
group:
@@ -475,7 +493,7 @@ resources:
k8s_type: etcds
cluster_id: {{ stack_name }}
type: etcd
- image: {{ openstack_image }}
+ image: {{ openstack_etcd_image }}
flavor: {{ etcd_flavor }}
key_name: {{ ssh_public_key }}
net: { get_resource: net }
@@ -483,7 +501,9 @@ resources:
secgrp:
- { get_resource: {% if openstack_flat_secgrp|default(False)|bool %}flat-secgrp{% else %}etcd-secgrp{% endif %} }
- { get_resource: common-secgrp }
+{% if not use_bastion|bool %}
floating_network: {{ external_network }}
+{% endif %}
net_name:
str_replace:
template: openshift-ansible-cluster_id-net
@@ -506,7 +526,7 @@ resources:
template: k8s_type-%index%.cluster_id
params:
cluster_id: {{ stack_name }}
- k8s_type: lb
+ k8s_type: {{ lb_hostname }}
cluster_env: {{ public_dns_domain }}
cluster_id: {{ stack_name }}
group:
@@ -516,7 +536,7 @@ resources:
k8s_type: lb
cluster_id: {{ stack_name }}
type: lb
- image: {{ openstack_image }}
+ image: {{ openstack_lb_image }}
flavor: {{ lb_flavor }}
key_name: {{ ssh_public_key }}
net: { get_resource: net }
@@ -530,7 +550,7 @@ resources:
template: openshift-ansible-cluster_id-net
params:
cluster_id: {{ stack_name }}
- volume_size: 5
+ volume_size: {{ lb_volume_size }}
depends_on:
- interface
{% endif %}
@@ -540,14 +560,18 @@ resources:
properties:
count: {{ num_masters }}
resource_def:
+{% if use_bastion|bool %}
+ type: server_nofloating.yaml
+{% else %}
type: server.yaml
+{% endif %}
properties:
name:
str_replace:
template: k8s_type-%index%.cluster_id
params:
cluster_id: {{ stack_name }}
- k8s_type: master
+ k8s_type: {{ master_hostname }}
cluster_env: {{ public_dns_domain }}
cluster_id: {{ stack_name }}
group:
@@ -557,7 +581,7 @@ resources:
k8s_type: masters
cluster_id: {{ stack_name }}
type: master
- image: {{ openstack_image }}
+ image: {{ openstack_master_image }}
flavor: {{ master_flavor }}
key_name: {{ ssh_public_key }}
net: { get_resource: net }
@@ -573,7 +597,9 @@ resources:
{% endif %}
{% endif %}
- { get_resource: common-secgrp }
+{% if not use_bastion|bool %}
floating_network: {{ external_network }}
+{% endif %}
net_name:
str_replace:
template: openshift-ansible-cluster_id-net
@@ -590,15 +616,18 @@ resources:
removal_policies:
- resource_list: {{ nodes_to_remove }}
resource_def:
+{% if use_bastion|bool %}
+ type: server_nofloating.yaml
+{% else %}
type: server.yaml
+{% endif %}
properties:
name:
str_replace:
- template: subtype-k8s_type-%index%.cluster_id
+ template: sub_type_k8s_type-%index%.cluster_id
params:
cluster_id: {{ stack_name }}
- k8s_type: node
- subtype: app
+ sub_type_k8s_type: {{ node_hostname }}
cluster_env: {{ public_dns_domain }}
cluster_id: {{ stack_name }}
group:
@@ -613,7 +642,7 @@ resources:
{% for k, v in openshift_cluster_node_labels.app.iteritems() %}
{{ k|e }}: {{ v|e }}
{% endfor %}
- image: {{ openstack_image }}
+ image: {{ openstack_node_image }}
flavor: {{ node_flavor }}
key_name: {{ ssh_public_key }}
net: { get_resource: net }
@@ -621,13 +650,15 @@ resources:
secgrp:
- { get_resource: {% if openstack_flat_secgrp|default(False)|bool %}flat-secgrp{% else %}node-secgrp{% endif %} }
- { get_resource: common-secgrp }
+{% if not use_bastion|bool %}
floating_network: {{ external_network }}
+{% endif %}
net_name:
str_replace:
template: openshift-ansible-cluster_id-net
params:
cluster_id: {{ stack_name }}
- volume_size: {{ app_volume_size }}
+ volume_size: {{ node_volume_size }}
depends_on:
- interface
@@ -640,11 +671,10 @@ resources:
properties:
name:
str_replace:
- template: subtypek8s_type-%index%.cluster_id
+ template: sub_type_k8s_type-%index%.cluster_id
params:
cluster_id: {{ stack_name }}
- k8s_type: node
- subtype: infra
+ sub_type_k8s_type: {{ infra_hostname }}
cluster_env: {{ public_dns_domain }}
cluster_id: {{ stack_name }}
group:
@@ -659,7 +689,7 @@ resources:
{% for k, v in openshift_cluster_node_labels.infra.iteritems() %}
{{ k|e }}: {{ v|e }}
{% endfor %}
- image: {{ openstack_image }}
+ image: {{ openstack_infra_image }}
flavor: {{ infra_flavor }}
key_name: {{ ssh_public_key }}
net: { get_resource: net }
@@ -671,6 +701,9 @@ resources:
{% else %}
- { get_resource: node-secgrp }
{% endif %}
+{% if ui_ssh_tunnel|bool and num_masters < 2 %}
+ - { get_resource: lb-secgrp }
+{% endif %}
- { get_resource: infra-secgrp }
- { get_resource: common-secgrp }
floating_network: {{ external_network }}
@@ -695,7 +728,7 @@ resources:
template: k8s_type-%index%.cluster_id
params:
cluster_id: {{ stack_name }}
- k8s_type: dns
+ k8s_type: {{ dns_hostname }}
cluster_env: {{ public_dns_domain }}
cluster_id: {{ stack_name }}
group:
@@ -705,7 +738,7 @@ resources:
k8s_type: dns
cluster_id: {{ stack_name }}
type: dns
- image: {{ openstack_image }}
+ image: {{ openstack_dns_image }}
flavor: {{ dns_flavor }}
key_name: {{ ssh_public_key }}
net: { get_resource: net }
diff --git a/roles/openstack-stack/templates/heat_stack_server.yaml.j2 b/roles/openstack-stack/templates/heat_stack_server.yaml.j2
index 5851d3b9b..32fb166f6 100644
--- a/roles/openstack-stack/templates/heat_stack_server.yaml.j2
+++ b/roles/openstack-stack/templates/heat_stack_server.yaml.j2
@@ -134,6 +134,7 @@ resources:
user_data:
get_file: user-data
user_data_format: RAW
+ user_data_update_policy: IGNORE
metadata:
group: { get_param: group }
environment: { get_param: cluster_env }
@@ -156,6 +157,7 @@ resources:
floating_network: { get_param: floating_network }
port_id: { get_resource: port }
+{% if not ephemeral_volumes|default(false)|bool %}
cinder_volume:
type: OS::Cinder::Volume
properties:
@@ -168,3 +170,4 @@ resources:
volume_id: { get_resource: cinder_volume }
instance_uuid: { get_resource: server }
mountpoint: /dev/sdb
+{% endif %}
diff --git a/roles/openstack-stack/templates/heat_stack_server_nofloating.yaml.j2 b/roles/openstack-stack/templates/heat_stack_server_nofloating.yaml.j2
new file mode 100644
index 000000000..638fc8b45
--- /dev/null
+++ b/roles/openstack-stack/templates/heat_stack_server_nofloating.yaml.j2
@@ -0,0 +1,152 @@
+heat_template_version: 2016-10-14
+
+description: OpenShift cluster server w/o floating IP
+
+parameters:
+
+ name:
+ type: string
+ label: Name
+ description: Name
+
+ group:
+ type: string
+ label: Host Group
+ description: The Primary Ansible Host Group
+ default: host
+
+ cluster_env:
+ type: string
+ label: Cluster environment
+ description: Environment of the cluster
+
+ cluster_id:
+ type: string
+ label: Cluster ID
+ description: Identifier of the cluster
+
+ type:
+ type: string
+ label: Type
+ description: Type master or node
+
+ subtype:
+ type: string
+ label: Sub-type
+ description: Sub-type compute or infra for nodes, default otherwise
+ default: default
+
+ key_name:
+ type: string
+ label: Key name
+ description: Key name of keypair
+
+ image:
+ type: string
+ label: Image
+ description: Name of the image
+
+ flavor:
+ type: string
+ label: Flavor
+ description: Name of the flavor
+
+ net:
+ type: string
+ label: Net ID
+ description: Net resource
+
+ net_name:
+ type: string
+ label: Net name
+ description: Net name
+
+ subnet:
+ type: string
+ label: Subnet ID
+ description: Subnet resource
+
+ secgrp:
+ type: comma_delimited_list
+ label: Security groups
+ description: Security group resources
+
+ availability_zone:
+ type: string
+ description: The Availability Zone to launch the instance.
+ default: nova
+
+ volume_size:
+ type: number
+ description: Size of the volume to be created.
+ default: 1
+ constraints:
+ - range: { min: 1, max: 1024 }
+ description: must be between 1 and 1024 Gb.
+
+ node_labels:
+ type: json
+ description: OpenShift Node Labels
+ default: {"region": "default" }
+
+outputs:
+
+ name:
+ description: Name of the server
+ value: { get_attr: [ server_nofloating, name ] }
+
+ private_ip:
+ description: Private IP of the server
+ value:
+ get_attr:
+ - server_nofloating
+ - addresses
+ - { get_param: net_name }
+ - 0
+ - addr
+
+resources:
+
+ server_nofloating:
+ type: OS::Nova::Server
+ properties:
+ name: { get_param: name }
+ key_name: { get_param: key_name }
+ image: { get_param: image }
+ flavor: { get_param: flavor }
+ networks:
+ - port: { get_resource: port }
+ user_data:
+ get_file: user-data
+ user_data_format: RAW
+ user_data_update_policy: IGNORE
+ metadata:
+ group: { get_param: group }
+ environment: { get_param: cluster_env }
+ clusterid: { get_param: cluster_id }
+ host-type: { get_param: type }
+ sub-host-type: { get_param: subtype }
+ node_labels: { get_param: node_labels }
+
+ port:
+ type: OS::Neutron::Port
+ properties:
+ network: { get_param: net }
+ fixed_ips:
+ - subnet: { get_param: subnet }
+ security_groups: { get_param: secgrp }
+
+{% if not ephemeral_volumes|default(false)|bool %}
+ cinder_volume:
+ type: OS::Cinder::Volume
+ properties:
+ size: { get_param: volume_size }
+ availability_zone: { get_param: availability_zone }
+
+ volume_attachment:
+ type: OS::Cinder::VolumeAttachment
+ properties:
+ volume_id: { get_resource: cinder_volume }
+ instance_uuid: { get_resource: server_nofloating }
+ mountpoint: /dev/sdb
+{% endif %}
diff --git a/roles/static_inventory/defaults/main.yml b/roles/static_inventory/defaults/main.yml
index 315965cde..871700f8c 100644
--- a/roles/static_inventory/defaults/main.yml
+++ b/roles/static_inventory/defaults/main.yml
@@ -4,5 +4,26 @@ refresh_inventory: True
inventory: static
inventory_path: ~/openstack-inventory
+# Either to configure bastion
+use_bastion: true
+
+# SSH user/key/options to access hosts via bastion
+ssh_user: openshift
+ssh_options: >-
+ -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
+ -o ConnectTimeout=90 -o ControlMaster=auto -o ControlPersist=270s
+ -o ServerAliveInterval=30 -o GSSAPIAuthentication=no
+
# SSH key to access nodes
private_ssh_key: ~/.ssh/openshift
+
+# The patch to store the generated config to access bastion/hosts
+ssh_config_path: /tmp/ssh.config.ansible
+
+# The IP:port to make an SSH tunnel to access UI on the 1st master
+# via bastion node (requires sudo on the ansible control node)
+ui_ssh_tunnel: False
+ui_port: "{{ openshift_master_api_port | default(8443) }}"
+target_ip: "{{ hostvars[groups['masters.' + stack_name|quote][0]].private_v4 }}"
+
+openstack_private_network: private
diff --git a/roles/static_inventory/tasks/main.yml b/roles/static_inventory/tasks/main.yml
index 15c81690e..24e11beb6 100644
--- a/roles/static_inventory/tasks/main.yml
+++ b/roles/static_inventory/tasks/main.yml
@@ -4,3 +4,14 @@
- name: Checkpoint in-memory data into a static inventory
include: checkpoint.yml
+
+- name: Generate SSH config for accessing hosts via bastion
+ include: sshconfig.yml
+ when: use_bastion|bool
+
+- name: Configure SSH tunneling to access UI
+ include: sshtun.yml
+ become: true
+ when:
+ - use_bastion|bool
+ - ui_ssh_tunnel|bool
diff --git a/roles/static_inventory/tasks/openstack.yml b/roles/static_inventory/tasks/openstack.yml
index a25502835..75d0ee6d5 100644
--- a/roles/static_inventory/tasks/openstack.yml
+++ b/roles/static_inventory/tasks/openstack.yml
@@ -16,6 +16,7 @@
- name: set_fact for openstack inventory nodes
set_fact:
+ registered_bastion_nodes: "{{ (registered_nodes_output.stdout | from_json) | json_query(q) }}"
registered_nodes_floating: "{{ (registered_nodes_output.stdout | from_json) | json_query(q2) }}"
vars:
q: "[] | [?metadata.group=='infra.{{stack_name}}']"
@@ -24,24 +25,53 @@
- refresh_inventory|bool
- name: Add cluster nodes w/o floating IPs to inventory
- with_items: "{{ registered_nodes }}"
- when: not item in registered_nodes_floating
+ with_items: "{{ registered_nodes|difference(registered_nodes_floating) }}"
add_host:
name: '{{ item.name }}'
groups: '{{ item.metadata.group }}'
- ansible_host: '{{ item.private_v4 }}'
+ ansible_host: >-
+ {% if use_bastion|bool -%}
+ {{ item.name }}
+ {%- else -%}
+ {%- set node = registered_nodes | json_query("[?name=='" + item.name + "']") -%}
+ {{ node[0].addresses[openstack_private_network|quote][0].addr }}
+ {%- endif %}
ansible_fqdn: '{{ item.name }}'
+ ansible_user: '{{ ssh_user }}'
ansible_private_key_file: '{{ private_ssh_key }}'
- private_v4: '{{ item.private_v4 }}'
+ ansible_ssh_extra_args: '-F {{ ssh_config_path }}'
+ private_v4: >-
+ {% set node = registered_nodes | json_query("[?name=='" + item.name + "']") -%}
+ {{ node[0].addresses[openstack_private_network|quote][0].addr }}
- name: Add cluster nodes with floating IPs to inventory
with_items: "{{ registered_nodes_floating }}"
- when: item in registered_nodes_floating
add_host:
name: '{{ item.name }}'
groups: '{{ item.metadata.group }}'
- ansible_host: '{{ item.public_v4 }}'
+ ansible_host: "{% if use_bastion|bool %}{{ item.name }}{% else %}{{ item.public_v4 }}{% endif %}"
ansible_fqdn: '{{ item.name }}'
+ ansible_user: '{{ ssh_user }}'
ansible_private_key_file: '{{ private_ssh_key }}'
- private_v4: '{{ item.private_v4 }}'
+ ansible_ssh_extra_args: '-F {{ ssh_config_path }}'
+ private_v4: >-
+ {% set node = registered_nodes | json_query("[?name=='" + item.name + "']") -%}
+ {{ node[0].addresses[openstack_private_network|quote][0].addr }}
public_v4: '{{ item.public_v4 }}'
+
+ - name: Add bastion node to inventory
+ add_host:
+ name: bastion
+ groups: bastions
+ ansible_host: '{{ registered_bastion_nodes[0].public_v4 }}'
+ ansible_fqdn: '{{ registered_bastion_nodes[0].name }}'
+ ansible_user: '{{ ssh_user }}'
+ ansible_private_key_file: '{{ private_ssh_key }}'
+ ansible_ssh_extra_args: '-F {{ ssh_config_path }}'
+ private_v4: >-
+ {% set node = registered_nodes | json_query("[?name=='" + registered_bastion_nodes[0].name + "']") -%}
+ {{ node[0].addresses[openstack_private_network|quote][0].addr }}
+ public_v4: '{{ registered_bastion_nodes[0].public_v4 }}'
+ when:
+ - registered_bastion_nodes is defined
+ - use_bastion|bool
diff --git a/roles/static_inventory/tasks/sshconfig.yml b/roles/static_inventory/tasks/sshconfig.yml
new file mode 100644
index 000000000..7119fe6ff
--- /dev/null
+++ b/roles/static_inventory/tasks/sshconfig.yml
@@ -0,0 +1,13 @@
+---
+- name: set ssh proxy command prefix for accessing nodes via bastion
+ set_fact:
+ ssh_proxy_command: >-
+ ssh {{ ssh_options }}
+ -i {{ private_ssh_key }}
+ {{ ssh_user }}@{{ hostvars['bastion'].ansible_host }}
+
+- name: regenerate ssh config
+ template:
+ src: openstack_ssh_config.j2
+ dest: "{{ ssh_config_path }}"
+ mode: 0644
diff --git a/roles/static_inventory/tasks/sshtun.yml b/roles/static_inventory/tasks/sshtun.yml
new file mode 100644
index 000000000..b0e4c832c
--- /dev/null
+++ b/roles/static_inventory/tasks/sshtun.yml
@@ -0,0 +1,15 @@
+---
+- name: Create ssh tunnel systemd service
+ template:
+ src: ssh-tunnel.service.j2
+ dest: /etc/systemd/system/ssh-tunnel.service
+ mode: 0644
+
+- name: reload the systemctl daemon after file update
+ command: systemctl daemon-reload
+
+- name: Enable ssh tunnel service
+ service:
+ name: ssh-tunnel
+ enabled: true
+ state: restarted
diff --git a/roles/static_inventory/templates/inventory.j2 b/roles/static_inventory/templates/inventory.j2
index 464726a0b..24dc9d4a8 100644
--- a/roles/static_inventory/templates/inventory.j2
+++ b/roles/static_inventory/templates/inventory.j2
@@ -10,9 +10,12 @@
%} private_v4={{ hostvars[host]['private_v4'] }}{% endif %}
{% if 'public_v4' in hostvars[host]
%} public_v4={{ hostvars[host]['public_v4'] }}{% endif %}
+{% if 'ansible_user' in hostvars[host]
+%} ansible_user={{ hostvars[host]['ansible_user'] }}{% endif %}
{% if 'ansible_private_key_file' in hostvars[host]
%} ansible_private_key_file={{ hostvars[host]['ansible_private_key_file'] }}{% endif %}
- openshift_hostname={{ host }}
+{% if use_bastion|bool and 'ansible_ssh_extra_args' in hostvars[host]
+%} ansible_ssh_extra_args={{ hostvars[host]['ansible_ssh_extra_args']|quote }}{% endif %} openshift_hostname={{ host }}
{% endif %}
{% endfor %}
diff --git a/roles/static_inventory/templates/openstack_ssh_config.j2 b/roles/static_inventory/templates/openstack_ssh_config.j2
new file mode 100644
index 000000000..ad5d1253a
--- /dev/null
+++ b/roles/static_inventory/templates/openstack_ssh_config.j2
@@ -0,0 +1,21 @@
+Host *
+ IdentitiesOnly yes
+
+Host bastion
+ Hostname {{ hostvars['bastion'].ansible_host }}
+ IdentityFile {{ hostvars['bastion'].ansible_private_key_file }}
+ User {{ ssh_user }}
+ StrictHostKeyChecking no
+ UserKnownHostsFile=/dev/null
+
+{% for host in groups['all'] | difference(groups['bastions'][0]) %}
+
+Host {{ host }}
+ Hostname {{ hostvars[host].ansible_host }}
+ ProxyCommand {{ ssh_proxy_command }} -W {{ hostvars[host].private_v4 }}:22
+ IdentityFile {{ hostvars[host].ansible_private_key_file }}
+ User {{ ssh_user }}
+ StrictHostKeyChecking no
+ UserKnownHostsFile=/dev/null
+
+{% endfor %}
diff --git a/roles/static_inventory/templates/ssh-tunnel.service.j2 b/roles/static_inventory/templates/ssh-tunnel.service.j2
new file mode 100644
index 000000000..0d1cf8f79
--- /dev/null
+++ b/roles/static_inventory/templates/ssh-tunnel.service.j2
@@ -0,0 +1,20 @@
+[Unit]
+Description=Set up ssh tunneling for OpenShift cluster UI
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/ssh -NT -o \
+ ServerAliveInterval=60 -o \
+ UserKnownHostsFile=/dev/null -o \
+ StrictHostKeyChecking=no -o \
+ ExitOnForwardFailure=no -i \
+ {{ private_ssh_key }} {{ ssh_user }}@{{ hostvars['bastion'].ansible_host }} \
+ -L 0.0.0.0:{{ ui_port }}:{{ target_ip }}:{{ ui_port }}
+
+
+# Restart every >2 seconds to avoid StartLimitInterval failure
+RestartSec=5
+Restart=always
+
+[Install]
+WantedBy=multi-user.target