diff --git a/plugins/modules/baremetal_node_state.py b/plugins/modules/baremetal_node_state.py
new file mode 100644
index 0000000..a8936de
--- /dev/null
+++ b/plugins/modules/baremetal_node_state.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# (c) 2015, Hewlett-Packard Development Company, L.P.
+# Copyright 2017 StackHPC Ltd.
+#
+# This module is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this software. If not, see .
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+module: baremetal_node_state
+short_description: Set provision state of Bare Metal Resources from OpenStack
+author: "Mark Goddard "
+extends_documentation_fragment: openstack
+description:
+ - Set the provision state of OpenStack ironic bare metal nodes.
+options:
+ provision_state:
+ description:
+ - Indicates desired provision state of the resource
+ choices: ['manage', 'provide']
+ default: present
+ uuid:
+ description:
+ - globally unique identifier (UUID) to be given to the resource.
+ required: false
+ default: None
+ ironic_url:
+ description:
+ - If noauth mode is utilized, this is required to be set to the
+ endpoint URL for the Ironic API. Use with "auth" and "auth_type"
+ settings set to None.
+ required: false
+ default: None
+ wait:
+ description:
+ - A boolean value instructing the module to wait for node
+ activation or deactivation to complete before returning.
+ required: false
+ default: False
+ version_added: "2.1"
+ timeout:
+ description:
+ - An integer value representing the number of seconds to
+ wait for the node activation or deactivation to complete.
+ version_added: "2.1"
+ availability_zone:
+ description:
+ - Ignored. Present for backwards compatibility
+ required: false
+'''
+
+EXAMPLES = r'''
+baremeta_node_state:
+ cloud: "openstack"
+ uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69"
+ provision_state: provide
+ delegate_to: localhost
+'''
+
+from ansible_collections.openstack.cloud.plugins.module_utils.openstack import (
+ OpenStackModule
+)
+
+
+def _choose_id_value(module):
+ if module.params['uuid']:
+ return module.params['uuid']
+ if module.params['name']:
+ return module.params['name']
+ return None
+
+
+def _change_required(current_provision_state, action):
+ """Return whether a change to the provision state is required.
+
+ :param current_provision_state: The current provision state of the node.
+ :param action: The requested action.
+ """
+ if action == 'manage':
+ if current_provision_state == 'manageable':
+ return False
+ if action == 'provide':
+ if current_provision_state == 'available':
+ return False
+ return True
+
+
+class BaremetalDeployTemplateModule(OpenStackModule):
+ argument_spec = dict(
+ uuid=dict(required=False),
+ name=dict(required=False),
+ ironic_url=dict(required=False),
+ provision_state=dict(required=True,
+ choices=['manage', 'provide']),
+ wait=dict(type='bool', required=False, default=False),
+ timeout=dict(required=False, type='int', default=1800),
+ )
+ module_kwargs = dict(
+ required_one_of=[
+ ('uuid', 'name'),
+ ],
+ )
+
+ def run(self):
+ if (self.params['auth_type'] in [None, 'None'] and
+ self.params['ironic_url'] is None):
+ self.fail_json(msg="Authentication appears disabled, Please "
+ "define an ironic_url parameter")
+
+ if (self.params['ironic_url'] and
+ self.params['auth_type'] in [None, 'None']):
+ self.params['auth'] = dict(
+ endpoint=self.params['ironic_url']
+ )
+
+ node_id = _choose_id_value(self)
+
+ if not node_id:
+ self.fail_json(msg="A uuid or name value must be defined "
+ "to use this module.")
+
+ try:
+ sdk, cloud = openstack_cloud_from_module(self)
+ node = cloud.get_machine(node_id)
+
+ if node is None:
+ self.fail_json(msg="node not found")
+
+ uuid = node['uuid']
+ changed = False
+ wait = self.params['wait']
+ timeout = self.params['timeout']
+ provision_state = self.params['provision_state']
+
+ if node['provision_state'] in [
+ 'cleaning',
+ 'deleting',
+ 'wait call-back']:
+ self.fail_json(msg="Node is in %s state, cannot act upon the "
+ "request as the node is in a transition "
+ "state" % node['provision_state'])
+
+ if _change_required(node['provision_state'], provision_state):
+ cloud.node_set_provision_state(uuid, provision_state, wait=wait,
+ timeout=timeout)
+ changed = True
+
+ self.exit_json(changed=changed)
+
+ except Exception as e:
+ self.fail_json(msg=str(e))
+
+
+def main():
+ module = BaremetalNodeStateModule()
+ module()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/roles/os_ironic_state/README.md b/roles/os_ironic_state/README.md
new file mode 100644
index 0000000..9990599
--- /dev/null
+++ b/roles/os_ironic_state/README.md
@@ -0,0 +1,50 @@
+OpenStack Ironic Node State
+===========================
+
+This role can be used to set the provision state of ironic nodes.
+
+Requirements
+------------
+
+The OpenStack keystone and ironic APIs should be accessible from the target
+host.
+
+Role Variables
+--------------
+
+`os_ironic_state_auth_type`: Authentication type as used by `os_*` modules'
+`auth_type` argument.
+
+`os_ironic_state_auth`: Authentication options as used by `os_*` modules'
+`auth` argument.
+
+`os_ironic_state_cacert`: CA certificate as used by `os_*` modules' `cacert`
+argument.
+
+`os_ironic_state_interface` is the endpoint URL type to fetch from the service
+catalog. Maybe be one of `public`, `admin`, or `internal`.
+
+`os_ironic_state_name`: Name of the ironic node.
+
+`os_ironic_state_provision_state`: Desired provision state.
+
+`os_ironic_state_delegate_to`: Host to delegate to.
+
+`os_ironic_state_wait`: Whether to wait for the state transition to complete.
+Default is `True`.
+
+`os_ironic_state_timeout`: Time to wait for state transition to complete, if
+`os_ironic_state_wait` is `True`. Default is 1200 seconds.
+
+Dependencies
+------------
+
+The delegate host should have the `openstacksdk` Python package installed.
+
+Example Playbook
+----------------
+
+Author Information
+------------------
+
+- Mark Goddard ()
diff --git a/roles/os_ironic_state/defaults/main.yml b/roles/os_ironic_state/defaults/main.yml
new file mode 100644
index 0000000..69965b9
--- /dev/null
+++ b/roles/os_ironic_state/defaults/main.yml
@@ -0,0 +1,29 @@
+---
+# Authentication type as used by os_* modules' 'auth_type' argument.
+os_ironic_state_auth_type:
+
+# Authentication options as used by os_* modules' 'auth' argument.
+os_ironic_state_auth: {}
+
+# CA certificate as used by os_* modules' 'cacert' argument.
+os_ironic_state_cacert:
+
+# Endpoint URL type to fetch from the service catalog. Maybe be one of:
+# public, admin, or internal.
+os_ironic_state_interface:
+
+# Name of the ironic node.
+os_ironic_state_name:
+
+# Desired provision state.
+os_ironic_state_provision_state:
+
+# Host to delegate to.
+os_ironic_state_delegate_to:
+
+# Whether to wait for the state transition to complete.
+os_ironic_state_wait: true
+
+# Time to wait for state transition to complete, if os_ironic_state_wait is
+# True.
+os_ironic_state_timeout: 1200
diff --git a/roles/os_ironic_state/tasks/main.yml b/roles/os_ironic_state/tasks/main.yml
new file mode 100644
index 0000000..4c6da00
--- /dev/null
+++ b/roles/os_ironic_state/tasks/main.yml
@@ -0,0 +1,16 @@
+---
+- name: Ensure baremetal compute nodes provision states are set
+ stackhpc.openstack.baremetal_node_state:
+ auth_type: "{{ os_ironic_state_auth_type }}"
+ auth: "{{ os_ironic_state_auth }}"
+ cacert: "{{ os_ironic_state_cacert | default(omit, true) }}"
+ interface: "{{ os_ironic_state_interface | default(omit, true) }}"
+ name: "{{ os_ironic_state_name }}"
+ provision_state: "{{ os_ironic_state_provision_state }}"
+ timeout: "{{ os_ironic_state_timeout }}"
+ wait: "{{ os_ironic_state_wait }}"
+ delegate_to: "{{ os_ironic_state_delegate_to }}"
+ vars:
+ # NOTE: Without this, the delegate hosts's ansible_host variable will not
+ # be respected.
+ ansible_host: "{{ hostvars[os_ironic_state_delegate_to].ansible_host | default(os_ironic_state_delegate_to) }}"
diff --git a/roles/os_ironic_state/tests/inventory b/roles/os_ironic_state/tests/inventory
new file mode 100644
index 0000000..5f8076c
--- /dev/null
+++ b/roles/os_ironic_state/tests/inventory
@@ -0,0 +1 @@
+localhost ansible_connection='local' ansible_python_interpreter='/usr/bin/env python'
diff --git a/roles/os_ironic_state/tests/test.yml b/roles/os_ironic_state/tests/test.yml
new file mode 100644
index 0000000..a652d48
--- /dev/null
+++ b/roles/os_ironic_state/tests/test.yml
@@ -0,0 +1,6 @@
+---
+- name: Test os_ironic_state
+ hosts: all
+ connection: local
+ roles:
+ - stackhpc.openstack.os_ironic_state