From f5a50a1d7d40fb39bbc503ab1079e89e4aafe3fe Mon Sep 17 00:00:00 2001
From: Jeff Peeler <jpeeler@redhat.com>
Date: Fri, 28 Aug 2015 11:26:40 -0400
Subject: [PATCH] Add Ansible support for Ironic

Configuration based off upstream documentation here:
http://docs.openstack.org/developer/ironic/deploy/install-guide.html

A few notes:
-ironic-api is not configured to use mod_wsgi
-several places it's noted that discoverd is going away and needs to be
replaced with ironic-inspector - (sqlite connection should be changed
too)
-currently enabling ironic reconfigures nova compute (driver and
scheduler) as well as changes neutron network settings
-a nice enhancement would be to configure the web console

Required post-deployment configuration:

Create the flat network to launch the instances:

neutron net-create --tenant-id $TENANT_ID sharednet1 --shared \
--provider:network_type flat --provider:physical_network physnet1

neutron subnet-create sharednet1 $NETWORK_CIDR --name $SUBNET_NAME \
--ip-version=4 --gateway=$GATEWAY_IP --allocation-pool \
start=$START_IP,end=$END_IP --enable-dhcp

And then the above ID is used to set cleaning_network_uuid in the neutron
section of ironic.conf.

Change-Id: I572e7ff1f23c4e57a2c50817cafe9269fd9950dd
Implements: blueprint ironic-container
---
 ansible/group_vars/all.yml                    |  5 ++
 ansible/inventory/all-in-one                  | 15 ++++
 ansible/inventory/multinode                   | 16 ++++
 .../roles/haproxy/templates/haproxy.cfg.j2    |  8 ++
 ansible/roles/ironic/defaults/main.yml        | 42 ++++++++++
 ansible/roles/ironic/meta/main.yml            |  3 +
 ansible/roles/ironic/tasks/bootstrap.yml      | 64 +++++++++++++++
 ansible/roles/ironic/tasks/config.yml         | 78 +++++++++++++++++++
 ansible/roles/ironic/tasks/main.yml           |  8 ++
 ansible/roles/ironic/tasks/register.yml       | 37 +++++++++
 ansible/roles/ironic/tasks/start.yml          | 77 ++++++++++++++++++
 .../roles/ironic/templates/discoverd.conf.j2  | 11 +++
 .../roles/ironic/templates/ironic-api.json.j2 | 11 +++
 .../ironic/templates/ironic-conductor.json.j2 | 11 +++
 .../ironic/templates/ironic-discoverd.json.j2 | 11 +++
 .../roles/ironic/templates/ironic-pxe.json.j2 |  4 +
 ansible/roles/ironic/templates/ironic.conf.j2 | 30 +++++++
 ansible/roles/neutron/tasks/ironic-check.yml  |  4 +
 ansible/roles/neutron/tasks/main.yml          |  3 +
 .../roles/neutron/templates/ml2_conf.ini.j2   | 10 +++
 ansible/roles/nova/templates/nova.conf.j2     | 22 +++++-
 ansible/site.yml                              |  4 +
 doc/ironic-guide.rst                          | 42 ++++++++++
 docker/ironic/ironic-api/Dockerfile.j2        |  7 +-
 docker/ironic/ironic-api/start.sh             | 13 ++--
 docker/ironic/ironic-base/Dockerfile.j2       |  1 +
 docker/ironic/ironic-conductor/Dockerfile.j2  |  7 +-
 .../ironic-conductor/config-external.sh       | 11 ---
 docker/ironic/ironic-conductor/start.sh       |  6 +-
 docker/ironic/ironic-discoverd/Dockerfile.j2  | 15 +++-
 .../ironic-discoverd/config-external.sh       | 11 ---
 docker/ironic/ironic-discoverd/start.sh       |  6 +-
 docker/ironic/ironic-pxe/Dockerfile.j2        | 35 +++++++++
 docker/ironic/ironic-pxe/start.sh             |  6 ++
 etc/kolla/config/discoverd.conf               |  0
 etc/kolla/config/ironic.conf                  |  0
 etc/kolla/config/ironic/discoverd.conf        |  0
 etc/kolla/config/ironic/ironic-api.conf       |  0
 etc/kolla/config/ironic/ironic-conductor.conf |  0
 etc/kolla/passwords.yml                       |  3 +
 tests/test_build.py                           |  4 +-
 41 files changed, 592 insertions(+), 49 deletions(-)
 create mode 100644 ansible/roles/ironic/defaults/main.yml
 create mode 100644 ansible/roles/ironic/meta/main.yml
 create mode 100644 ansible/roles/ironic/tasks/bootstrap.yml
 create mode 100644 ansible/roles/ironic/tasks/config.yml
 create mode 100644 ansible/roles/ironic/tasks/main.yml
 create mode 100644 ansible/roles/ironic/tasks/register.yml
 create mode 100644 ansible/roles/ironic/tasks/start.yml
 create mode 100644 ansible/roles/ironic/templates/discoverd.conf.j2
 create mode 100644 ansible/roles/ironic/templates/ironic-api.json.j2
 create mode 100644 ansible/roles/ironic/templates/ironic-conductor.json.j2
 create mode 100644 ansible/roles/ironic/templates/ironic-discoverd.json.j2
 create mode 100644 ansible/roles/ironic/templates/ironic-pxe.json.j2
 create mode 100644 ansible/roles/ironic/templates/ironic.conf.j2
 create mode 100644 ansible/roles/neutron/tasks/ironic-check.yml
 create mode 100644 doc/ironic-guide.rst
 delete mode 100644 docker/ironic/ironic-conductor/config-external.sh
 delete mode 100644 docker/ironic/ironic-discoverd/config-external.sh
 create mode 100644 docker/ironic/ironic-pxe/Dockerfile.j2
 create mode 100755 docker/ironic/ironic-pxe/start.sh
 create mode 100644 etc/kolla/config/discoverd.conf
 create mode 100644 etc/kolla/config/ironic.conf
 create mode 100644 etc/kolla/config/ironic/discoverd.conf
 create mode 100644 etc/kolla/config/ironic/ironic-api.conf
 create mode 100644 etc/kolla/config/ironic/ironic-conductor.conf

diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index a7f7c408e4..3f3072d1ca 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -104,6 +104,9 @@ heat_api_cfn_port: "8000"
 
 murano_api_port: "8082"
 
+ironic_api_port: "6385"
+
+
 ####################
 # Openstack options
 ####################
@@ -146,7 +149,9 @@ enable_heat: "yes"
 enable_horizon: "yes"
 enable_swift: "no"
 enable_murano: "no"
+enable_ironic: "no"
 
+ironic_keystone_user: "ironic"
 
 ####################
 # RabbitMQ options
diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one
index d820c3e606..ab6eceeb27 100644
--- a/ansible/inventory/all-in-one
+++ b/ansible/inventory/all-in-one
@@ -60,6 +60,8 @@ control
 [ceph-osd:children]
 storage
 
+[ironic:children]
+control
 
 # Additional control implemented here. These groups allow you to control which
 # services run on which hosts at a per-service level.
@@ -140,3 +142,16 @@ murano
 
 [murano-engine:children]
 murano
+
+# Ironic
+[ironic-api:children]
+ironic
+
+[ironic-conductor:children]
+ironic
+
+[ironic-discoverd:children]
+ironic
+
+[ironic-pxe:children]
+ironic
diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode
index e677fcaeff..cc643be11e 100644
--- a/ansible/inventory/multinode
+++ b/ansible/inventory/multinode
@@ -62,6 +62,9 @@ control
 [murano:children]
 control
 
+[ironic:children]
+control
+
 [ceph-mon:children]
 control
 
@@ -148,3 +151,16 @@ murano
 
 [murano-engine:children]
 murano
+
+# Ironic
+[ironic-api:children]
+ironic
+
+[ironic-conductor:children]
+ironic
+
+[ironic-discoverd:children]
+ironic
+
+[ironic-pxe:children]
+ironic
diff --git a/ansible/roles/haproxy/templates/haproxy.cfg.j2 b/ansible/roles/haproxy/templates/haproxy.cfg.j2
index 637cb1ce1d..0c8d59c912 100644
--- a/ansible/roles/haproxy/templates/haproxy.cfg.j2
+++ b/ansible/roles/haproxy/templates/haproxy.cfg.j2
@@ -141,3 +141,11 @@ listen heat_api_cfn
   server {{ hostvars[host]['ansible_hostname'] }} {{ hostvars[host]['ansible_' + api_interface]['ipv4']['address'] }}:{{ heat_api_cfn_port }} check inter 2000 rise 2 fall 5
 {% endfor %}
 {% endif %}
+
+{% if enable_ironic | bool %}
+listen ironic_api
+  bind {{ kolla_internal_address}}:{{ ironic_api_port }}
+{% for host in groups['ironic-api'] %}
+  server {{ hostvars[host]['ansible_hostname'] }} {{ hostvars[host]['ansible_' + api_interface]['ipv4']['address'] }}:{{ ironic_api_port }} check inter 2000 rise 2 fall 5
+{% endfor %}
+{% endif %}
diff --git a/ansible/roles/ironic/defaults/main.yml b/ansible/roles/ironic/defaults/main.yml
new file mode 100644
index 0000000000..8a9ee32cda
--- /dev/null
+++ b/ansible/roles/ironic/defaults/main.yml
@@ -0,0 +1,42 @@
+---
+project_name: "ironic"
+
+####################
+# Database
+####################
+ironic_database_name: "ironic"
+ironic_database_user: "ironic"
+ironic_database_address: "{{ kolla_internal_address }}"
+
+
+####################
+# Docker
+####################
+ironic_api_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ kolla_install_type }}-ironic-api"
+ironic_api_tag: "{{ openstack_release }}"
+ironic_api_image_full: "{{ ironic_api_image }}:{{ ironic_api_tag }}"
+
+ironic_conductor_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ kolla_install_type }}-ironic-conductor"
+ironic_conductor_tag: "{{ openstack_release }}"
+ironic_conductor_image_full: "{{ ironic_conductor_image }}:{{ ironic_conductor_tag }}"
+
+ironic_discoverd_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ kolla_install_type }}-ironic-discoverd"
+ironic_discoverd_tag: "{{ openstack_release }}"
+ironic_discoverd_image_full: "{{ ironic_discoverd_image }}:{{ ironic_discoverd_tag }}"
+
+ironic_pxe_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ kolla_install_type }}-ironic-pxe"
+ironic_pxe_tag: "{{ openstack_release }}"
+ironic_pxe_image_full: "{{ ironic_pxe_image }}:{{ ironic_pxe_tag }}"
+
+
+####################
+# Openstack
+####################
+ironic_public_address: "{{ kolla_external_address }}"
+ironic_admin_address: "{{ kolla_internal_address }}"
+ironic_internal_address: "{{ kolla_internal_address }}"
+
+ironic_logging_verbose: "{{ openstack_logging_verbose }}"
+ironic_logging_debug: "{{ openstack_logging_debug }}"
+
+openstack_ironic_auth: "{'auth_url':'{{ openstack_auth_v2.auth_url }}','username':'{{ openstack_auth_v2.username }}','password':'{{ openstack_auth_v2.password }}','project_name':'{{ openstack_auth_v2.project_name }}'}"
diff --git a/ansible/roles/ironic/meta/main.yml b/ansible/roles/ironic/meta/main.yml
new file mode 100644
index 0000000000..6b4fff8fef
--- /dev/null
+++ b/ansible/roles/ironic/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+  - { role: common }
diff --git a/ansible/roles/ironic/tasks/bootstrap.yml b/ansible/roles/ironic/tasks/bootstrap.yml
new file mode 100644
index 0000000000..2f99cf59fe
--- /dev/null
+++ b/ansible/roles/ironic/tasks/bootstrap.yml
@@ -0,0 +1,64 @@
+---
+- name: Creating Ironic database
+  command: docker exec -t kolla_ansible /usr/bin/ansible localhost
+    -m mysql_db
+    -a "login_host='{{ database_address }}'
+        login_port='{{ mariadb_port }}'
+        login_user='{{ database_user }}'
+        login_password='{{ database_password }}'
+        name='{{ ironic_database_name }}'"
+  register: database
+  changed_when: "{{ database.stdout.find('localhost | SUCCESS => ') != -1 and (database.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed }}"
+  failed_when: database.stdout.split()[2] != 'SUCCESS'
+  run_once: True
+
+- name: Creating Ironic database user and setting permissions
+  command: docker exec -t kolla_ansible /usr/bin/ansible localhost
+    -m mysql_user
+    -a "login_host='{{ database_address }}'
+        login_port='{{ mariadb_port }}'
+        login_user='{{ database_user }}'
+        login_password='{{ database_password }}'
+        name='{{ ironic_database_name }}'
+        password='{{ ironic_database_password }}'
+        host='%'
+        priv='{{ ironic_database_name }}.*:ALL'
+        append_privs='yes'"
+  register: database_user_create
+  changed_when: "{{ database.stdout.find('localhost | SUCCESS => ') != -1 and (database_user_create.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed }}"
+  failed_when: database_user_create.stdout.split()[2] != 'SUCCESS'
+  run_once: True
+
+- name: Starting Ironic bootstrap container
+  docker:
+    detach: False
+    docker_api_version: "{{ docker_api_version }}"
+    net: host
+    pull: "{{ docker_pull_policy }}"
+    restart_policy: "no"
+    state: reloaded
+    registry: "{{ docker_registry }}"
+    username: "{{ docker_registry_username }}"
+    password: "{{ docker_registry_password }}"
+    insecure_registry: "{{ docker_insecure_registry }}"
+    name: bootstrap_ironic
+    image: "{{ ironic_api_image_full }}"
+    volumes: "{{ node_config_directory }}/ironic-api/:/opt/kolla/config_files/:ro"
+    env:
+      KOLLA_BOOTSTRAP:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+  run_once: True
+  when: database.stdout.find('localhost | SUCCESS => ') != -1 and (database.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed
+
+# https://github.com/ansible/ansible-modules-core/pull/1031
+- name: Waiting for Ironic bootstrap container to exit
+  command: docker wait bootstrap_ironic
+  run_once: True
+  when: database.stdout.find('localhost | SUCCESS => ') != -1 and (database.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed
+
+- name: Cleaning up boostrap container
+  docker:
+    name: bootstrap_ironic
+    image: "{{ ironic_api_image_full }}"
+    state: absent
+  when: database.stdout.find('localhost | SUCCESS => ') != -1 and (database.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed
diff --git a/ansible/roles/ironic/tasks/config.yml b/ansible/roles/ironic/tasks/config.yml
new file mode 100644
index 0000000000..45a3f7ea5b
--- /dev/null
+++ b/ansible/roles/ironic/tasks/config.yml
@@ -0,0 +1,78 @@
+---
+- include: ../../config.yml
+  vars:
+    service_name: "ironic-api"
+    config_source:
+      - "roles/ironic/templates/ironic.conf.j2"
+      - "/etc/kolla/config/global.conf"
+      - "/etc/kolla/config/database.conf"
+      - "/etc/kolla/config/messaging.conf"
+      - "/etc/kolla/config/{{ project_name }}.conf"
+      - "/etc/kolla/config/{{ project_name }}/{{ service_name }}.conf"
+    config_template_dest:
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_minimal"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_global"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_database"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_messaging"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_augment"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ service_name }}.conf_augment"
+    config_dest: "{{ node_config_directory }}/{{ service_name }}/ironic.conf"
+  when: inventory_hostname in groups['ironic-api']
+
+- name: Copying Ironic API JSON configuration file
+  template:
+    src: "roles/ironic/templates/ironic-api.json.j2"
+    dest: "{{ node_config_directory }}/ironic-api/config.json"
+
+- include: ../../config.yml
+  vars:
+    service_name: "ironic-conductor"
+    config_source:
+      - "roles/ironic/templates/ironic.conf.j2"
+      - "/etc/kolla/config/global.conf"
+      - "/etc/kolla/config/database.conf"
+      - "/etc/kolla/config/messaging.conf"
+      - "/etc/kolla/config/{{ project_name }}.conf"
+      - "/etc/kolla/config/{{ project_name }}/{{ service_name }}.conf"
+    config_template_dest:
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_minimal"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_global"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_database"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_messaging"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_augment"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ service_name }}.conf_augment"
+    config_dest: "{{ node_config_directory }}/{{ service_name }}/ironic.conf"
+  when: inventory_hostname in groups['ironic-conductor']
+
+- name: Copying Ironic conductor JSON configuration file
+  template:
+    src: "roles/ironic/templates/ironic-conductor.json.j2"
+    dest: "{{ node_config_directory }}/ironic-conductor/config.json"
+
+- include: ../../config.yml
+  vars:
+    service_name: "ironic-discoverd"
+    config_source:
+      - "roles/ironic/templates/discoverd.conf.j2"
+      - "/etc/kolla/config/global.conf"
+      - "/etc/kolla/config/database.conf"
+      - "/etc/kolla/config/messaging.conf"
+      - "/etc/kolla/config/{{ project_name }}/discoverd.conf"
+    config_template_dest:
+      - "{{ node_templates_directory }}/{{ service_name }}/discoverd.conf_minimal"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_global"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_database"
+      - "{{ node_templates_directory }}/{{ service_name }}/{{ project_name }}.conf_messaging"
+      - "{{ node_templates_directory }}/{{ service_name }}/discoverd.conf_augment"
+    config_dest: "{{ node_config_directory }}/{{ service_name }}/discoverd.conf"
+  when: inventory_hostname in groups['ironic-discoverd']
+
+- name: Copying Ironic discoverd JSON configuration file
+  template:
+    src: "roles/ironic/templates/ironic-discoverd.json.j2"
+    dest: "{{ node_config_directory }}/ironic-discoverd/config.json"
+
+- name: Copying Ironic PXE JSON configuration file
+  template:
+    src: "roles/ironic/templates/ironic-pxe.json.j2"
+    dest: "{{ node_config_directory }}/ironic-pxe/config.json"
diff --git a/ansible/roles/ironic/tasks/main.yml b/ansible/roles/ironic/tasks/main.yml
new file mode 100644
index 0000000000..5c48120b7c
--- /dev/null
+++ b/ansible/roles/ironic/tasks/main.yml
@@ -0,0 +1,8 @@
+---
+- include: register.yml
+
+- include: config.yml
+
+- include: bootstrap.yml
+
+- include: start.yml
diff --git a/ansible/roles/ironic/tasks/register.yml b/ansible/roles/ironic/tasks/register.yml
new file mode 100644
index 0000000000..ce44210f20
--- /dev/null
+++ b/ansible/roles/ironic/tasks/register.yml
@@ -0,0 +1,37 @@
+---
+- name: Creating the Ironic service and endpoint
+  command: docker exec -t kolla_ansible /usr/bin/ansible localhost
+    -m kolla_keystone_service
+    -a "service_name=ironic
+        service_type=baremetal
+        description='Ironic bare metal provisioning service'
+        endpoint_region={{ openstack_region_name }}
+        admin_url='http://{{ ironic_admin_address }}:{{ ironic_api_port }}'
+        internal_url='http://{{ ironic_internal_address }}:{{ ironic_api_port }}'
+        public_url='http://{{ ironic_public_address }}:{{ ironic_api_port }}'
+        region_name={{ openstack_region_name }}
+        auth={{ '{{ openstack_ironic_auth }}' }}"
+    -e "{'openstack_ironic_auth':{{ openstack_ironic_auth }}}"
+  register: ironic_endpoint
+  changed_when: "{{ ironic_endpoint.stdout.find('localhost | SUCCESS => ') != -1 and (ironic_endpoint.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed }}"
+  until: ironic_endpoint.stdout.split()[2] == 'SUCCESS'
+  retries: 10
+  delay: 5
+  run_once: True
+
+- name: Creating the Ironic project, user, and role
+  command: docker exec -t kolla_ansible /usr/bin/ansible localhost
+    -m kolla_keystone_user
+    -a "project=service
+        user=ironic
+        password={{ ironic_keystone_password }}
+        role=admin
+        region_name={{ openstack_region_name }}
+        auth={{ '{{ openstack_ironic_auth }}' }}"
+    -e "{'openstack_ironic_auth':{{ openstack_ironic_auth }}}"
+  register: ironic_user
+  changed_when: "{{ ironic_user.stdout.find('localhost | SUCCESS => ') != -1 and (ironic_user.stdout.split('localhost | SUCCESS => ')[1]|from_json).changed }}"
+  until: ironic_user.stdout.split()[2] == 'SUCCESS'
+  retries: 10
+  delay: 5
+  run_once: True
diff --git a/ansible/roles/ironic/tasks/start.yml b/ansible/roles/ironic/tasks/start.yml
new file mode 100644
index 0000000000..4b6c5b4530
--- /dev/null
+++ b/ansible/roles/ironic/tasks/start.yml
@@ -0,0 +1,77 @@
+---
+- name: Starting Ironic-api container
+  docker:
+    docker_api_version: "{{ docker_api_version }}"
+    net: host
+    pull: "{{ docker_pull_policy }}"
+    restart_policy: "{{ docker_restart_policy }}"
+    restart_policy_retry: "{{ docker_restart_policy_retry }}"
+    state: reloaded
+    registry: "{{ docker_registry }}"
+    username: "{{ docker_registry_username }}"
+    password: "{{ docker_registry_password }}"
+    insecure_registry: "{{ docker_insecure_registry }}"
+    name: ironic-api
+    image: "{{ ironic_api_image_full }}"
+    volumes: "{{ node_config_directory }}/ironic-api/:/opt/kolla/config_files/:ro"
+    env:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+  when: inventory_hostname in groups['ironic-api']
+
+- name: Starting Ironic-conductor container
+  docker:
+    docker_api_version: "{{ docker_api_version }}"
+    net: host
+    pull: "{{ docker_pull_policy }}"
+    restart_policy: "{{ docker_restart_policy }}"
+    restart_policy_retry: "{{ docker_restart_policy_retry }}"
+    state: reloaded
+    registry: "{{ docker_registry }}"
+    username: "{{ docker_registry_username }}"
+    password: "{{ docker_registry_password }}"
+    insecure_registry: "{{ docker_insecure_registry }}"
+    name: ironic-conductor
+    image: "{{ ironic_conductor_image_full }}"
+    volumes: "{{ node_config_directory }}/ironic-conductor/:/opt/kolla/config_files/:ro"
+    env:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+  when: inventory_hostname in groups['ironic-conductor']
+
+- name: Starting Ironic-discoverd container
+  docker:
+    docker_api_version: "{{ docker_api_version }}"
+    net: host
+    pull: "{{ docker_pull_policy }}"
+    restart_policy: "{{ docker_restart_policy }}"
+    restart_policy_retry: "{{ docker_restart_policy_retry }}"
+    state: reloaded
+    registry: "{{ docker_registry }}"
+    username: "{{ docker_registry_username }}"
+    password: "{{ docker_registry_password }}"
+    insecure_registry: "{{ docker_insecure_registry }}"
+    privileged: True
+    name: ironic-discoverd
+    image: "{{ ironic_discoverd_image_full }}"
+    volumes: "{{ node_config_directory }}/ironic-discoverd/:/opt/kolla/config_files/:ro"
+    env:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+  when: inventory_hostname in groups['ironic-discoverd']
+
+- name: Starting Ironic-pxe container
+  docker:
+    docker_api_version: "{{ docker_api_version }}"
+    net: host
+    pull: "{{ docker_pull_policy }}"
+    restart_policy: "{{ docker_restart_policy }}"
+    restart_policy_retry: "{{ docker_restart_policy_retry }}"
+    state: reloaded
+    registry: "{{ docker_registry }}"
+    username: "{{ docker_registry_username }}"
+    password: "{{ docker_registry_password }}"
+    insecure_registry: "{{ docker_insecure_registry }}"
+    name: ironic-pxe
+    image: "{{ ironic_pxe_image_full }}"
+    volumes: "{{ node_config_directory }}/ironic-pxe/:/opt/kolla/config_files/:ro"
+    env:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+  when: inventory_hostname in groups['ironic-pxe']
diff --git a/ansible/roles/ironic/templates/discoverd.conf.j2 b/ansible/roles/ironic/templates/discoverd.conf.j2
new file mode 100644
index 0000000000..1f64d564ac
--- /dev/null
+++ b/ansible/roles/ironic/templates/discoverd.conf.j2
@@ -0,0 +1,11 @@
+[discoverd]
+database = inspector.sqlite3
+os_auth_url = http://{{ kolla_internal_address }}:{{ keystone_public_port }}/v2.0
+os_username = {{ openstack_auth.username }}
+os_password = {{ openstack_auth.password }}
+os_tenant_name = {{ openstack_auth.project_name }}
+identity_uri = {{ openstack_auth.auth_url }}
+
+# Note: this will be in the firewall section once upgraded to inspector
+# unsure of the correct interface here
+dnsmasq_interface = {{ api_interface }}
diff --git a/ansible/roles/ironic/templates/ironic-api.json.j2 b/ansible/roles/ironic/templates/ironic-api.json.j2
new file mode 100644
index 0000000000..dd2caadf40
--- /dev/null
+++ b/ansible/roles/ironic/templates/ironic-api.json.j2
@@ -0,0 +1,11 @@
+{
+    "command": "/usr/bin/ironic-api",
+    "config_files": [
+        {
+            "source": "/opt/kolla/config_files/ironic.conf",
+            "dest": "/etc/ironic/ironic.conf",
+            "owner": "ironic",
+            "perm": "0600"
+        }
+    ]
+}
diff --git a/ansible/roles/ironic/templates/ironic-conductor.json.j2 b/ansible/roles/ironic/templates/ironic-conductor.json.j2
new file mode 100644
index 0000000000..e77781c112
--- /dev/null
+++ b/ansible/roles/ironic/templates/ironic-conductor.json.j2
@@ -0,0 +1,11 @@
+{
+    "command": "/usr/bin/ironic-conductor",
+    "config_files": [
+        {
+            "source": "/opt/kolla/config_files/ironic.conf",
+            "dest": "/etc/ironic/ironic.conf",
+            "owner": "ironic",
+            "perm": "0600"
+        }
+    ]
+}
diff --git a/ansible/roles/ironic/templates/ironic-discoverd.json.j2 b/ansible/roles/ironic/templates/ironic-discoverd.json.j2
new file mode 100644
index 0000000000..a4dc91d569
--- /dev/null
+++ b/ansible/roles/ironic/templates/ironic-discoverd.json.j2
@@ -0,0 +1,11 @@
+{
+    "command": "/usr/bin/ironic-discoverd --config-file /etc/ironic-discoverd/discoverd.conf",
+    "config_files": [
+        {
+            "source": "/opt/kolla/config_files/discoverd.conf",
+            "dest": "/etc/ironic-discoverd/discoverd.conf",
+            "owner": "root",
+            "perm": "0600"
+        }
+    ]
+}
diff --git a/ansible/roles/ironic/templates/ironic-pxe.json.j2 b/ansible/roles/ironic/templates/ironic-pxe.json.j2
new file mode 100644
index 0000000000..f8f1666e7b
--- /dev/null
+++ b/ansible/roles/ironic/templates/ironic-pxe.json.j2
@@ -0,0 +1,4 @@
+{
+    "command": "/usr/sbin/in.tftpd --verbose --foreground --user root --address 0.0.0.0:69 --map-file /tftpboot/map-file /tftpboot",
+    "config_files": []
+}
diff --git a/ansible/roles/ironic/templates/ironic.conf.j2 b/ansible/roles/ironic/templates/ironic.conf.j2
new file mode 100644
index 0000000000..6860aee12d
--- /dev/null
+++ b/ansible/roles/ironic/templates/ironic.conf.j2
@@ -0,0 +1,30 @@
+[DEFAULT]
+debug = {{ ironic_logging_debug }}
+verbose = {{ ironic_logging_verbose }}
+
+admin_token = {{ keystone_admin_token }}
+
+[database]
+connection = mysql://{{ ironic_database_user }}:{{ ironic_database_password }}@{{ ironic_database_address }}/{{ ironic_database_name }}
+
+[keystone_authtoken]
+auth_uri = http://{{ kolla_internal_address }}:{{ keystone_public_port }}
+auth_url = http://{{ kolla_internal_address }}:{{ keystone_admin_port }}
+auth_plugin = password
+project_domain_id = default
+user_domain_id = default
+project_name = service
+username = {{ ironic_keystone_user }}
+password = {{ ironic_keystone_password }}
+
+[glance]
+glance_host = {{ kolla_internal_address }}
+
+[neutron]
+url = http://{{ kolla_internal_address }}:{{ neutron_server_port }}
+
+[oslo_messaging_rabbit]
+rabbit_host = {{ kolla_internal_address }}
+rabbit_userid = {{ rabbitmq_user }}
+rabbit_password = {{ rabbitmq_password }}
+rabbit_ha_queues = true
diff --git a/ansible/roles/neutron/tasks/ironic-check.yml b/ansible/roles/neutron/tasks/ironic-check.yml
new file mode 100644
index 0000000000..f32a58a72d
--- /dev/null
+++ b/ansible/roles/neutron/tasks/ironic-check.yml
@@ -0,0 +1,4 @@
+---
+# TODO(SamYaple): run verification checks at start of playbook
+- fail: msg="neutron_plugin_agent must use openvswitch with Ironic"
+  when: enable_ironic | bool and neutron_plugin_agent != "openvswitch"
diff --git a/ansible/roles/neutron/tasks/main.yml b/ansible/roles/neutron/tasks/main.yml
index 5c48120b7c..3ff2244691 100644
--- a/ansible/roles/neutron/tasks/main.yml
+++ b/ansible/roles/neutron/tasks/main.yml
@@ -1,4 +1,7 @@
 ---
+# enforce ironic usage only with openvswtich
+- include: ironic-check.yml
+
 - include: register.yml
 
 - include: config.yml
diff --git a/ansible/roles/neutron/templates/ml2_conf.ini.j2 b/ansible/roles/neutron/templates/ml2_conf.ini.j2
index 92ec89c87c..18816147f4 100644
--- a/ansible/roles/neutron/templates/ml2_conf.ini.j2
+++ b/ansible/roles/neutron/templates/ml2_conf.ini.j2
@@ -1,8 +1,14 @@
 # ml2_conf.ini
 [ml2]
+{% if enable_ironic | bool %}
+type_drivers = flat
+tenant_network_types = flat
+mechanism_drivers = openvswitch
+{% else %}
 # Changing type_drivers after bootstrap can lead to database inconsistencies
 type_drivers = flat,vlan,vxlan
 tenant_network_types = vxlan
+{% endif %}
 
 {% if neutron_plugin_agent == "openvswitch" %}
 mechanism_drivers = openvswitch,l2population
@@ -11,7 +17,11 @@ mechanism_drivers = linuxbridge,l2population
 {% endif %}
 
 [ml2_type_vlan]
+{% if enable_ironic | bool %}
+network_vlan_ranges = physnet1
+{% else %}
 network_vlan_ranges =
+{% endif %}
 
 [ml2_type_flat]
 flat_networks = physnet1
diff --git a/ansible/roles/nova/templates/nova.conf.j2 b/ansible/roles/nova/templates/nova.conf.j2
index 28be05408e..a07751261e 100644
--- a/ansible/roles/nova/templates/nova.conf.j2
+++ b/ansible/roles/nova/templates/nova.conf.j2
@@ -30,7 +30,6 @@ linuxnet_interface_driver = nova.network.linux_net.LinuxOVSInterfaceDriver
 linuxnet_interface_driver = nova.network.linux_net.BridgeInterfaceDriver
 {% endif %}
 
-compute_driver = libvirt.LibvirtDriver
 allow_resize_to_same_host = true
 
 # Though my_ip is not used directly, lots of other variables use $my_ip
@@ -46,6 +45,27 @@ novncproxy_port = {{ nova_novncproxy_port }}
 novncproxy_base_url = http://{{ kolla_internal_address }}:{{ nova_novncproxy_port }}/vnc_auto.html
 {% endif %}
 
+{% if enable_ironic | bool %}
+compute_driver = nova.virt.ironic.IronicDriver
+scheduler_host_manager = nova.scheduler.ironic_host_manager.IronicHostManager
+ram_allocation_ratio = 1.0
+reserved_host_memory_mb = 0
+compute_manager = ironic.nova.compute.manager.ClusteredComputeManager
+scheduler_use_baremetal_filters = True
+{% else %}
+compute_driver = libvirt.LibvirtDriver
+{% endif %}
+
+{% if enable_ironic == 'yes' %}
+[ironic]
+#(TODO) remember to update this once discoverd is replaced by inspector
+admin_username = {{ ironic_keystone_user }}
+admin_password = {{ ironic_keystone_password }}
+admin_url = {{ openstack_auth.auth_url }}
+admin_tenant_name = {{ openstack_auth.project_name }}
+api_endpoint = http://{{ kolla_internal_address }}:{{ ironic_api_port }}/v1
+{% endif %}
+
 [oslo_messaging_rabbit]
 rabbit_host = {{ kolla_internal_address }}
 rabbit_userid = {{ rabbitmq_user }}
diff --git a/ansible/site.yml b/ansible/site.yml
index 6f12363607..b31c6a546a 100755
--- a/ansible/site.yml
+++ b/ansible/site.yml
@@ -51,3 +51,7 @@
 - hosts: [murano-api, murano-engine]
   roles:
     - { role: murano, tags: murano, when: enable_murano | bool }
+
+- hosts: [ironic-api, ironic-conductor, ironic-discoverd, ironic-pxe]
+  roles:
+    - {role: ironic, tags: ironic, when: enable_ironic | bool }
diff --git a/doc/ironic-guide.rst b/doc/ironic-guide.rst
new file mode 100644
index 0000000000..da8795d63a
--- /dev/null
+++ b/doc/ironic-guide.rst
@@ -0,0 +1,42 @@
+Ironic in Kolla
+===============
+
+Overview
+--------
+Currently Kolla can deploy the Ironic services:
+
+- ironic-api
+- ironic-conductor
+- ironic-discoverd
+
+As well as a required PXE service, deployed as ironic-pxe.
+
+Current status
+--------------
+The Ironic implementation is "tech preview", so currently instances can only be
+deployed on baremetal. Further work will be done to allow scheduling for both
+virtualized and baremetal deployments. Most probably at that time discoverd
+will be replaced by ironic-inspector.
+
+Post-deployment configuration
+-----------------------------
+Configuration based off upstream documentation_.
+
+Again, remember that enabling Ironic reconfigures nova compute (driver and
+scheduler) as well as changes neutron network settings. Further neutron setup
+is required as outlined below.
+
+Create the flat network to launch the instances:
+::
+
+    neutron net-create --tenant-id $TENANT_ID sharednet1 --shared \
+    --provider:network_type flat --provider:physical_network physnet1
+
+    neutron subnet-create sharednet1 $NETWORK_CIDR --name $SUBNET_NAME \
+    --ip-version=4 --gateway=$GATEWAY_IP --allocation-pool \
+    start=$START_IP,end=$END_IP --enable-dhcp
+
+And then the above ID is used to set cleaning_network_uuid in the neutron
+section of ironic.conf.
+
+.. _documentation: http://docs.openstack.org/developer/ironic/deploy/install-guide.html
diff --git a/docker/ironic/ironic-api/Dockerfile.j2 b/docker/ironic/ironic-api/Dockerfile.j2
index 112ff8b2df..016b3560bd 100644
--- a/docker/ironic/ironic-api/Dockerfile.j2
+++ b/docker/ironic/ironic-api/Dockerfile.j2
@@ -4,15 +4,16 @@ MAINTAINER Kolla Project (https://launchpad.net/kolla)
 {% if install_type == 'binary' %}
     {% if base_distro in ['centos', 'fedora', 'oraclelinux', 'rhel'] %}
 
-RUN yum -y install \
-    openstack-ironic-api \
+# Install delorean version even though version number is less
+# http://lists.openstack.org/pipermail/openstack-dev/2015-August/073100.html
+RUN VER_TO_GET=$(yum --showduplicates list openstack-ironic-api | awk '/delorean/ {print $2}') \
+    && yum -y install openstack-ironic-api-$VER_TO_GET \
     && yum clean all
 
     {% endif %}
 {% endif %}
 
 COPY start.sh /
-COPY config-external.sh /opt/kolla/
 
 CMD ["/start.sh"]
 
diff --git a/docker/ironic/ironic-api/start.sh b/docker/ironic/ironic-api/start.sh
index 9d449defbe..60b3ea5a08 100755
--- a/docker/ironic/ironic-api/start.sh
+++ b/docker/ironic/ironic-api/start.sh
@@ -1,10 +1,13 @@
 #!/bin/bash
 set -o errexit
 
-CMD="/usr/bin/ironic-api"
-ARGS=""
-
 source /opt/kolla/kolla-common.sh
-set_configs
 
-exec $CMD $ARGS
+# Bootstrap and exit if KOLLA_BOOTSTRAP variable is set. This catches all cases
+# of the KOLLA_BOOTSTRAP variable being set, including empty.
+if [[ "${!KOLLA_BOOTSTRAP[@]}" ]]; then
+    su -s /bin/sh -c "ironic-dbsync upgrade" ironic
+    exit 0
+fi
+
+exec $CMD
diff --git a/docker/ironic/ironic-base/Dockerfile.j2 b/docker/ironic/ironic-base/Dockerfile.j2
index 2f8207c0ea..bb05d2dd75 100644
--- a/docker/ironic/ironic-base/Dockerfile.j2
+++ b/docker/ironic/ironic-base/Dockerfile.j2
@@ -10,6 +10,7 @@ RUN yum -y install \
         python-oslo-log \
         python-oslo-concurrency \
         python-oslo-policy \
+        python-oslo-versionedobjects \
     && yum clean all
 
     {% endif %}
diff --git a/docker/ironic/ironic-conductor/Dockerfile.j2 b/docker/ironic/ironic-conductor/Dockerfile.j2
index f62f7b7a0f..7fe401b4d1 100644
--- a/docker/ironic/ironic-conductor/Dockerfile.j2
+++ b/docker/ironic/ironic-conductor/Dockerfile.j2
@@ -4,15 +4,16 @@ MAINTAINER Kolla Project (https://launchpad.net/kolla)
 {% if install_type == 'binary' %}
     {% if base_distro in ['centos', 'fedora', 'oraclelinux', 'rhel'] %}
 
-RUN yum -y install \
-    openstack-ironic-conductor \
+# Install delorean version even though version number is less
+# http://lists.openstack.org/pipermail/openstack-dev/2015-August/073100.html
+RUN VER_TO_GET=$(yum --showduplicates list openstack-ironic-conductor | awk '/delorean/ {print $2}') \
+    && yum -y install openstack-ironic-conductor-$VER_TO_GET \
     && yum clean all
 
     {% endif %}
 {% endif %}
 
 COPY start.sh /
-COPY config-external.sh /opt/kolla/
 
 CMD ["/start.sh"]
 
diff --git a/docker/ironic/ironic-conductor/config-external.sh b/docker/ironic/ironic-conductor/config-external.sh
deleted file mode 100644
index b838ee19d0..0000000000
--- a/docker/ironic/ironic-conductor/config-external.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-SOURCE="/opt/kolla/ironic-api/ironic.conf"
-TARGET="/etc/ironic/ironic.conf"
-OWNER="ironic"
-
-if [[ -f "$SOURCE" ]]; then
-    cp $SOURCE $TARGET
-    chown ${OWNER}: $TARGET
-    chmod 0644 $TARGET
-fi
diff --git a/docker/ironic/ironic-conductor/start.sh b/docker/ironic/ironic-conductor/start.sh
index c766f04c5b..e6b7c5bb5f 100755
--- a/docker/ironic/ironic-conductor/start.sh
+++ b/docker/ironic/ironic-conductor/start.sh
@@ -1,10 +1,6 @@
 #!/bin/bash
 set -o errexit
 
-CMD="/usr/bin/ironic-conductor"
-ARGS=""
-
 source /opt/kolla/kolla-common.sh
-set_configs
 
-exec $CMD $ARGS
+exec $CMD
diff --git a/docker/ironic/ironic-discoverd/Dockerfile.j2 b/docker/ironic/ironic-discoverd/Dockerfile.j2
index 1843cb24cf..5e15c8d585 100644
--- a/docker/ironic/ironic-discoverd/Dockerfile.j2
+++ b/docker/ironic/ironic-discoverd/Dockerfile.j2
@@ -4,15 +4,22 @@ MAINTAINER Kolla Project (https://launchpad.net/kolla)
 {% if install_type == 'binary' %}
     {% if base_distro in ['centos', 'fedora', 'oraclelinux', 'rhel'] %}
 
-RUN yum -y install \
-    openstack-ironic-discoverd \
-    && yum clean all
+RUN pip install ironic-discoverd
+# discoverd no longer in delorean 9/28/2015, switch to inspector on TODO
+#RUN yum -y install \
+#    openstack-ironic-discoverd \
+#    && yum clean all
 
     {% endif %}
+
+{% elif install_type == 'source' %}
+
+RUN echo '{{ install_type }} not yet available for {{ base_distro }}' \
+    && /bin/false
+
 {% endif %}
 
 COPY start.sh /
-COPY config-external.sh /opt/kolla/
 
 CMD ["/start.sh"]
 
diff --git a/docker/ironic/ironic-discoverd/config-external.sh b/docker/ironic/ironic-discoverd/config-external.sh
deleted file mode 100644
index a3f82b8f30..0000000000
--- a/docker/ironic/ironic-discoverd/config-external.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-
-SOURCE="/opt/kolla/ironic-discoverd/discoverd.conf"
-TARGET="/etc/ironic-discoverd/discoverd.conf"
-OWNER="ironic"
-
-if [[ -f "$SOURCE" ]]; then
-    cp $SOURCE $TARGET
-    chown ${OWNER}: $TARGET
-    chmod 0644 $TARGET
-fi
diff --git a/docker/ironic/ironic-discoverd/start.sh b/docker/ironic/ironic-discoverd/start.sh
index 093dc517af..e6b7c5bb5f 100755
--- a/docker/ironic/ironic-discoverd/start.sh
+++ b/docker/ironic/ironic-discoverd/start.sh
@@ -1,10 +1,6 @@
 #!/bin/bash
 set -o errexit
 
-CMD="/usr/bin/ironic-discoverd"
-ARGS=""
-
 source /opt/kolla/kolla-common.sh
-set_configs
 
-exec $CMD $ARGS
+exec $CMD
diff --git a/docker/ironic/ironic-pxe/Dockerfile.j2 b/docker/ironic/ironic-pxe/Dockerfile.j2
new file mode 100644
index 0000000000..d4484d1dba
--- /dev/null
+++ b/docker/ironic/ironic-pxe/Dockerfile.j2
@@ -0,0 +1,35 @@
+FROM {{ namespace }}/{{ base_distro }}-{{ install_type }}-base:{{ tag }}
+MAINTAINER Kolla Project (https://launchpad.net/kolla)
+
+{% if install_type == 'binary' %}
+    {% if base_distro in ['centos', 'fedora', 'oraclelinux', 'rhel'] %}
+
+RUN yum -y install tftp-server syslinux-tftpboot \
+    && yum clean all
+
+# PXE configuration
+RUN mkdir -p /tftpboot \
+    && cp /var/lib/tftpboot/chain.c32 /tftpboot \
+    && echo 're ^(/tftpboot/) /tftpboot/\2' > /tftpboot/map-file \
+    && echo 're ^/tftpboot/ /tftpboot/' >> /tftpboot/map-file \
+    && echo 're ^(^/) /tftpboot/\1' >> /tftpboot/map-file \
+    && echo 're ^([^/]) /tftpboot/\1' >> /tftpboot/map-file
+
+    {% elif base_distro in ['ubuntu', 'debian'] %}
+RUN echo '{{ install_type }} not yet available for {{ base_distro }}' \
+    && /bin/false
+
+    {% endif %}
+
+{% elif install_type == 'source' %}
+
+RUN echo '{{ install_type }} not yet available for {{ base_distro }}' \
+    && /bin/false
+
+{% endif %}
+
+COPY start.sh /
+
+CMD ["/start.sh"]
+
+{{ include_footer }}
diff --git a/docker/ironic/ironic-pxe/start.sh b/docker/ironic/ironic-pxe/start.sh
new file mode 100755
index 0000000000..e6b7c5bb5f
--- /dev/null
+++ b/docker/ironic/ironic-pxe/start.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -o errexit
+
+source /opt/kolla/kolla-common.sh
+
+exec $CMD
diff --git a/etc/kolla/config/discoverd.conf b/etc/kolla/config/discoverd.conf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/etc/kolla/config/ironic.conf b/etc/kolla/config/ironic.conf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/etc/kolla/config/ironic/discoverd.conf b/etc/kolla/config/ironic/discoverd.conf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/etc/kolla/config/ironic/ironic-api.conf b/etc/kolla/config/ironic/ironic-api.conf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/etc/kolla/config/ironic/ironic-conductor.conf b/etc/kolla/config/ironic/ironic-conductor.conf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml
index b50ddd3dd8..04a30e7975 100644
--- a/etc/kolla/passwords.yml
+++ b/etc/kolla/passwords.yml
@@ -53,6 +53,9 @@ heat_domain_admin_password: "password"
 murano_database_password: "password"
 murano_keystone_password: "password"
 
+ironic_database_password: "password"
+ironic_keystone_password: "password"
+
 
 ####################
 # RabbitMQ options
diff --git a/tests/test_build.py b/tests/test_build.py
index a58327a510..8feb98d00d 100644
--- a/tests/test_build.py
+++ b/tests/test_build.py
@@ -39,7 +39,9 @@ class BuildTest(base.BaseTestCase):
 
         # these are images that are known to not build properly
         excluded_images = ["gnocchi-base",
-                           "murano-base"]
+                           "murano-base",
+                           "ironic-pxe",
+                           "ironic-discoverd"]
 
         failures = 0
         for image, result in bad_results.iteritems():