From 2fc6d4cfc5aaaf915b45b4343135794b98105ca3 Mon Sep 17 00:00:00 2001
From: Eduardo Gonzalez <dabarren@gmail.com>
Date: Fri, 26 Oct 2018 18:13:48 +0200
Subject: [PATCH] Split placement from nova

Depends-On: https://review.openstack.org/#/c/642958
Depends-On: https://review.openstack.org/642984
Change-Id: If795a9eb3ec92f75867ce3f755d6b832eba31af9
---
 ansible/group_vars/all.yml                    |   1 +
 ansible/roles/nova/defaults/main.yml          |  34 -----
 ansible/roles/nova/handlers/main.yml          |  26 ----
 ansible/roles/nova/tasks/config.yml           |  16 ---
 ansible/roles/nova/tasks/precheck.yml         |  15 ---
 ansible/roles/nova/tasks/register.yml         |  17 ---
 .../nova/templates/placement-api.json.j2      |  32 -----
 ansible/roles/placement/defaults/main.yml     |  87 ++++++++++++
 ansible/roles/placement/handlers/main.yml     |  15 +++
 ansible/roles/placement/meta/main.yml         |   3 +
 ansible/roles/placement/tasks/bootstrap.yml   |  65 +++++++++
 .../placement/tasks/bootstrap_service.yml     |  20 +++
 ansible/roles/placement/tasks/check.yml       |   1 +
 ansible/roles/placement/tasks/clone.yml       |   7 +
 ansible/roles/placement/tasks/config.yml      | 124 ++++++++++++++++++
 ansible/roles/placement/tasks/deploy.yml      |  14 ++
 .../roles/placement/tasks/loadbalancer.yml    |   7 +
 ansible/roles/placement/tasks/main.yml        |   2 +
 ansible/roles/placement/tasks/precheck.yml    |  20 +++
 ansible/roles/placement/tasks/pull.yml        |  11 ++
 ansible/roles/placement/tasks/reconfigure.yml |   2 +
 ansible/roles/placement/tasks/register.yml    |  32 +++++
 ansible/roles/placement/tasks/stop.yml        |   6 +
 ansible/roles/placement/tasks/upgrade.yml     |  42 ++++++
 .../placement/templates/migrate-db.rc.j2      |   8 ++
 .../templates/placement-api-wsgi.conf.j2      |  21 +--
 .../placement/templates/placement-api.json.j2 |  42 ++++++
 .../placement/templates/placement.conf.j2     |  61 +++++++++
 ansible/site.yml                              |  16 +++
 etc/kolla/globals.yml                         |   1 +
 etc/kolla/passwords.yml                       |   1 +
 tools/setup_gate.sh                           |   2 +-
 32 files changed, 600 insertions(+), 151 deletions(-)
 delete mode 100644 ansible/roles/nova/templates/placement-api.json.j2
 create mode 100644 ansible/roles/placement/defaults/main.yml
 create mode 100644 ansible/roles/placement/handlers/main.yml
 create mode 100644 ansible/roles/placement/meta/main.yml
 create mode 100644 ansible/roles/placement/tasks/bootstrap.yml
 create mode 100644 ansible/roles/placement/tasks/bootstrap_service.yml
 create mode 100644 ansible/roles/placement/tasks/check.yml
 create mode 100644 ansible/roles/placement/tasks/clone.yml
 create mode 100644 ansible/roles/placement/tasks/config.yml
 create mode 100644 ansible/roles/placement/tasks/deploy.yml
 create mode 100644 ansible/roles/placement/tasks/loadbalancer.yml
 create mode 100644 ansible/roles/placement/tasks/main.yml
 create mode 100644 ansible/roles/placement/tasks/precheck.yml
 create mode 100644 ansible/roles/placement/tasks/pull.yml
 create mode 100644 ansible/roles/placement/tasks/reconfigure.yml
 create mode 100644 ansible/roles/placement/tasks/register.yml
 create mode 100644 ansible/roles/placement/tasks/stop.yml
 create mode 100644 ansible/roles/placement/tasks/upgrade.yml
 create mode 100644 ansible/roles/placement/templates/migrate-db.rc.j2
 rename ansible/roles/{nova => placement}/templates/placement-api-wsgi.conf.j2 (61%)
 create mode 100644 ansible/roles/placement/templates/placement-api.json.j2
 create mode 100644 ansible/roles/placement/templates/placement.conf.j2

diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index c227b14edf..4f9ca56414 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -600,6 +600,7 @@ enable_openvswitch: "{{ enable_neutron | bool and neutron_plugin_agent != 'linux
 enable_ovs_dpdk: "no"
 enable_osprofiler: "no"
 enable_panko: "no"
+enable_placement: "{{ enable_nova }}"
 enable_qdrouterd: "{{ 'yes' if om_rpc_transport == 'amqp' else 'no' }}"
 enable_rally: "no"
 enable_redis: "no"
diff --git a/ansible/roles/nova/defaults/main.yml b/ansible/roles/nova/defaults/main.yml
index cc35dd4eb4..8b0152ef3d 100644
--- a/ansible/roles/nova/defaults/main.yml
+++ b/ansible/roles/nova/defaults/main.yml
@@ -36,30 +36,6 @@ nova_services:
       - "{% if enable_shared_var_lib_nova_mnt | bool %}/var/lib/nova/mnt:/var/lib/nova/mnt:shared{% endif %}"
       - "{{ kolla_dev_repos_directory ~ '/nova/nova:/var/lib/kolla/venv/lib/python2.7/site-packages/nova' if nova_dev_mode | bool else '' }}"
     dimensions: "{{ nova_ssh_dimensions }}"
-  placement-api:
-    container_name: "placement_api"
-    group: "placement-api"
-    image: "{{ placement_api_image_full }}"
-    enabled: True
-    volumes:
-      - "{{ node_config_directory }}/placement-api/:{{ container_config_directory }}/:ro"
-      - "/etc/localtime:/etc/localtime:ro"
-      - "kolla_logs:/var/log/kolla/"
-      - "{{ kolla_dev_repos_directory ~ '/nova/nova:/var/lib/kolla/venv/lib/python2.7/site-packages/nova' if nova_dev_mode | bool else '' }}"
-    dimensions: "{{ placement_api_dimensions }}"
-    haproxy:
-      placement_api:
-        enabled: "{{ enable_nova }}"
-        mode: "http"
-        external: false
-        port: "{{ placement_api_port }}"
-        listen_port: "{{ placement_api_listen_port }}"
-      placement_api_external:
-        enabled: "{{ enable_nova }}"
-        mode: "http"
-        external: true
-        port: "{{ placement_api_port }}"
-        listen_port: "{{ placement_api_listen_port }}"
   nova-api:
     container_name: "nova_api"
     group: "nova-api"
@@ -341,13 +317,8 @@ nova_serialproxy_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{
 nova_serialproxy_tag: "{{ nova_tag }}"
 nova_serialproxy_image_full: "{{ nova_serialproxy_image }}:{{ nova_serialproxy_tag }}"
 
-placement_api_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ nova_install_type }}-nova-placement-api"
-placement_api_tag: "{{ nova_tag }}"
-placement_api_image_full: "{{ placement_api_image }}:{{ placement_api_tag }}"
-
 nova_libvirt_dimensions: "{{ default_container_dimensions }}"
 nova_ssh_dimensions: "{{ default_container_dimensions }}"
-placement_api_dimensions: "{{ default_container_dimensions }}"
 nova_api_dimensions: "{{ default_container_dimensions }}"
 nova_consoleauth_dimensions: "{{ default_container_dimensions }}"
 nova_novncproxy_dimensions: "{{ default_container_dimensions }}"
@@ -375,14 +346,9 @@ nova_admin_endpoint: "{{ admin_protocol }}://{{ nova_internal_fqdn }}:{{ nova_ap
 nova_internal_endpoint: "{{ internal_protocol }}://{{ nova_internal_fqdn }}:{{ nova_api_port }}/v2.1/%(tenant_id)s"
 nova_public_endpoint: "{{ public_protocol }}://{{ nova_external_fqdn }}:{{ nova_api_port }}/v2.1/%(tenant_id)s"
 
-placement_admin_endpoint: "{{ admin_protocol }}://{{ placement_internal_fqdn }}:{{ placement_api_port }}"
-placement_internal_endpoint: "{{ internal_protocol }}://{{ placement_internal_fqdn }}:{{ placement_api_port }}"
-placement_public_endpoint: "{{ public_protocol }}://{{ placement_external_fqdn }}:{{ placement_api_port }}"
-
 nova_logging_debug: "{{ openstack_logging_debug }}"
 
 openstack_nova_auth: "{{ openstack_auth }}"
-openstack_placement_auth: "{{ openstack_auth }}"
 
 nova_compute_host_rp_filter_mode: 0
 nova_enable_rolling_upgrade: "yes"
diff --git a/ansible/roles/nova/handlers/main.yml b/ansible/roles/nova/handlers/main.yml
index a634128179..b46d00fc55 100644
--- a/ansible/roles/nova/handlers/main.yml
+++ b/ansible/roles/nova/handlers/main.yml
@@ -1,30 +1,4 @@
 ---
-- name: Restart placement-api container
-  vars:
-    service_name: "placement-api"
-    service: "{{ nova_services[service_name] }}"
-    config_json: "{{ config_jsons.results|selectattr('item.key', 'equalto', service_name)|first }}"
-    nova_conf: "{{ nova_confs.results|selectattr('item.key', 'equalto', service_name)|first }}"
-    policy_overwriting: "{{ nova_policy_overwriting.results|selectattr('item.key', 'equalto', service_name)|first }}"
-    placement_api_container: "{{ check_nova_containers.results|selectattr('item.key', 'equalto', service_name)|first }}"
-  become: true
-  kolla_docker:
-    action: "recreate_or_restart_container"
-    common_options: "{{ docker_common_options }}"
-    name: "{{ service.container_name }}"
-    image: "{{ service.image }}"
-    volumes: "{{ service.volumes|reject('equalto', '')|list }}"
-    dimensions: "{{ service.dimensions }}"
-  when:
-    - kolla_action != "config"
-    - inventory_hostname in groups[service.group]
-    - service.enabled | bool
-    - config_json.changed | bool
-      or nova_conf.changed | bool
-      or policy_overwriting.changed | bool
-      or placement_api_wsgi_conf | changed
-      or placement_api_container.changed | bool
-
 - name: Restart nova-conductor container
   vars:
     service_name: "nova-conductor"
diff --git a/ansible/roles/nova/tasks/config.yml b/ansible/roles/nova/tasks/config.yml
index 22c537b1cd..60cab45be8 100644
--- a/ansible/roles/nova/tasks/config.yml
+++ b/ansible/roles/nova/tasks/config.yml
@@ -82,7 +82,6 @@
   become: true
   vars:
     services_require_nova_conf:
-      - placement-api
       - nova-api
       - nova-compute
       - nova-compute-ironic
@@ -129,20 +128,6 @@
   notify:
     - Restart nova-libvirt container
 
-- name: Copying over placement-api wsgi configuration
-  become: true
-  vars:
-    service: "{{ nova_services['placement-api'] }}"
-  template:
-    src: "placement-api-wsgi.conf.j2"
-    dest: "{{ node_config_directory }}/placement-api/placement-api-wsgi.conf"
-  register: placement_api_wsgi_conf
-  when:
-    - inventory_hostname in groups[service.group]
-    - service.enabled | bool
-  notify:
-    - Restart placement-api container
-
 - name: Copying files for nova-ssh
   become: true
   vars:
@@ -201,7 +186,6 @@
   become: true
   vars:
     services_require_policy_json:
-      - placement-api
       - nova-api
       - nova-compute
       - nova-compute-ironic
diff --git a/ansible/roles/nova/tasks/precheck.yml b/ansible/roles/nova/tasks/precheck.yml
index d20fbb74b2..8a639cb012 100644
--- a/ansible/roles/nova/tasks/precheck.yml
+++ b/ansible/roles/nova/tasks/precheck.yml
@@ -8,7 +8,6 @@
       - nova_spicehtml5proxy
       - nova_ssh
       - nova_libvirt
-      - placement_api
   register: container_facts
 
 - name: Checking available compute nodes in inventory
@@ -119,20 +118,6 @@
     - nova_libvirt.enabled | bool
     - inventory_hostname in groups[nova_libvirt.group]
 
-- name: Checking free port for Nova Placement API
-  vars:
-    placement_api: "{{ nova_services['placement-api'] }}"
-  wait_for:
-    host: "{{ api_interface_address }}"
-    port: "{{ placement_api_listen_port }}"
-    connect_timeout: 1
-    timeout: 1
-    state: stopped
-  when:
-    - container_facts['placement_api'] is not defined
-    - inventory_hostname in groups[placement_api.group]
-    - placement_api.enabled | bool
-
 - name: Checking that libvirt is not running
   vars:
     nova_libvirt: "{{ nova_services['nova-libvirt'] }}"
diff --git a/ansible/roles/nova/tasks/register.yml b/ansible/roles/nova/tasks/register.yml
index 60a6c1c7ae..e0d0acb9c6 100644
--- a/ansible/roles/nova/tasks/register.yml
+++ b/ansible/roles/nova/tasks/register.yml
@@ -20,10 +20,6 @@
     - {'name': 'nova', 'service_type': 'compute', 'interface': 'admin', 'url': '{{ nova_admin_endpoint }}', 'description': 'OpenStack Compute Service'}
     - {'name': 'nova', 'service_type': 'compute', 'interface': 'internal', 'url': '{{ nova_internal_endpoint }}', 'description': 'OpenStack Compute Service'}
     - {'name': 'nova', 'service_type': 'compute', 'interface': 'public', 'url': '{{ nova_public_endpoint }}', 'description': 'OpenStack Compute Service'}
-    - {'name': 'placement', 'service_type': 'placement', 'interface': 'admin', 'url': '{{ placement_admin_endpoint }}', 'description': 'Placement Service'}
-    - {'name': 'placement', 'service_type': 'placement', 'interface': 'internal', 'url': '{{ placement_internal_endpoint }}', 'description': 'Placement Service'}
-    - {'name': 'placement', 'service_type': 'placement', 'interface': 'public', 'url': '{{ placement_public_endpoint }}', 'description': 'Placement Service'}
-
 
 - name: Creating the Nova project, user, and role
   kolla_toolbox:
@@ -37,16 +33,3 @@
       auth: "{{ openstack_nova_auth }}"
       endpoint_type: "{{ openstack_interface }}"
   run_once: True
-
-- name: Creating the placement project, user, and role
-  kolla_toolbox:
-    module_name: "kolla_keystone_user"
-    module_args:
-      project: "service"
-      user: "{{ placement_keystone_user }}"
-      password: "{{ placement_keystone_password }}"
-      role: "admin"
-      region_name: "{{ openstack_region_name }}"
-      auth: "{{ openstack_placement_auth }}"
-      endpoint_type: "{{ openstack_interface }}"
-  run_once: True
diff --git a/ansible/roles/nova/templates/placement-api.json.j2 b/ansible/roles/nova/templates/placement-api.json.j2
deleted file mode 100644
index 395f7d06fa..0000000000
--- a/ansible/roles/nova/templates/placement-api.json.j2
+++ /dev/null
@@ -1,32 +0,0 @@
-{% set apache_binary = 'apache2' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd' %}
-{% set apache_conf_dir = 'apache2/conf-enabled' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd/conf.d' %}
-{
-    "command": "/usr/sbin/{{ apache_binary }} -DFOREGROUND",
-    "config_files": [
-        {
-            "source": "{{ container_config_directory }}/nova.conf",
-            "dest": "/etc/nova/nova.conf",
-            "owner": "nova",
-            "perm": "0600"
-        }{% if nova_policy_file is defined %},
-        {
-            "source": "{{ container_config_directory }}/{{ nova_policy_file }}",
-            "dest": "/etc/nova/{{ nova_policy_file }}",
-            "owner": "nova",
-            "perm": "0600"
-        }{% endif %},
-        {
-            "source": "{{ container_config_directory }}/placement-api-wsgi.conf",
-            "dest": "/etc/{{ apache_conf_dir }}/placement-api-wsgi.conf",
-            "owner": "nova",
-            "perm": "0600"
-        }
-    ],
-    "permissions": [
-        {
-            "path": "/var/log/kolla/nova",
-            "owner": "nova:nova",
-            "recurse": true
-        }
-    ]
-}
diff --git a/ansible/roles/placement/defaults/main.yml b/ansible/roles/placement/defaults/main.yml
new file mode 100644
index 0000000000..a25496f28d
--- /dev/null
+++ b/ansible/roles/placement/defaults/main.yml
@@ -0,0 +1,87 @@
+---
+project_name: "placement"
+
+placement_services:
+  placement-api:
+    container_name: "placement_api"
+    group: "placement-api"
+    image: "{{ placement_api_image_full }}"
+    enabled: True
+    volumes:
+      - "{{ node_config_directory }}/placement-api/:{{ container_config_directory }}/:ro"
+      - "/etc/localtime:/etc/localtime:ro"
+      - "kolla_logs:/var/log/kolla/"
+      - "{{ kolla_dev_repos_directory ~ '/placement/placement:/var/lib/kolla/venv/lib/python2.7/site-packages/placement' if placement_dev_mode | bool else '' }}"
+    dimensions: "{{ placement_api_dimensions }}"
+    haproxy:
+      placement_api:
+        enabled: "{{ enable_placement }}"
+        mode: "http"
+        external: false
+        port: "{{ placement_api_port }}"
+        listen_port: "{{ placement_api_listen_port }}"
+      placement_api_external:
+        enabled: "{{ enable_placement }}"
+        mode: "http"
+        external: true
+        port: "{{ placement_api_port }}"
+        listen_port: "{{ placement_api_listen_port }}"
+
+####################
+# Database
+####################
+placement_database_name: "placement"
+placement_database_user: "{% if use_preconfigured_databases | bool and use_common_mariadb_user | bool %}{{ database_user }}{% else %}placement{% endif %}"
+placement_database_address: "{{ database_address }}:{{ database_port }}"
+
+####################
+# Docker
+####################
+placement_install_type: "{{ kolla_install_type }}"
+placement_tag: "{{ openstack_release }}"
+
+placement_api_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/{{ kolla_base_distro }}-{{ placement_install_type }}-placement-api"
+placement_api_tag: "{{ placement_tag }}"
+placement_api_image_full: "{{ placement_api_image }}:{{ placement_api_tag }}"
+
+placement_api_dimensions: "{{ default_container_dimensions }}"
+
+####################
+# OpenStack
+####################
+placement_admin_endpoint: "{{ admin_protocol }}://{{ placement_internal_fqdn }}:{{ placement_api_port }}"
+placement_internal_endpoint: "{{ internal_protocol }}://{{ placement_internal_fqdn }}:{{ placement_api_port }}"
+placement_public_endpoint: "{{ public_protocol }}://{{ placement_external_fqdn }}:{{ placement_api_port }}"
+
+placement_logging_debug: "{{ openstack_logging_debug }}"
+
+openstack_placement_auth: "{{ openstack_auth }}"
+
+
+####################
+# Notification
+####################
+placement_notification_topics:
+  - name: notifications
+    enabled: "{{ enable_ceilometer | bool or enable_searchlight | bool or enable_neutron_infoblox_ipam_agent | bool }}"
+  - name: notifications_designate
+    enabled: "{{ enable_designate | bool }}"
+
+placement_enabled_notification_topics: "{{ placement_notification_topics | selectattr('enabled', 'equalto', true) | list }}"
+
+
+####################
+# Kolla
+####################
+placement_git_repository: "{{ kolla_dev_repos_git }}/{{ project_name }}"
+placement_dev_repos_pull: "{{ kolla_dev_repos_pull }}"
+placement_dev_mode: "{{ kolla_dev_mode }}"
+placement_source_version: "{{ kolla_source_version }}"
+
+####################
+# Upgrade
+####################
+nova_api_database_name: "nova_api"
+nova_api_database_user: "{% if use_preconfigured_databases | bool and use_common_mariadb_user | bool %}{{ database_user }}{% else %}nova_api{% endif %}"
+nova_api_database_host: "{{ database_address }}"
+placement_database_host: "{{ database_address }}"
diff --git a/ansible/roles/placement/handlers/main.yml b/ansible/roles/placement/handlers/main.yml
new file mode 100644
index 0000000000..ffeb23b499
--- /dev/null
+++ b/ansible/roles/placement/handlers/main.yml
@@ -0,0 +1,15 @@
+---
+- name: Restart placement-api container
+  become: true
+  vars:
+    service_name: "placement-api"
+    service: "{{ placement_services[service_name] }}"
+  kolla_docker:
+    action: "recreate_or_restart_container"
+    common_options: "{{ docker_common_options }}"
+    name: "{{ service.container_name }}"
+    image: "{{ service.image }}"
+    volumes: "{{ service.volumes|reject('equalto', '')|list }}"
+    dimensions: "{{ service.dimensions }}"
+  when:
+    - kolla_action != "config"
diff --git a/ansible/roles/placement/meta/main.yml b/ansible/roles/placement/meta/main.yml
new file mode 100644
index 0000000000..6b4fff8fef
--- /dev/null
+++ b/ansible/roles/placement/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+  - { role: common }
diff --git a/ansible/roles/placement/tasks/bootstrap.yml b/ansible/roles/placement/tasks/bootstrap.yml
new file mode 100644
index 0000000000..0628807782
--- /dev/null
+++ b/ansible/roles/placement/tasks/bootstrap.yml
@@ -0,0 +1,65 @@
+---
+- name: Creating placement databases
+  kolla_toolbox:
+    module_name: mysql_db
+    module_args:
+      login_host: "{{ database_address }}"
+      login_port: "{{ database_port }}"
+      login_user: "{{ database_user }}"
+      login_password: "{{ database_password }}"
+      name: "{{ placement_database_name }}"
+  register: database
+  run_once: True
+  delegate_to: "{{ groups['placement-api'][0] }}"
+  when:
+    - not use_preconfigured_databases | bool
+
+- name: Creating placement databases user and setting permissions
+  kolla_toolbox:
+    module_name: mysql_user
+    module_args:
+      login_host: "{{ database_address }}"
+      login_port: "{{ database_port }}"
+      login_user: "{{ database_user }}"
+      login_password: "{{ database_password }}"
+      name: "{{ placement_database_user }}"
+      password: "{{ placement_database_password }}"
+      host: "%"
+      priv: "{{ placement_database_name }}.*:ALL"
+      append_privs: "yes"
+  run_once: True
+  delegate_to: "{{ groups['placement-api'][0] }}"
+  when:
+    - database.changed
+    - not use_preconfigured_databases | bool
+
+# TODO(egonzalez): Remove this task once stein is release as will not be required to migrate data.
+# Error codes https://github.com/openstack/placement/blob/master/tools/mysql-migrate-db.sh#L230
+- name: Migrate placement database
+  vars:
+    placement_api: "{{ placement_services['placement-api'] }}"
+  become: true
+  kolla_docker:
+    action: "start_container"
+    command: bash -c 'sudo -E kolla_set_configs && bash /opt/mysql-migrate-db.sh --migrate /etc/placement/migrate-db.rc'
+    common_options: "{{ docker_common_options }}"
+    detach: False
+    image: "{{ placement_api.image }}"
+    labels:
+      BOOTSTRAP:
+    name: "migrate_placement_database"
+    restart_policy: "never"
+    volumes: "{{ placement_api.volumes|reject('equalto', '')|list }}"
+  register: migrate_placement
+  changed_when:
+    - migrate_placement is success
+    - migrate_placement.rc == 0
+  failed_when:
+    - migrate_placement.rc not in [0, 3, 4]
+  run_once: True
+  delegate_to: "{{ groups[placement_api.group][0] }}"
+  when:
+    - kolla_action == "upgrade"
+
+- include_tasks: bootstrap_service.yml
+  when: database.changed or use_preconfigured_databases | bool
diff --git a/ansible/roles/placement/tasks/bootstrap_service.yml b/ansible/roles/placement/tasks/bootstrap_service.yml
new file mode 100644
index 0000000000..7d7b8e7b4f
--- /dev/null
+++ b/ansible/roles/placement/tasks/bootstrap_service.yml
@@ -0,0 +1,20 @@
+---
+- name: Running placement bootstrap container
+  vars:
+    placement_api: "{{ placement_services['placement-api'] }}"
+  become: true
+  kolla_docker:
+    action: "start_container"
+    common_options: "{{ docker_common_options }}"
+    detach: False
+    environment:
+      KOLLA_BOOTSTRAP:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+    image: "{{ placement_api.image }}"
+    labels:
+      BOOTSTRAP:
+    name: "bootstrap_placement"
+    restart_policy: "never"
+    volumes: "{{ placement_api.volumes|reject('equalto', '')|list }}"
+  run_once: True
+  delegate_to: "{{ groups[placement_api.group][0] }}"
diff --git a/ansible/roles/placement/tasks/check.yml b/ansible/roles/placement/tasks/check.yml
new file mode 100644
index 0000000000..ed97d539c0
--- /dev/null
+++ b/ansible/roles/placement/tasks/check.yml
@@ -0,0 +1 @@
+---
diff --git a/ansible/roles/placement/tasks/clone.yml b/ansible/roles/placement/tasks/clone.yml
new file mode 100644
index 0000000000..cfe3fe3436
--- /dev/null
+++ b/ansible/roles/placement/tasks/clone.yml
@@ -0,0 +1,7 @@
+---
+- name: Cloning placement source repository for development
+  git:
+    repo: "{{ placement_git_repository }}"
+    dest: "{{ kolla_dev_repos_directory }}/{{ project_name }}"
+    update: "{{ placement_dev_repos_pull }}"
+    version: "{{ placement_source_version }}"
diff --git a/ansible/roles/placement/tasks/config.yml b/ansible/roles/placement/tasks/config.yml
new file mode 100644
index 0000000000..f8863f5af5
--- /dev/null
+++ b/ansible/roles/placement/tasks/config.yml
@@ -0,0 +1,124 @@
+---
+- name: Ensuring config directories exist
+  become: true
+  file:
+    path: "{{ node_config_directory }}/{{ item.key }}"
+    state: "directory"
+    owner: "{{ config_owner_user }}"
+    group: "{{ config_owner_group }}"
+    mode: "0770"
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ placement_services }}"
+
+- name: Check if policies shall be overwritten
+  local_action: stat path="{{ item }}"
+  run_once: True
+  register: placement_policy
+  with_first_found:
+    - files: "{{ supported_policy_format_list }}"
+      paths:
+        - "{{ node_custom_config }}/placement/"
+      skip: true
+
+- name: Set placement policy file
+  set_fact:
+    placement_policy_file: "{{ placement_policy.results.0.stat.path | basename }}"
+    placement_policy_file_path: "{{ placement_policy.results.0.stat.path }}"
+  when:
+    - placement_policy.results
+
+- name: Copying over config.json files for services
+  become: true
+  template:
+    src: "{{ item.key }}.json.j2"
+    dest: "{{ node_config_directory }}/{{ item.key }}/config.json"
+    mode: "0770"
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ placement_services }}"
+  notify:
+    - "Restart {{ item.key }} container"
+
+- name: Copying over placement.conf
+  become: true
+  vars:
+    service_name: "{{ item.key }}"
+  merge_configs:
+    sources:
+      - "{{ role_path }}/templates/placement.conf.j2"
+      - "{{ node_custom_config }}/global.conf"
+      - "{{ node_custom_config }}/placement.conf"
+      - "{{ node_custom_config }}/placement/{{ item.key }}.conf"
+      - "{{ node_custom_config }}/placement/{{ inventory_hostname }}/placement.conf"
+    dest: "{{ node_config_directory }}/{{ item.key }}/placement.conf"
+    mode: "0660"
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ placement_services }}"
+  notify:
+    - "Restart {{ item.key }} container"
+
+- name: Copying over placement-api wsgi configuration
+  become: true
+  vars:
+    service: "{{ placement_services['placement-api'] }}"
+  template:
+    src: "placement-api-wsgi.conf.j2"
+    dest: "{{ node_config_directory }}/placement-api/placement-api-wsgi.conf"
+  when:
+    - inventory_hostname in groups[service.group]
+    - service.enabled | bool
+  notify:
+    - Restart placement-api container
+
+- name: Copying over migrate-db.rc.j2 configuration
+  become: true
+  vars:
+    service: "{{ placement_services['placement-api'] }}"
+  template:
+    src: "migrate-db.rc.j2"
+    dest: "{{ node_config_directory }}/placement-api/migrate-db.rc"
+  when:
+    - inventory_hostname in groups[service.group]
+    - service.enabled | bool
+  notify:
+    - Restart placement-api container
+
+- name: Copying over existing policy file
+  become: true
+  template:
+    src: "{{ placement_policy_file_path }}"
+    dest: "{{ placement_config_directory }}/{{ item.key }}/{{ placement_policy_file }}"
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+    - placement_policy_file is defined
+  with_dict: "{{ placement_services }}"
+  notify:
+    - "Restart {{ item.key }} container"
+
+# check whether the containers parameter is changed. If yes, trigger the handler
+- name: Check placement containers
+  become: true
+  kolla_docker:
+    action: "compare_container"
+    common_options: "{{ docker_common_options }}"
+    name: "{{ item.value.container_name }}"
+    image: "{{ item.value.image }}"
+    environment: "{{ item.value.environment|default(omit) }}"
+    pid_mode: "{{ item.value.pid_mode|default('') }}"
+    ipc_mode: "{{ item.value.ipc_mode|default(omit) }}"
+    privileged: "{{ item.value.privileged|default(False) }}"
+    volumes: "{{ item.value.volumes|reject('equalto', '')|list }}"
+    dimensions: "{{ item.value.dimensions }}"
+  when:
+    - kolla_action != "config"
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ placement_services }}"
+  notify:
+    - "Restart {{ item.key }} container"
diff --git a/ansible/roles/placement/tasks/deploy.yml b/ansible/roles/placement/tasks/deploy.yml
new file mode 100644
index 0000000000..efb54983f5
--- /dev/null
+++ b/ansible/roles/placement/tasks/deploy.yml
@@ -0,0 +1,14 @@
+---
+- include_tasks: register.yml
+  when: inventory_hostname in groups['placement-api']
+
+- include_tasks: clone.yml
+  when: placement_dev_mode | bool
+
+- include_tasks: config.yml
+
+- include_tasks: bootstrap.yml
+  when: inventory_hostname in groups['placement-api']
+
+- name: Flush handlers
+  meta: flush_handlers
diff --git a/ansible/roles/placement/tasks/loadbalancer.yml b/ansible/roles/placement/tasks/loadbalancer.yml
new file mode 100644
index 0000000000..9b6b8970a5
--- /dev/null
+++ b/ansible/roles/placement/tasks/loadbalancer.yml
@@ -0,0 +1,7 @@
+---
+- name: "Configure haproxy for {{ project_name }}"
+  import_role:
+    role: haproxy-config
+  vars:
+    project_services: "{{ placement_services }}"
+  tags: always
diff --git a/ansible/roles/placement/tasks/main.yml b/ansible/roles/placement/tasks/main.yml
new file mode 100644
index 0000000000..bc5d1e6257
--- /dev/null
+++ b/ansible/roles/placement/tasks/main.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: "{{ kolla_action }}.yml"
diff --git a/ansible/roles/placement/tasks/precheck.yml b/ansible/roles/placement/tasks/precheck.yml
new file mode 100644
index 0000000000..dbb012036f
--- /dev/null
+++ b/ansible/roles/placement/tasks/precheck.yml
@@ -0,0 +1,20 @@
+---
+- name: Get container facts
+  kolla_container_facts:
+    name:
+      - placement_api
+  register: container_facts
+
+- name: Checking free port for Placement API
+  vars:
+    placement_api: "{{ placement_services['placement-api'] }}"
+  wait_for:
+    host: "{{ api_interface_address }}"
+    port: "{{ placement_api_port }}"
+    connect_timeout: 1
+    timeout: 1
+    state: stopped
+  when:
+    - container_facts['placement_api'] is not defined
+    - inventory_hostname in groups[placement_api.group]
+    - placement_api.enabled | bool
diff --git a/ansible/roles/placement/tasks/pull.yml b/ansible/roles/placement/tasks/pull.yml
new file mode 100644
index 0000000000..f82b749393
--- /dev/null
+++ b/ansible/roles/placement/tasks/pull.yml
@@ -0,0 +1,11 @@
+---
+- name: Pulling placement images
+  become: true
+  kolla_docker:
+    action: "pull_image"
+    common_options: "{{ docker_common_options }}"
+    image: "{{ item.value.image }}"
+  when:
+    - inventory_hostname in groups[item.value.group]
+    - item.value.enabled | bool
+  with_dict: "{{ placement_services }}"
diff --git a/ansible/roles/placement/tasks/reconfigure.yml b/ansible/roles/placement/tasks/reconfigure.yml
new file mode 100644
index 0000000000..f670a5b78d
--- /dev/null
+++ b/ansible/roles/placement/tasks/reconfigure.yml
@@ -0,0 +1,2 @@
+---
+- include_tasks: deploy.yml
diff --git a/ansible/roles/placement/tasks/register.yml b/ansible/roles/placement/tasks/register.yml
new file mode 100644
index 0000000000..e611cea6da
--- /dev/null
+++ b/ansible/roles/placement/tasks/register.yml
@@ -0,0 +1,32 @@
+---
+- name: Creating the placement service and endpoint
+  kolla_toolbox:
+    module_name: "kolla_keystone_service"
+    module_args:
+      service_name: "{{ item.name }}"
+      service_type: "{{ item.service_type }}"
+      description: "{{ item.description }}"
+      endpoint_region: "{{ openstack_region_name }}"
+      url: "{{ item.url }}"
+      interface: "{{ item.interface }}"
+      region_name: "{{ openstack_region_name }}"
+      auth: "{{ openstack_placement_auth }}"
+      endpoint_type: "{{ openstack_interface }}"
+  run_once: True
+  with_items:
+    - {'name': 'placement', 'service_type': 'placement', 'interface': 'admin', 'url': '{{ placement_admin_endpoint }}', 'description': 'Placement Service'}
+    - {'name': 'placement', 'service_type': 'placement', 'interface': 'internal', 'url': '{{ placement_internal_endpoint }}', 'description': 'Placement Service'}
+    - {'name': 'placement', 'service_type': 'placement', 'interface': 'public', 'url': '{{ placement_public_endpoint }}', 'description': 'Placement Service'}
+
+- name: Creating the placement project, user, and role
+  kolla_toolbox:
+    module_name: "kolla_keystone_user"
+    module_args:
+      project: "service"
+      user: "{{ placement_keystone_user }}"
+      password: "{{ placement_keystone_password }}"
+      role: "admin"
+      region_name: "{{ openstack_region_name }}"
+      auth: "{{ openstack_placement_auth }}"
+      endpoint_type: "{{ openstack_interface }}"
+  run_once: True
diff --git a/ansible/roles/placement/tasks/stop.yml b/ansible/roles/placement/tasks/stop.yml
new file mode 100644
index 0000000000..eb98f416f9
--- /dev/null
+++ b/ansible/roles/placement/tasks/stop.yml
@@ -0,0 +1,6 @@
+---
+- import_role:
+    role: service-stop
+  vars:
+    project_services: "{{ placement_services }}"
+    service_name: "{{ project_name }}"
diff --git a/ansible/roles/placement/tasks/upgrade.yml b/ansible/roles/placement/tasks/upgrade.yml
new file mode 100644
index 0000000000..67e66dc7cd
--- /dev/null
+++ b/ansible/roles/placement/tasks/upgrade.yml
@@ -0,0 +1,42 @@
+---
+- include_tasks: register.yml
+  when: inventory_hostname in groups['placement-api']
+
+- include_tasks: clone.yml
+  when: placement_dev_mode | bool
+
+# TODO(mgoddard): Remove this in Train once all old containers have been
+# stopped.
+- name: "Stopping old placement-api containers"
+  kolla_docker:
+    action: "stop_container"
+    common_options: "{{ docker_common_options }}"
+    name: "placement_api"
+
+- include_tasks: config.yml
+
+- include_tasks: bootstrap.yml
+  when: inventory_hostname in groups['placement-api']
+
+- name: Flush handlers
+  meta: flush_handlers
+
+- name: Perform Placement online data migration
+  vars:
+    placement_api: "{{ placement_services['placement-api'] }}"
+  become: true
+  kolla_docker:
+    action: "start_container"
+    common_options: "{{ docker_common_options }}"
+    detach: False
+    environment:
+      KOLLA_OSM:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+    image: "{{ placement_api.image }}"
+    labels:
+      BOOTSTRAP:
+    name: "bootstrap_placement"
+    restart_policy: "never"
+    volumes: "{{ placement_api.volumes }}"
+  run_once: True
+  delegate_to: "{{ groups[placement_api.group][0] }}"
diff --git a/ansible/roles/placement/templates/migrate-db.rc.j2 b/ansible/roles/placement/templates/migrate-db.rc.j2
new file mode 100644
index 0000000000..4d938836e6
--- /dev/null
+++ b/ansible/roles/placement/templates/migrate-db.rc.j2
@@ -0,0 +1,8 @@
+NOVA_API_DB={{ nova_api_database_name }}
+NOVA_API_USER={{ nova_api_database_user }}
+NOVA_API_PASS={{ nova_api_database_password }}
+NOVA_API_DB_HOST={{ nova_api_database_host }}
+PLACEMENT_DB={{ placement_database_name }}
+PLACEMENT_USER={{ placement_database_user }}
+PLACEMENT_PASS={{ placement_database_password }}
+PLACEMENT_DB_HOST={{ placement_database_host }}
diff --git a/ansible/roles/nova/templates/placement-api-wsgi.conf.j2 b/ansible/roles/placement/templates/placement-api-wsgi.conf.j2
similarity index 61%
rename from ansible/roles/nova/templates/placement-api-wsgi.conf.j2
rename to ansible/roles/placement/templates/placement-api-wsgi.conf.j2
index 556aa00a1f..60fd5a3706 100644
--- a/ansible/roles/nova/templates/placement-api-wsgi.conf.j2
+++ b/ansible/roles/placement/templates/placement-api-wsgi.conf.j2
@@ -1,10 +1,11 @@
-{% set log_dir = '/var/log/kolla/nova' %}
-{% if nova_install_type == 'binary' %}
+{% set log_dir = '/var/log/kolla/placement' %}
+{% if placement_install_type == 'binary' %}
     {% set python_path = '/usr/lib/python3/dist-packages' if kolla_base_distro == 'ubuntu' else '/usr/lib/python2.7/site-packages' %}
 {% else %}
     {% set python_path = '/var/lib/kolla/venv/lib/python2.7/site-packages' %}
 {% endif %}
-{% set wsgi_directory = '/usr/bin' if nova_install_type == 'binary' else '/var/lib/kolla/venv/bin' %}
+{% set wsgi_directory = '/usr/bin' if placement_install_type == 'binary' else '/var/lib/kolla/venv/bin' %}
+
 Listen {{ api_interface_address }}:{{ placement_api_listen_port }}
 
 ServerSignature Off
@@ -12,12 +13,12 @@ ServerTokens Prod
 TraceEnable off
 
 <VirtualHost *:{{ placement_api_listen_port }}>
-    WSGIDaemonProcess placement-api processes={{ openstack_service_workers }} threads=1 user=nova group=nova display-name=%{GROUP} python-path={{ python_path }}
+    WSGIDaemonProcess placement-api processes={{ openstack_service_workers }} threads=1 user=placement group=placement display-name=%{GROUP} python-path={{ python_path }}
     WSGIProcessGroup placement-api
-{% if kolla_install_type == 'binary' and kolla_base_distro == 'ubuntu' %}
-    WSGIScriptAlias / {{ wsgi_directory }}/python3-nova-placement-api
+{% if placement_install_type == 'binary' and kolla_base_distro == 'ubuntu' %}
+    WSGIScriptAlias / {{ wsgi_directory }}/python3-placement-api
 {% else %}
-    WSGIScriptAlias / {{ wsgi_directory }}/nova-placement-api
+    WSGIScriptAlias / {{ wsgi_directory }}/placement-api
 {% endif %}
     WSGIApplicationGroup %{GLOBAL}
     WSGIPassAuthorization On
@@ -28,10 +29,10 @@ TraceEnable off
     LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b %D \"%{Referer}i\" \"%{User-Agent}i\"" logformat
     CustomLog "{{ log_dir }}/placement-api-access.log" logformat
     <Directory {{ wsgi_directory }}>
-{% if kolla_install_type == 'binary' and kolla_base_distro == 'ubuntu' %}
-        <Files python3-nova-placement-api>
+{% if placement_install_type == 'binary' and kolla_base_distro == 'ubuntu' %}
+        <Files python3-placement-api>
 {% else %}
-        <Files nova-placement-api>
+        <Files placement-api>
 {% endif %}
             Require all granted
         </Files>
diff --git a/ansible/roles/placement/templates/placement-api.json.j2 b/ansible/roles/placement/templates/placement-api.json.j2
new file mode 100644
index 0000000000..14638509c5
--- /dev/null
+++ b/ansible/roles/placement/templates/placement-api.json.j2
@@ -0,0 +1,42 @@
+{% set apache_binary = 'apache2' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd' %}
+{% set apache_conf_dir = 'apache2/conf-enabled' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd/conf.d' %}
+{
+    "command": "/usr/sbin/{{ apache_binary }} -DFOREGROUND",
+    "config_files": [
+        {
+            "source": "{{ container_config_directory }}/placement.conf",
+            "dest": "/etc/placement/placement.conf",
+            "owner": "placement",
+            "perm": "0600"
+        }{% if placement_policy_file is defined %},
+        {
+            "source": "{{ container_config_directory }}/{{ placement_policy_file }}",
+            "dest": "/etc/placement/{{ placement_policy_file }}",
+            "owner": "placement",
+            "perm": "0600"
+        }{% endif %},
+        {
+            "source": "{{ container_config_directory }}/placement-api-wsgi.conf",
+            "dest": "/etc/{{ apache_conf_dir }}/placement-api-wsgi.conf",
+            "owner": "placement",
+            "perm": "0600"
+        },
+        {
+            "source": "{{ container_config_directory }}/migrate-db.rc",
+            "dest": "/etc/placement/migrate-db.rc",
+            "owner": "placement",
+            "perm": "0600"
+        }
+    ],
+    "permissions": [
+        {
+            "path": "/var/log/kolla/placement",
+            "owner": "placement:kolla",
+            "recurse": true
+        },
+        {
+            "path": "/var/log/kolla/placement/placement.log",
+            "owner": "placement:placement"
+        }
+    ]
+}
diff --git a/ansible/roles/placement/templates/placement.conf.j2 b/ansible/roles/placement/templates/placement.conf.j2
new file mode 100644
index 0000000000..82f2575193
--- /dev/null
+++ b/ansible/roles/placement/templates/placement.conf.j2
@@ -0,0 +1,61 @@
+[DEFAULT]
+debug = {{ placement_logging_debug }}
+
+log_dir = /var/log/kolla/placement
+
+state_path = /var/lib/placement
+
+osapi_compute_listen = {{ api_interface_address }}
+
+# Though my_ip is not used directly, lots of other variables use $my_ip
+my_ip = {{ api_interface_address }}
+
+transport_url = {{ rpc_transport_url }}
+
+[api]
+use_forwarded_for = true
+
+[oslo_middleware]
+enable_proxy_headers_parsing = True
+
+[oslo_concurrency]
+lock_path = /var/lib/placement/tmp
+
+[placement_database]
+connection = mysql+pymysql://{{ placement_database_user }}:{{ placement_database_password }}@{{ placement_database_address }}/{{ placement_database_name }}
+max_pool_size = 50
+max_overflow = 1000
+max_retries = -1
+
+[cache]
+backend = oslo_cache.memcache_pool
+enabled = True
+memcache_servers = {% for host in groups['memcached'] %}{{ hostvars[host]['ansible_' + hostvars[host]['api_interface']]['ipv4']['address'] }}:{{ memcached_port }}{% if not loop.last %},{% endif %}{% endfor %}
+
+
+[keystone_authtoken]
+www_authenticate_uri = {{ keystone_internal_url }}
+auth_url = {{ keystone_admin_url }}
+auth_type = password
+project_domain_id = {{ default_project_domain_id }}
+user_domain_id = {{ default_user_domain_id }}
+project_name = service
+username = {{ placement_keystone_user }}
+password = {{ placement_keystone_password }}
+
+memcache_security_strategy = ENCRYPT
+memcache_secret_key = {{ memcache_secret_key }}
+memcached_servers = {% for host in groups['memcached'] %}{{ hostvars[host]['ansible_' + hostvars[host]['api_interface']]['ipv4']['address'] }}:{{ memcached_port }}{% if not loop.last %},{% endif %}{% endfor %}
+
+{% if placement_policy_file is defined %}
+[oslo_policy]
+policy_file = {{ placement_policy_file }}
+{% endif %}
+
+{% if enable_osprofiler | bool %}
+[profiler]
+enabled = true
+trace_sqlalchemy = true
+hmac_keys = {{ osprofiler_secret }}
+connection_string = {{ osprofiler_backend_connection_string }}
+{% endif %}
diff --git a/ansible/site.yml b/ansible/site.yml
index 5814e1b646..2344d4a0a5 100644
--- a/ansible/site.yml
+++ b/ansible/site.yml
@@ -65,6 +65,7 @@
         - enable_openvswitch_{{ enable_openvswitch | bool }}_enable_ovs_dpdk_{{ enable_ovs_dpdk | bool }}
         - enable_outward_rabbitmq_{{ enable_outward_rabbitmq | bool }}
         - enable_panko_{{ enable_panko | bool }}
+        - enable_placement_{{ enable_placement | bool }}
         - enable_prometheus_{{ enable_prometheus | bool }}
         - enable_qdrouterd_{{ enable_qdrouterd | bool }}
         - enable_rabbitmq_{{ enable_rabbitmq | bool }}
@@ -266,6 +267,10 @@
             tasks_from: loadbalancer
           tags: neutron
           when: enable_neutron | bool
+        - include_role:
+            role: placement
+            tasks_from: loadbalancer
+          tags: placement
         - include_role:
             role: nova
             tasks_from: loadbalancer
@@ -674,6 +679,17 @@
         tags: cinder,
         when: enable_cinder | bool }
 
+- name: Apply role placement
+  gather_facts: false
+  hosts:
+    - placement-api
+    - '&enable_placement_True'
+  serial: '{{ kolla_serial|default("0") }}'
+  roles:
+    - { role: placement,
+        tags: placement,
+        when: enable_placement | bool }
+
 - name: Apply role nova
   gather_facts: false
   hosts:
diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml
index f900232add..d9891172ed 100644
--- a/etc/kolla/globals.yml
+++ b/etc/kolla/globals.yml
@@ -281,6 +281,7 @@ kolla_internal_vip_address: "10.10.10.254"
 #enable_ovs_dpdk: "no"
 #enable_osprofiler: "no"
 #enable_panko: "no"
+#enable_placement: "{{ enable_nova }}"
 #enable_prometheus: "no"
 #enable_qdrouterd: "no"
 #enable_rally: "no"
diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml
index 8d60d26180..beb9c86e1f 100644
--- a/etc/kolla/passwords.yml
+++ b/etc/kolla/passwords.yml
@@ -90,6 +90,7 @@ nova_api_database_password:
 nova_keystone_password:
 
 placement_keystone_password:
+placement_database_password:
 
 neutron_database_password:
 neutron_keystone_password:
diff --git a/tools/setup_gate.sh b/tools/setup_gate.sh
index e022906df0..debea5e3c3 100755
--- a/tools/setup_gate.sh
+++ b/tools/setup_gate.sh
@@ -35,7 +35,7 @@ EOF
     rm ${PIP_CONF}
 
     if [[ $ACTION != "bifrost" ]]; then
-        GATE_IMAGES="cron,fluentd,glance,haproxy,keepalived,keystone,kolla-toolbox,mariadb,memcached,neutron,nova,openvswitch,rabbitmq,horizon,chrony,heat"
+        GATE_IMAGES="cron,fluentd,glance,haproxy,keepalived,keystone,kolla-toolbox,mariadb,memcached,neutron,nova,openvswitch,rabbitmq,horizon,chrony,heat,placement"
     else
         GATE_IMAGES="bifrost"
     fi