From df8f5f0e251a014ab30dabd62c17e151b7fe36e8 Mon Sep 17 00:00:00 2001
From: Bogdan Dobrelya <bdobreli@redhat.com>
Date: Wed, 12 Jul 2017 13:09:45 +0200
Subject: Options for bastion, SSH config, static inventory autogeneration

* At the provisioning stage, allow users to auto-generate SSH config,
  when using a static inventory.
* Run playbooks to provsion and post-provision as a separate, when
  using a bastion. This re-applies the SSH config, which ansible can't
  do on the fly.
* Support a pre-installed bastion node, colocated with the 1st infra
  node.
* With a bastion enabled, reduce floating IP footprint to infra and
  dns nodes only, effectively isolating a cluster in a private
  network.

Signed-off-by: Bogdan Dobrelya <bdobreli@redhat.com>
---
 playbooks/provisioning/openstack/README.md         |  31 ++++-
 .../openstack/openstack_dns_records.yml            |   2 +
 .../openstack/post-provision-openstack.yml         |   6 +-
 .../provisioning/openstack/provision-openstack.yml |  11 +-
 .../openstack/sample-inventory/group_vars/all.yml  |   7 +
 playbooks/provisioning/openstack/stack_params.yaml |   1 +
 roles/openstack-stack/defaults/main.yml            |   2 +
 roles/openstack-stack/tasks/main.yml               |   9 +-
 roles/openstack-stack/templates/heat_stack.yaml.j2 |  25 ++++
 .../templates/heat_stack_server_nofloating.yaml.j2 | 149 +++++++++++++++++++++
 roles/static_inventory/tasks/openstack.yml         |   7 +-
 roles/static_inventory/templates/inventory.j2      |   5 +-
 12 files changed, 238 insertions(+), 17 deletions(-)
 create mode 100644 roles/openstack-stack/templates/heat_stack_server_nofloating.yaml.j2

diff --git a/playbooks/provisioning/openstack/README.md b/playbooks/provisioning/openstack/README.md
index 1ff586b49..6b9e5a3a9 100644
--- a/playbooks/provisioning/openstack/README.md
+++ b/playbooks/provisioning/openstack/README.md
@@ -40,7 +40,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)).
@@ -177,16 +177,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).
 
-In this guide, the latter points to the current directory, where you run ansible commands
+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
+
+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 +208,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 +227,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
 
diff --git a/playbooks/provisioning/openstack/openstack_dns_records.yml b/playbooks/provisioning/openstack/openstack_dns_records.yml
index b5f0840c5..980221ed6 100644
--- a/playbooks/provisioning/openstack/openstack_dns_records.yml
+++ b/playbooks/provisioning/openstack/openstack_dns_records.yml
@@ -36,11 +36,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/post-provision-openstack.yml b/playbooks/provisioning/openstack/post-provision-openstack.yml
index a807c4d2f..c7df74a87 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
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/group_vars/all.yml b/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml
index 9eb36ab13..6d07f9b56 100644
--- a/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml
+++ b/playbooks/provisioning/openstack/sample-inventory/group_vars/all.yml
@@ -69,5 +69,12 @@ 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 }}
diff --git a/playbooks/provisioning/openstack/stack_params.yaml b/playbooks/provisioning/openstack/stack_params.yaml
index 9c0b09b45..c3a42ab06 100644
--- a/playbooks/provisioning/openstack/stack_params.yaml
+++ b/playbooks/provisioning/openstack/stack_params.yaml
@@ -21,3 +21,4 @@ master_volume_size: "{{ docker_volume_size }}"
 app_volume_size: "{{ docker_volume_size }}"
 infra_volume_size: "{{ docker_volume_size }}"
 nodes_to_remove: "{{ openstack_nodes_to_remove | default([]) |  to_yaml }}"
+use_bastion: "{{ openstack_use_bastion|default(False) }}"
diff --git a/roles/openstack-stack/defaults/main.yml b/roles/openstack-stack/defaults/main.yml
index 4831d6bc4..803a96389 100644
--- a/roles/openstack-stack/defaults/main.yml
+++ b/roles/openstack-stack/defaults/main.yml
@@ -4,6 +4,7 @@ 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 +12,4 @@ num_dns: 1
 num_infra: 1
 nodes_to_remove: []
 etcd_volume_size: 2
+use_bastion: 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..524f466ff 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 }}
@@ -458,7 +465,11 @@ resources:
     properties:
       count: {{ num_etcd }}
       resource_def:
+{% if use_bastion|bool %}
+        type: server_nofloating.yaml
+{% else %}
         type: server.yaml
+{% endif %}
         properties:
           name:
             str_replace:
@@ -483,7 +494,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
@@ -540,7 +553,11 @@ resources:
     properties:
       count: {{ num_masters }}
       resource_def:
+{% if use_bastion|bool %}
+        type: server_nofloating.yaml
+{% else %}
         type: server.yaml
+{% endif %}
         properties:
           name:
             str_replace:
@@ -573,7 +590,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,7 +609,11 @@ 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:
@@ -621,7 +644,9 @@ 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
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..792a8b90c
--- /dev/null
+++ b/roles/openstack-stack/templates/heat_stack_server_nofloating.yaml.j2
@@ -0,0 +1,149 @@
+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
+      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 }
+
+  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
diff --git a/roles/static_inventory/tasks/openstack.yml b/roles/static_inventory/tasks/openstack.yml
index 95d0d172f..499adf08c 100644
--- a/roles/static_inventory/tasks/openstack.yml
+++ b/roles/static_inventory/tasks/openstack.yml
@@ -23,11 +23,9 @@
         q2: "[] | [?metadata.clusterid=='{{stack_name}}'] | [?public_v4!='']"
       when:
         - refresh_inventory|bool
-        - use_bastion|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 }}'
@@ -40,11 +38,10 @@
 
     - 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: "{% if use_bastion|bool %}{{ item.name }}{% else %}{{ item.private_v4 }}{% endif %}"
+        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 }}'
diff --git a/roles/static_inventory/templates/inventory.j2 b/roles/static_inventory/templates/inventory.j2
index ac74db35c..24dc9d4a8 100644
--- a/roles/static_inventory/templates/inventory.j2
+++ b/roles/static_inventory/templates/inventory.j2
@@ -14,9 +14,8 @@
 %} 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 %}
-{% if 'ansible_ssh_extra_args' in hostvars[host]
-%} ansible_ssh_extra_args={{ hostvars[host]['ansible_ssh_extra_args']|quote }}{% 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 %}
-- 
cgit v1.2.3