diff --git a/playbooks/library/os_ironic.py b/playbooks/library/os_ironic.py deleted file mode 100644 index 7b2137163..000000000 --- a/playbooks/library/os_ironic.py +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/python -# coding: utf-8 -*- - -# (c) 2014, Hewlett-Packard Development Company, L.P. -# -# 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 . - -try: - import shade - HAS_SHADE = True -except ImportError: - HAS_SHADE = False - -import jsonpatch -DOCUMENTATION = ''' ---- -module: os_ironic -short_description: Create/Delete Bare Metal Resources from OpenStack -extends_documentation_fragment: openstack -description: - - Create or Remove Ironic nodes from OpenStack. -options: - state: - description: - - Indicates desired state of the resource - choices: ['present', 'absent'] - default: present - uuid: - description: - - globally unique identifier (UUID) to be given to the resource. Will - be auto-generated if not specified, and name is specified. - - Definition of a UUID will always take precedence to a name value. - required: false - default: None - name: - description: - - unique name identifier to be given to the resource. - required: false - default: None - driver: - description: - - The name of the Ironic Driver to use with this node. - required: true - default: None - chassis_uuid: - description: - - Associate the node with a pre-defined chassis. - 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 - driver_info: - description: - - Information for this server's driver. Will vary based on which - driver is in use. Any sub-field which is populated will be validated - during creation. - power: - - Information necessary to turn this server on / off. This often - includes such things as IPMI username, password, and IP address. - required: true - deploy: - - Information necessary to deploy this server directly, without - using Nova. THIS IS NOT RECOMMENDED. - console: - - Information necessary to connect to this server's serial console. - Not all drivers support this. - management: - - Information necessary to interact with this server's management - interface. May be shared by power_info in some cases. - required: true - nics: - description: - - A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc" - required: true - properties: - description: - - Definition of the physical characteristics of this server, used for - scheduling purposes - cpu_arch: - description: - - CPU architecture (x86_64, i686, ...) - default: x86_64 - cpus: - description: - - Number of CPU cores this machine has - default: 1 - ram: - description: - - amount of RAM this machine has, in MB - default: 1 - disk_size: - description: - - size of first storage device in this machine (typically - /dev/sda), in GB - default: 1 - skip_update_of_driver_password: - description: - - Allows the code that would assert changes to nodes to skip the - update if the change is a single line consisting of the password - field. As of Kilo, by default, passwords are always masked to API - requests, which means the logic as a result always attempts to - re-assert the password field. - required: false - default: false - -requirements: ["shade", "jsonpatch"] -''' - -EXAMPLES = ''' -# Enroll a node with some basic properties and driver info -- os_ironic: - cloud: "devstack" - driver: "pxe_ipmitool" - uuid: "00000000-0000-0000-0000-000000000002" - properties: - cpus: 2 - cpu_arch: "x86_64" - ram: 8192 - disk_size: 64 - nics: - - mac: "aa:bb:cc:aa:bb:cc" - - mac: "dd:ee:ff:dd:ee:ff" - driver_info: - power: - ipmi_address: "1.2.3.4" - ipmi_username: "admin" - ipmi_password: "adminpass" - chassis_uuid: "00000000-0000-0000-0000-000000000001" - -''' - - -def _parse_properties(module): - p = module.params['properties'] - props = dict( - cpu_arch=p.get('cpu_arch') if p.get('cpu_arch') else 'x86_64', - cpus=p.get('cpus') if p.get('cpus') else 1, - memory_mb=p.get('ram') if p.get('ram') else 1, - local_gb=p.get('disk_size') if p.get('disk_size') else 1, - ) - return props - - -def _parse_driver_info(module): - p = module.params['driver_info'] - info = p.get('power') - if not info: - raise shade.OpenStackCloudException( - "driver_info['power'] is required") - if p.get('console'): - info.update(p.get('console')) - if p.get('management'): - info.update(p.get('management')) - if p.get('deploy'): - info.update(p.get('deploy')) - return info - - -def _choose_id_value(module): - if module.params['uuid']: - return module.params['uuid'] - if module.params['name']: - return module.params['name'] - return None - - -def _is_value_true(value): - true_values = [True, 'yes', 'Yes', 'True', 'true'] - if value in true_values: - return True - return False - - -def _choose_if_password_only(module, patch): - if len(patch) is 1: - if 'password' in patch[0]['path'] and _is_value_true( - module.params['skip_update_of_masked_password']): - # Return false to aabort update as the password appears - # to be the only element in the patch. - return False - return True - - -def _exit_node_not_updated(module, server): - module.exit_json( - changed=False, - result="Node not updated", - uuid=server['uuid'], - provision_state=server['provision_state'] - ) - - -def main(): - argument_spec = openstack_full_argument_spec( - auth_type=dict(required=False), - uuid=dict(required=False), - name=dict(required=False), - driver=dict(required=False), - driver_info=dict(type='dict', required=True), - nics=dict(type='list', required=True), - properties=dict(type='dict', default={}), - ironic_url=dict(required=False), - chassis_uuid=dict(required=False), - skip_update_of_masked_password=dict(required=False, choices=BOOLEANS), - state=dict(required=False, default='present') - ) - module_kwargs = openstack_module_kwargs() - module = AnsibleModule(argument_spec, **module_kwargs) - - if not HAS_SHADE: - module.fail_json(msg='shade is required for this module') - if (module.params['auth_type'] in [None, 'None'] and - module.params['ironic_url'] is None): - module.fail_json(msg="Authentication appears to be disabled, " - "Please define an ironic_url parameter") - - if (module.params['ironic_url'] and - module.params['auth_type'] in [None, 'None']): - module.params['auth'] = dict( - endpoint=module.params['ironic_url'] - ) - - node_id = _choose_id_value(module) - - try: - cloud = shade.operator_cloud(**module.params) - server = cloud.get_machine(node_id) - if module.params['state'] == 'present': - if module.params['driver'] is None: - module.fail_json(msg="A driver must be defined in order " - "to set a node to present.") - - properties = _parse_properties(module) - driver_info = _parse_driver_info(module) - kwargs = dict( - driver=module.params['driver'], - properties=properties, - driver_info=driver_info, - name=module.params['name'], - ) - - if module.params['chassis_uuid']: - kwargs['chassis_uuid'] = module.params['chassis_uuid'] - - if server is None: - # Note(TheJulia): Add a specific UUID to the request if - # present in order to be able to re-use kwargs for if - # the node already exists logic, since uuid cannot be - # updated. - if module.params['uuid']: - kwargs['uuid'] = module.params['uuid'] - - server = cloud.register_machine(module.params['nics'], - **kwargs) - module.exit_json(changed=True, uuid=server['uuid'], - provision_state=server['provision_state']) - else: - # TODO(TheJulia): Presently this does not support updating - # nics. Support needs to be added. - # - # Note(TheJulia): This message should never get logged - # however we cannot realistically proceed if neither a - # name or uuid was supplied to begin with. - if not node_id: - module.fail_json(msg="A uuid or name value " - "must be defined") - - # Note(TheJulia): Constructing the configuration to compare - # against. The items listed in the server_config block can - # be updated via the API. - - server_config = dict( - driver=server['driver'], - properties=server['properties'], - driver_info=server['driver_info'], - name=server['name'], - ) - - # Add the pre-existing chassis_uuid only if - # it is present in the server configuration. - if hasattr(server, 'chassis_uuid'): - server_config['chassis_uuid'] = server['chassis_uuid'] - - # Note(TheJulia): If a password is defined and concealed, a - # patch will always be generated and re-asserted. - patch = jsonpatch.JsonPatch.from_diff(server_config, kwargs) - - if not patch: - _exit_node_not_updated(module, server) - elif _choose_if_password_only(module, list(patch)): - # Note(TheJulia): Normally we would allow the general - # exception catch below, however this allows a specific - # message. - try: - server = cloud.patch_machine( - server['uuid'], - list(patch)) - except Exception as e: - module.fail_json(msg="Failed to update node, " - "Error: %s" % e.message) - - # Enumerate out a list of changed paths. - change_list = [] - for change in list(patch): - change_list.append(change['path']) - module.exit_json(changed=True, - result="Node Updated", - changes=change_list, - uuid=server['uuid'], - provision_state=server['provision_state']) - - # Return not updated by default as the conditions were not met - # to update. - _exit_node_not_updated(module, server) - - if module.params['state'] == 'absent': - if not node_id: - module.fail_json(msg="A uuid or name value must be defined " - "in order to remove a node.") - - if server is not None: - cloud.unregister_machine(module.params['nics'], - server['uuid']) - module.exit_json(changed=True, result="deleted") - else: - module.exit_json(changed=False, result="Server not found") - - except shade.OpenStackCloudException as e: - module.fail_json(msg=e.message) - - -# this is magic, see lib/ansible/module_common.py -from ansible.module_utils.basic import * -from ansible.module_utils.openstack import * -main() diff --git a/playbooks/library/os_ironic_node.py b/playbooks/library/os_ironic_node.py deleted file mode 100644 index a7186ad4a..000000000 --- a/playbooks/library/os_ironic_node.py +++ /dev/null @@ -1,333 +0,0 @@ -#!/usr/bin/python -# coding: utf-8 -*- - -# (c) 2015, Hewlett-Packard Development Company, L.P. -# -# 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 . - -try: - import shade - HAS_SHADE = True -except ImportError: - HAS_SHADE = False - -DOCUMENTATION = ''' ---- -module: os_ironic_node -short_description: Activate/Deactivate Bare Metal Resources from OpenStack -extends_documentation_fragment: openstack -description: - - Deploy to nodes controlled by Ironic. -options: - state: - description: - - Indicates desired state of the resource - choices: ['present', 'absent'] - default: present - deploy: - description: - - Indicates if the resource should be deployed. Allows for deployment - logic to be disengaged and control of the node power or maintenance - state to be changed. - choices: ['true', 'false'] - default: true - 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 - config_drive: - description: - - A configdrive file or HTTP(S) URL that will be passed along to the - node. - required: false - default: None - instance_info: - description: - - Definition of the instance information which is used to deploy - the node. This information is only required when an instance is - set to present. - image_source: - description: - - An HTTP(S) URL where the image can be retrieved from. - image_checksum: - description: - - The checksum of image_source. - image_disk_format: - description: - - The type of image that has been requested to be deployed. - power: - description: - - A setting to allow power state to be asserted allowing nodes - that are not yet deployed to be powered on, and nodes that - are deployed to be powered off. - choices: ['present', 'absent'] - default: present - maintenance: - description: - - A setting to allow the direct control if a node is in - maintenance mode. - required: false - default: false - maintenance_reason: - description: - - A string expression regarding the reason a node is in a - maintenance mode. - required: false - default: None - -requirements: ["shade"] -''' - -EXAMPLES = ''' -# Activate a node by booting an image with a configdrive attached -os_ironic_node: - cloud: "openstack" - uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" - state: present - power: present - deploy: True - maintenance: False - config_drive: "http://192.168.1.1/host-configdrive.iso" - instance_info: - image_source: "http://192.168.1.1/deploy_image.img" - image_checksum: "356a6b55ecc511a20c33c946c4e678af" - image_disk_format: "qcow" - delegate_to: localhost -''' - - -def _choose_id_value(module): - if module.params['uuid']: - return module.params['uuid'] - if module.params['name']: - return module.params['name'] - return None - - -# TODO(TheJulia): Change this over to use the machine patch method -# in shade once it is available. -def _prepare_instance_info_patch(instance_info): - patch = [] - patch.append({ - 'op': 'replace', - 'path': '/instance_info', - 'value': instance_info - }) - return patch - - -def _is_true(value): - true_values = [True, 'yes', 'Yes', 'True', 'true', 'present', 'on'] - if value in true_values: - return True - return False - - -def _is_false(value): - false_values = [False, None, 'no', 'No', 'False', 'false', 'absent', 'off'] - if value in false_values: - return True - return False - - -def _check_set_maintenance(module, cloud, node): - if _is_true(module.params['maintenance']): - if _is_false(node['maintenance']): - cloud.set_machine_maintenance_state( - node['uuid'], - True, - reason=module.params['maintenance_reason']) - module.exit_json(changed=True, msg="Node has been set into " - "maintenance mode") - else: - # User has requested maintenance state, node is already in the - # desired state, checking to see if the reason has changed. - if (str(node['maintenance_reason']) not in - str(module.params['maintenance_reason'])): - cloud.set_machine_maintenance_state( - node['uuid'], - True, - reason=module.params['maintenance_reason']) - module.exit_json(changed=True, msg="Node maintenance reason " - "updated, cannot take any " - "additional action.") - elif _is_false(module.params['maintenance']): - if node['maintenance'] is True: - cloud.remove_machine_from_maintenance(node['uuid']) - return True - else: - module.fail_json(msg="maintenance parameter was set but a valid " - "the value was not recognized.") - return False - - -def _check_set_power_state(module, cloud, node): - if 'power on' in str(node['power_state']): - if _is_false(module.params['power']): - # User has requested the node be powered off. - cloud.set_machine_power_off(node['uuid']) - module.exit_json(changed=True, msg="Power requested off") - if 'power off' in str(node['power_state']): - if (_is_false(module.params['power']) and - _is_false(module.params['state'])): - return False - if (_is_false(module.params['power']) and - _is_false(module.params['state'])): - module.exit_json( - changed=False, - msg="Power for node is %s, node must be reactivated " - "OR set to state absent" - ) - # In the event the power has been toggled on and - # deployment has been requested, we need to skip this - # step. - if (_is_true(module.params['power']) and - _is_false(module.params['deploy'])): - # Node is powered down when it is not awaiting to be provisioned - cloud.set_machine_power_on(node['uuid']) - return True - # Default False if no action has been taken. - return False - - -def main(): - argument_spec = openstack_full_argument_spec( - auth_type=dict(required=False), - uuid=dict(required=False), - name=dict(required=False), - instance_info=dict(type='dict', required=False), - config_drive=dict(required=False), - ironic_url=dict(required=False), - state=dict(required=False, default='present'), - maintenance=dict(required=False), - maintenance_reason=dict(required=False), - power=dict(required=False, default='present'), - deploy=dict(required=False, default=True), - ) - module_kwargs = openstack_module_kwargs() - module = AnsibleModule(argument_spec, **module_kwargs) - if not HAS_SHADE: - module.fail_json(msg='shade is required for this module') - if (module.params['auth_type'] in [None, 'None'] and - module.params['ironic_url'] is None): - module.fail_json(msg="Authentication appears disabled, Please " - "define an ironic_url parameter") - - if (module.params['ironic_url'] and - module.params['auth_type'] in [None, 'None']): - module.params['auth'] = dict( - endpoint=module.params['ironic_url'] - ) - - node_id = _choose_id_value(module) - - if not node_id: - module.fail_json(msg="A uuid or name value must be defined " - "to use this module.") - try: - cloud = shade.operator_cloud(**module.params) - node = cloud.get_machine(node_id) - - if node is None: - module.fail_json(msg="node not found") - - uuid = node['uuid'] - instance_info = module.params['instance_info'] - changed = False - - # User has reqeusted desired state to be in maintenance state. - if module.params['state'] is 'maintenance': - module.params['maintenance'] = True - - if node['provision_state'] in [ - 'cleaning', - 'deleting', - 'wait call-back']: - module.fail_json(msg="Node is in %s state, cannot act upon the " - "request as the node is in a transition " - "state" % node['provision_state']) - # TODO(TheJulia) This is in-development code, that requires - # code in the shade library that is still in development. - if _check_set_maintenance(module, cloud, node): - if node['provision_state'] in 'active': - module.exit_json(changed=True, - result="Maintenance state changed") - changed = True - node = cloud.get_machine(node_id) - - if _check_set_power_state(module, cloud, node): - changed = True - node = cloud.get_machine(node_id) - - if _is_true(module.params['state']): - if _is_false(module.params['deploy']): - module.exit_json( - changed=changed, - result="User request has explicitly disabled " - "deployment logic" - ) - - if 'active' in node['provision_state']: - module.exit_json( - changed=changed, - result="Node already in an active state." - ) - - if instance_info is None: - module.fail_json( - changed=changed, - msg="When setting an instance to present, " - "instance_info is a required variable.") - - # TODO(TheJulia): Update instance info, however info is - # deployment specific. Perhaps consider adding rebuild - # support, although there is a known desire to remove - # rebuild support from Ironic at some point in the future. - patch = _prepare_instance_info_patch(instance_info) - cloud.set_node_instance_info(uuid, patch) - cloud.validate_node(uuid) - cloud.activate_node(uuid, module.params['config_drive']) - # TODO(TheJulia): Add more error checking and a wait option. - # We will need to loop, or just add the logic to shade, - # although this could be a very long running process as - # baremetal deployments are not a "quick" task. - module.exit_json(changed=changed, result="node activated") - - elif _is_false(module.params['state']): - if node['provision_state'] not in "deleted": - cloud.purge_node_instance_info(uuid) - cloud.deactivate_node(uuid) - module.exit_json(changed=True, result="deleted") - else: - module.exit_json(changed=False, result="node not found") - else: - module.fail_json(msg="State must be present, absent, " - "maintenance, off") - - except shade.OpenStackCloudException as e: - module.fail_json(msg=e.message) - - -# this is magic, see lib/ansible/module_common.py -from ansible.module_utils.basic import * -from ansible.module_utils.openstack import * -main() diff --git a/releasenotes/notes/ansible-2.0-support-81fa15cc27c28ba8.yaml b/releasenotes/notes/ansible-2.0-support-81fa15cc27c28ba8.yaml new file mode 100644 index 000000000..00b3915a1 --- /dev/null +++ b/releasenotes/notes/ansible-2.0-support-81fa15cc27c28ba8.yaml @@ -0,0 +1,16 @@ +--- +upgrade: + - Bifrost has moved to focusing its use on Ansible 2.0. + While Ansible 2.0 is relatively new, it has been stable + development for quite some time. If a pre-existing user + intends to reinstall/upgrade their environment, they may find + the need to remove their pre-existing ansible environment + located at ``/opt/stack/ansible``. +deprecations: + - Moving forward, bifrost will be targeting use of + Ansible 2.0. Due to some style/configuration changes, + some roles have been marked in their metadata as being + intended and only functional with Ansible 2.0 due to + required features having been added in 2.0 that were + not present or available to reproduce in a 1.9.x + compatible way. diff --git a/scripts/env-setup.sh b/scripts/env-setup.sh index 496b3faa5..e421d4883 100755 --- a/scripts/env-setup.sh +++ b/scripts/env-setup.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eu ANSIBLE_GIT_URL=${ANSIBLE_GIT_URL:-https://github.com/ansible/ansible.git} # Note(TheJulia): Normally this should be stable-2.0, pinning due to @@ -7,6 +7,14 @@ ANSIBLE_GIT_URL=${ANSIBLE_GIT_URL:-https://github.com/ansible/ansible.git} # https://github.com/ansible/ansible-modules-core/issues/2804 ANSIBLE_GIT_BRANCH=${ANSIBLE_GIT_BRANCH:-v2.0.0.0-1} +function check_get_module () { + local file=${1} + local url=${2} + if [ ! -e ${file} ]; then + wget -O ${file} ${url} + fi +} + if [ -x '/usr/bin/apt-get' ]; then if ! $(gcc -v &>/dev/null); then sudo -H apt-get -y install gcc @@ -68,6 +76,7 @@ cd /opt/stack if [ ! -d ansible ]; then git clone $ANSIBLE_GIT_URL --recursive -b $ANSIBLE_GIT_BRANCH + cd ansible else cd ansible git checkout $ANSIBLE_GIT_BRANCH @@ -75,6 +84,15 @@ else git submodule update --init --recursive git fetch fi +# Note(TheJulia): These files should be in the ansible folder +# and this functionality exists for a level of ansible 1.9.x +# backwards compatability although the modules were developed +# for Ansible 2.0. + +check_get_module `pwd`/lib/ansible/modules/core/cloud/openstack/os_ironic.py \ + https://raw.githubusercontent.com/ansible/ansible-modules-core/stable-2.0/cloud/openstack/os_ironic.py +check_get_module `pwd`/lib/ansible/modules/core/cloud/openstack/os_ironic_node.py \ + https://raw.githubusercontent.com/ansible/ansible-modules-core/stable-2.0/cloud/openstack/os_ironic_node.py echo echo "If you're using this script directly, execute the"