diff options
40 files changed, 529 insertions, 118 deletions
diff --git a/devstack/lib/ironic b/devstack/lib/ironic index a578ad74b..d9ceeabf1 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -69,6 +69,7 @@ IRONIC_BAREMETAL_BASIC_OPS=$(trueorfalse False IRONIC_BAREMETAL_BASIC_OPS) IRONIC_ENABLED_DRIVERS=${IRONIC_ENABLED_DRIVERS:-fake,pxe_ssh,pxe_ipmitool} IRONIC_SSH_USERNAME=${IRONIC_SSH_USERNAME:-`whoami`} IRONIC_SSH_TIMEOUT=${IRONIC_SSH_TIMEOUT:-15} +IRONIC_SSH_ATTEMPTS=${IRONIC_SSH_ATTEMPTS:-5} IRONIC_SSH_KEY_DIR=${IRONIC_SSH_KEY_DIR:-$IRONIC_DATA_DIR/ssh_keys} IRONIC_SSH_KEY_FILENAME=${IRONIC_SSH_KEY_FILENAME:-ironic_key} IRONIC_KEY_FILE=${IRONIC_KEY_FILE:-$IRONIC_SSH_KEY_DIR/$IRONIC_SSH_KEY_FILENAME} @@ -89,7 +90,7 @@ IRONIC_VM_MACS_CSV_FILE=${IRONIC_VM_MACS_CSV_FILE:-$IRONIC_DATA_DIR/ironic_macs. IRONIC_AUTHORIZED_KEYS_FILE=${IRONIC_AUTHORIZED_KEYS_FILE:-$HOME/.ssh/authorized_keys} # By default, baremetal VMs will console output to file. -IRONIC_VM_LOG_CONSOLE=${IRONIC_VM_LOG_CONSOLE:-True} +IRONIC_VM_LOG_CONSOLE=$(trueorfalse True IRONIC_VM_LOG_CONSOLE) IRONIC_VM_LOG_DIR=${IRONIC_VM_LOG_DIR:-$IRONIC_DATA_DIR/logs/} IRONIC_VM_LOG_ROTATE=$(trueorfalse True IRONIC_VM_LOG_ROTATE) @@ -689,6 +690,7 @@ function _clean_ncpu_failure { function enroll_nodes { local chassis_id chassis_id=$(ironic chassis-create -d "ironic test chassis" | grep " uuid " | get_field 2) + die_if_not_set $LINENO chassis_id "Failed to create chassis" if [[ "$IRONIC_IS_HARDWARE" == "False" ]]; then local ironic_node_cpu=$IRONIC_VM_SPECS_CPU @@ -843,13 +845,16 @@ function configure_ironic_ssh_keypair { mkdir -p $HOME/.ssh chmod 700 $HOME/.ssh fi - if [[ ! -e $IRONIC_KEY_FILE ]]; then + # recreate ssh if any part is missing + if [[ ! -e $IRONIC_KEY_FILE ]] || [[ ! -e $IRONIC_KEY_FILE.pub ]]; then if [[ ! -d $(dirname $IRONIC_KEY_FILE) ]]; then mkdir -p $(dirname $IRONIC_KEY_FILE) fi echo -e 'n\n' | ssh-keygen -q -t rsa -P '' -f $IRONIC_KEY_FILE fi cat $IRONIC_KEY_FILE.pub | tee -a $IRONIC_AUTHORIZED_KEYS_FILE + # remove duplicate keys. + sort -u -o $IRONIC_AUTHORIZED_KEYS_FILE $IRONIC_AUTHORIZED_KEYS_FILE } function ironic_ssh_check { @@ -857,15 +862,26 @@ function ironic_ssh_check { local floating_ip=$2 local port=$3 local default_instance_user=$4 - local active_timeout=$5 - if ! timeout $active_timeout sh -c "while ! ssh -p $port -o StrictHostKeyChecking=no -i $key_file ${default_instance_user}@$floating_ip echo success; do sleep 1; done"; then + local attempt=$5 + local status=false + local ssh_options="-o BatchMode=yes -o ConnectTimeout=$IRONIC_SSH_TIMEOUT -o StrictHostKeyChecking=no" + while [[ $attempt -gt 0 ]]; do + ssh -p $port $ssh_options -i $key_file ${default_instance_user}@$floating_ip exit + if [[ "$?" == "0" ]]; then + status=true + break + fi + attempt=$((attempt - 1)) + echo "SSH connection failed. $attempt attempts left." + done + if ! $status; then die $LINENO "server didn't become ssh-able!" fi } function configure_ironic_auxiliary { configure_ironic_ssh_keypair - ironic_ssh_check $IRONIC_KEY_FILE $IRONIC_VM_SSH_ADDRESS $IRONIC_VM_SSH_PORT $IRONIC_SSH_USERNAME $IRONIC_SSH_TIMEOUT + ironic_ssh_check $IRONIC_KEY_FILE $IRONIC_VM_SSH_ADDRESS $IRONIC_VM_SSH_PORT $IRONIC_SSH_USERNAME $IRONIC_SSH_ATTEMPTS } function build_ipa_coreos_ramdisk { @@ -1008,9 +1024,11 @@ function cleanup_baremetal_basic_ops { local vm_name for vm_name in $(_ironic_bm_vm_names); do - sudo su $STACK_USER -c "$IRONIC_SCRIPTS_DIR/cleanup-node.sh $vm_name $IRONIC_VM_NETWORK_BRIDGE" + sudo su $STACK_USER -c "$IRONIC_SCRIPTS_DIR/cleanup-node.sh $vm_name" done + sudo ovs-vsctl --if-exists del-br $IRONIC_VM_NETWORK_BRIDGE + sudo rm -rf /etc/xinetd.d/tftp /etc/init/tftpd-hpa.override restart_service xinetd sudo iptables -D INPUT -d $HOST_IP -p udp --dport 69 -j ACCEPT || true @@ -1023,6 +1041,14 @@ function cleanup_baremetal_basic_ops { sudo rmmod nf_nat_tftp || true } +function ironic_configure_tempest { + local bm_flavor_id + bm_flavor_id=$(openstack flavor show baremetal -f value -c id) + die_if_not_set $LINENO bm_flavor_id "Failed to get id of baremetal flavor" + iniset $TEMPEST_CONFIG compute flavor_ref $bm_flavor_id + iniset $TEMPEST_CONFIG compute flavor_ref_alt $bm_flavor_id +} + # Restore xtrace + pipefail $_XTRACE_IRONIC $_PIPEFAIL_IRONIC diff --git a/devstack/plugin.sh b/devstack/plugin.sh index de584e6d9..06f83c8a4 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -26,6 +26,9 @@ if is_service_enabled ir-api ir-cond; then echo_summary "Starting Ironic" start_ironic prepare_baremetal_basic_ops + if is_service_enabled tempest; then + ironic_configure_tempest + fi fi if [[ "$1" == "unstack" ]]; then diff --git a/devstack/tools/ironic/scripts/cleanup-node.sh b/devstack/tools/ironic/scripts/cleanup-node.sh index 60cd39a3c..0fa9e25bb 100755 --- a/devstack/tools/ironic/scripts/cleanup-node.sh +++ b/devstack/tools/ironic/scripts/cleanup-node.sh @@ -11,7 +11,6 @@ LIBVIRT_STORAGE_POOL=${LIBVIRT_STORAGE_POOL:-"default"} LIBVIRT_CONNECT_URI=${LIBVIRT_CONNECT_URI:-"qemu:///system"} NAME=$1 -NETWORK_BRIDGE=$2 export VIRSH_DEFAULT_CONNECT_URI=$LIBVIRT_CONNECT_URI @@ -30,6 +29,5 @@ if virsh pool-list | grep -q $LIBVIRT_STORAGE_POOL ; then fi sudo brctl delif br-$NAME ovs-$NAME || true -sudo ovs-vsctl del-port $NETWORK_BRIDGE ovs-$NAME || true sudo ip link set dev br-$NAME down || true sudo brctl delbr br-$NAME || true diff --git a/doc/source/deploy/install-guide.rst b/doc/source/deploy/install-guide.rst index a05fe332b..b85262585 100644 --- a/doc/source/deploy/install-guide.rst +++ b/doc/source/deploy/install-guide.rst @@ -828,10 +828,10 @@ node(s) where ``ironic-conductor`` is running. #. Install tftp server and the syslinux package with the PXE boot images:: Ubuntu: (Up to and including 14.04) - sudo apt-get install tftpd-hpa syslinux-common syslinux + sudo apt-get install xinetd tftpd-hpa syslinux-common syslinux Ubuntu: (14.10 and after) - sudo apt-get install tftpd-hpa syslinux-common pxelinux + sudo apt-get install xinetd tftpd-hpa syslinux-common pxelinux Fedora 21/RHEL7/CentOS7: sudo yum install tftp-server syslinux-tftpboot @@ -839,7 +839,31 @@ node(s) where ``ironic-conductor`` is running. Fedora 22 or higher: sudo dnf install tftp-server syslinux-tftpboot -#. Setup tftp server to serve ``/tftpboot``. +#. Using xinetd to provide a tftp server setup to serve ``/tftpboot``. + Create or edit ``/etc/xinetd.d/tftp`` as below:: + + service tftp + { + protocol = udp + port = 69 + socket_type = dgram + wait = yes + user = root + server = /usr/sbin/in.tftpd + server_args = -v -v -v -v -v --map-file /tftpboot/map-file /tftpboot + disable = no + # This is a workaround for Fedora, where TFTP will listen only on + # IPv6 endpoint, if IPv4 flag is not used. + flags = IPv4 + } + + and restart xinetd service:: + + Ubuntu: + sudo service xinetd restart + + Fedora: + sudo systemctl restart xinetd #. Copy the PXE image to ``/tftpboot``. The PXE image might be found at [1]_:: @@ -876,11 +900,6 @@ node(s) where ``ironic-conductor`` is running. echo 're ^(^/) /tftpboot/\1' >> /tftpboot/map-file echo 're ^([^/]) /tftpboot/\1' >> /tftpboot/map-file -#. Enable tftp map file, modify ``/etc/xinetd.d/tftp`` as below and restart xinetd - service:: - - server_args = -v -v -v -v -v --map-file /tftpboot/map-file /tftpboot - .. [1] On **Fedora/RHEL** the ``syslinux-tftpboot`` package already install the library modules and PXE image at ``/tftpboot``. If the TFTP server is configured to listen to a different directory you should copy the @@ -1386,11 +1405,6 @@ bare metal node to be deployed with when Ironic is responsible for partitioning the disk; therefore choosing the disk label does not apply when the image being deployed is a ``whole disk image``. -.. note:: - At present the agent_* drivers do not support deploying partition - images, therefore forcing the disk label for those drivers is not - possible. - There are some edge cases where someone may want to choose a specific disk label for the images being deployed, including but not limited to: @@ -2019,6 +2033,72 @@ of the following ways: <http://docs.openstack.org/developer/swift/deployment_guide.html>`_ (recommended only for testing purpose by swift). +.. _EnableHTTPSinGlance: + +Enabling HTTPS in Image service +=============================== + +Ironic drivers usually use Image service during node provisioning. By default, +image service does not use HTTPS, but it is required for secure communication. +It can be enabled by making the following changes to ``/etc/glance/glance-api.conf``: + +#. `Configuring SSL support + <http://docs.openstack.org/developer/glance/configuring.html#configuring-ssl-support>`_ + +#. Restart the glance-api service:: + + Fedora/RHEL7/CentOS7: + sudo systemctl restart openstack-glance-api + + Debian/Ubuntu: + sudo service glance-api restart + +See the `Glance <http://docs.openstack.org/developer/glance/>`_ documentation, +for more details on the Image service. + +Enabling HTTPS communication between Image service and Object storage +===================================================================== + +This section describes the steps needed to enable secure HTTPS communication between +Image service and Object storage when Object storage is used as the Backend. + +To enable secure HTTPS communication between Image service and Object storage follow these steps: + +#. :ref:`EnableHTTPSinSwift`. + +#. `Configure Swift Storage Backend + <http://docs.openstack.org/developer/glance/configuring.html#configuring-the-swift-storage-backend>`_ + +#. :ref:`EnableHTTPSinGlance` + +Enabling HTTPS communication between Image service and Bare Metal service +========================================================================= + +This section describes the steps needed to enable secure HTTPS communication between +Image service and Bare Metal service. + +To enable secure HTTPS communication between Bare Metal service and Image service follow these steps: + +#. Edit ``/etc/ironic/ironic.conf``:: + + [glance] + ... + glance_cafile=/path/to/certfile + glance_protocol=https + glance_api_insecure=False + + .. note:: + 'glance_cafile' is a optional path to a CA certificate bundle to be used to validate the SSL certificate + served by Image service. + +#. Restart ironic-conductor service:: + + Fedora/RHEL7/CentOS7: + sudo systemctl restart openstack-ironic-conductor + + Debian/Ubuntu: + sudo service ironic-conductor restart + Using Bare Metal service as a standalone service ================================================ diff --git a/doc/source/deploy/user-guide.rst b/doc/source/deploy/user-guide.rst index b586379ea..9c0f503ba 100644 --- a/doc/source/deploy/user-guide.rst +++ b/doc/source/deploy/user-guide.rst @@ -173,8 +173,12 @@ through the steps involved during the provisioning of a bare metal instance. These pre-requisites must be met before the deployment process: -- Dependent packages to be configured on the compute node like tftp-server, - ipmi, syslinux etc for bare metal provisioning. +- Dependent packages to be configured on the Bare Metal service node(s) + where ironic-conductor is running like tftp-server, ipmi, syslinux etc for + bare metal provisioning. +- Nova must be configured to make use of the bare metal service endpoint + and compute driver should be configured to use ironic driver on the Nova + compute node(s). - Flavors to be created for the available hardware. Nova must know the flavor to boot from. - Images to be made available in Glance. Listed below are some image types diff --git a/doc/source/drivers/ilo.rst b/doc/source/drivers/ilo.rst index 541d27af4..348b4119f 100644 --- a/doc/source/drivers/ilo.rst +++ b/doc/source/drivers/ilo.rst @@ -28,7 +28,8 @@ it from data channel which is used for deployment. ``iscsi_ilo`` and ``agent_ilo`` drivers use deployment ramdisk built from ``diskimage-builder``. The ``iscsi_ilo`` driver deploys from ironic conductor and supports both net-boot and local-boot of instance. -``agent_ilo`` deploys from bare metal node and always does local-boot. +``agent_ilo`` deploys from bare metal node and supports both net-boot +and local-boot of instance. ``pxe_ilo`` driver uses PXE/iSCSI for deployment (just like normal PXE driver) and deploys from ironic conductor. Additionally it supports automatic setting of @@ -206,12 +207,12 @@ Target Users security enhanced PXE-less deployment mechanism. The PXE driver passes management information in clear-text to the - bare metal node. However, if swift proxy server has an HTTPS - endpoint (See :ref:`EnableHTTPSinSwift` for more information), the - ``iscsi_ilo`` driver provides enhanced security by passing - management information to and from swift endpoint over HTTPS. The - management information, deploy ramdisk and boot images for the instance will - be retrieved over encrypted management network via iLO virtual media. + bare metal node. However, if swift proxy server and glance have HTTPS + endpoints (See :ref:`EnableHTTPSinSwift`, :ref:`EnableHTTPSinGlance` for more + information), the ``iscsi_ilo`` driver provides enhanced security by + exchanging management information with swift and glance endpoints over HTTPS. + The management information, deploy ramdisk and boot images for the instance + will be retrieved over encrypted management network via iLO virtual media. Tested Platforms ~~~~~~~~~~~~~~~~ @@ -239,11 +240,11 @@ Features * UEFI Boot Support * UEFI Secure Boot Support * Passing management information via secure, encrypted management network - (virtual media) if swift proxy server has an HTTPS endpoint. See - :ref:`EnableHTTPSinSwift` for more info. User image provisioning is done - using iSCSI over data network, so this driver has the benefit - of security enhancement with the same performance. It segregates management - info from data channel. + (virtual media) if swift proxy server and glance have HTTPS endpoints. See + :ref:`EnableHTTPSinSwift`, :ref:`EnableHTTPSinGlance` for more info. User + image provisioning is done using iSCSI over data network, so this driver has + the benefit of security enhancement with the same performance. It segregates + management info from data channel. * Support for out-of-band cleaning operations. * Remote Console * HW Sensors @@ -267,8 +268,10 @@ Requirements Deploy Process ~~~~~~~~~~~~~~ -Refer to `Netboot with glance and swift`_ for the deploy process of partition image -and `Localboot with glance and swift`_ for the deploy process of whole disk image. +Refer to `Netboot with glance and swift`_ and +`Localboot with glance and swift for partition images`_ for the deploy process +of partition image and `Localboot with glance and swift`_ for the deploy +process of whole disk image. Configuring and Enabling the driver ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -348,12 +351,12 @@ Target Users want to have a security enhanced PXE-less deployment mechanism. The PXE based agent drivers pass management information in clear-text to - the bare metal node. However, if swift proxy server has an HTTPS - endpoint (See :ref:`EnableHTTPSinSwift` for more information), - the ``agent_ilo`` driver provides enhanced security by passing authtoken - and management information to and from swift endpoint over HTTPS. The - management information and deploy ramdisk will be retrieved over encrypted - management network via iLO. + the bare metal node. However, if swift proxy server and glance have HTTPS + endpoints (See :ref:`EnableHTTPSinSwift`, :ref:`EnableHTTPSinGlance` for more + information), the ``agent_ilo`` driver provides enhanced security by + exchanging authtoken and management information with swift and glance + endpoints over HTTPS. The management information and deploy ramdisk will be + retrieved over encrypted management network via iLO. Tested Platforms ~~~~~~~~~~~~~~~~ @@ -376,7 +379,9 @@ Features * Remote Console * HW Sensors * IPA runs on the bare metal node and pulls the image directly from swift. -* IPA deployed instances always boots from local disk. +* Supports booting the instance from virtual media (netboot) as well as booting + locally from disk. By default, the instance will always boot from virtual + media for partition images. * Segregates management info from data channel. * UEFI Boot Support * UEFI Secure Boot Support @@ -400,7 +405,10 @@ Requirements Deploy Process ~~~~~~~~~~~~~~ -Refer to `Localboot with glance and swift`_ for details. +Refer to `Netboot with glance and swift`_ and +`Localboot with glance and swift for partition images`_ for the deploy process +of partition image and `Localboot with glance and swift`_ for the deploy +process of whole disk image. Configuring and Enabling the driver ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -943,11 +951,10 @@ intermediate images on conductor as described in Deploy Process ~~~~~~~~~~~~~~ -``iscsi_ilo`` supports both netboot and localboot, while ``agent_ilo`` supports -only localboot. Refer to `Netboot in standalone ironic`_ and -`Localboot in standalone ironic`_ for details of deploy process -for netboot and localboot respectively. For ``pxe_ilo``, the deploy process -is same as native ``pxe_ipmitool`` driver. +``iscsi_ilo`` and ``agent_ilo`` supports both netboot and localboot. Refer +to `Netboot in standalone ironic`_ and `Localboot in standalone ironic`_ +for details of deploy process for netboot and localboot respectively. +For ``pxe_ilo``, the deploy process is same as native ``pxe_ipmitool`` driver. Deploy Process ============== @@ -996,6 +1003,48 @@ Netboot with glance and swift Baremetal -> Baremetal [label = "Instance kernel finds root partition and continues booting from disk"]; } +Localboot with glance and swift for partition images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. seqdiag:: + :scale: 80 + + diagram { + Glance; Conductor; Baremetal; Swift; IPA; iLO; + activation = none; + span_height = 1; + edge_length = 250; + default_note_color = white; + default_fontsize = 14; + + Conductor -> iLO [label = "Powers off the node"]; + Conductor -> Glance [label = "Get the metadata for deploy ISO"]; + Glance -> Conductor [label = "Returns the metadata for deploy ISO"]; + Conductor -> Conductor [label = "Generates swift tempURL for deploy ISO"]; + Conductor -> Conductor [label = "Creates the FAT32 image containing ironic API URL and driver name"]; + Conductor -> Swift [label = "Uploads the FAT32 image"]; + Conductor -> Conductor [label = "Generates swift tempURL for FAT32 image"]; + Conductor -> iLO [label = "Attaches the FAT32 image swift tempURL as virtual media floppy"]; + Conductor -> iLO [label = "Attaches the deploy ISO swift tempURL as virtual media CDROM"]; + Conductor -> iLO [label = "Sets one time boot to CDROM"]; + Conductor -> iLO [label = "Reboot the node"]; + iLO -> Swift [label = "Downloads deploy ISO"]; + Baremetal -> iLO [label = "Boots deploy kernel/ramdisk from iLO virtual media CDROM"]; + IPA -> Conductor [label = "Lookup node"]; + Conductor -> IPA [label = "Provides node UUID"]; + IPA -> Conductor [label = "Heartbeat"]; + Conductor -> IPA [label = "Sends the user image HTTP(S) URL"]; + IPA -> Swift [label = "Retrieves the user image on bare metal"]; + IPA -> IPA [label = "Writes user image to root partition"]; + IPA -> IPA [label = "Installs boot loader"]; + IPA -> Conductor [label = "Heartbeat"]; + Conductor -> Baremetal [label = "Sets boot device to disk"]; + Conductor -> IPA [label = "Power off the node"]; + Conductor -> iLO [label = "Power on the node"]; + Baremetal -> Baremetal [label = "Boot user image from disk"]; + } + + Localboot with glance and swift ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/drivers/ipa.rst b/doc/source/drivers/ipa.rst index aa5ecf7a0..bebbafba5 100644 --- a/doc/source/drivers/ipa.rst +++ b/doc/source/drivers/ipa.rst @@ -104,3 +104,23 @@ Steps to enable proxies ``image_no_proxy`` to driver_info properties in each node that will use the proxy. Please refer to ``ironic driver-properties`` output of the ``agent_*`` driver you're using for descriptions of these properties. + +Advanced configuration +====================== + +Out-of-band Vs. in-band power off on deploy +------------------------------------------- + +After deploying an image onto the node's hard disk Ironic will reboot +the machine into the new image. By default this power action happens +``in-band``, meaning that the ironic-conductor will instruct the IPA +ramdisk to power itself off. + +Some hardware may have a problem with the default approach and +would require Ironic to talk directly to the management controller +to switch the power off and on again. In order to tell Ironic to do +that you have to update the node's ``driver_info`` field and set the +``deploy_forces_oob_reboot`` parameter with the value of **True**. For +example, the below command sets this configuration in a specific node:: + + ironic node-update <UUID or name> add driver_info/deploy_forces_oob_reboot=True diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index cc651034d..7a53e1af5 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -919,6 +919,10 @@ # Deprecated group/name - [agent]/agent_erase_devices_iterations #erase_devices_iterations=1 +# Whether to power off a node after deploy failure. Defaults +# to True. (boolean value) +#power_off_after_deploy_failure=true + [dhcp] diff --git a/ironic/api/controllers/v1/chassis.py b/ironic/api/controllers/v1/chassis.py index 819278fae..df5f9df78 100644 --- a/ironic/api/controllers/v1/chassis.py +++ b/ironic/api/controllers/v1/chassis.py @@ -143,6 +143,9 @@ class ChassisCollection(collection.Collection): @classmethod def sample(cls): + # FIXME(jroll) hack for docs build, bug #1560508 + if not hasattr(objects, 'Chassis'): + objects.register_all() sample = cls() sample.chassis = [Chassis.sample(expand=False)] return sample diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 30d52e868..381eb3ce6 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -1051,7 +1051,7 @@ class NodesController(rest.RestController): If it does, is necessary to prevent updating it because the new driver will not be able to stop a console started by the previous one. - :param rpc_node: RPC Node object to be veryfied. + :param rpc_node: RPC Node object to be verified. :param node_ident: the UUID or logical name of a node. :raises: wsme.exc.ClientSideError """ diff --git a/ironic/api/hooks.py b/ironic/api/hooks.py index 3c406e554..764b0a434 100644 --- a/ironic/api/hooks.py +++ b/ironic/api/hooks.py @@ -89,6 +89,15 @@ class ContextHook(hooks.PecanHook): show_password=show_password, **creds) + def after(self, state): + if state.request.context == {}: + # An incorrect url path will not create RequestContext + return + # NOTE(lintan): RequestContext will generate a request_id if no one + # passing outside, so it always contain a request_id. + request_id = state.request.context.request_id + state.response.headers['Openstack-Request-Id'] = request_id + class RPCHook(hooks.PecanHook): """Attach the rpcapi object to the request so controllers can get to it.""" diff --git a/ironic/common/driver_factory.py b/ironic/common/driver_factory.py index 191ced70a..6e834d4f7 100644 --- a/ironic/common/driver_factory.py +++ b/ironic/common/driver_factory.py @@ -23,6 +23,7 @@ from stevedore import dispatch from ironic.common import exception from ironic.common.i18n import _ from ironic.common.i18n import _LI +from ironic.common.i18n import _LW from ironic.drivers import base as driver_base @@ -136,6 +137,18 @@ class DriverFactory(object): if cls._extension_manager: return + # Check for duplicated driver entries and warn the operator + # about them + counter = collections.Counter(CONF.enabled_drivers).items() + duplicated_drivers = list(dup for (dup, i) in counter if i > 1) + if duplicated_drivers: + LOG.warning(_LW('The driver(s) "%s" is/are duplicated in the ' + 'list of enabled_drivers. Please check your ' + 'configuration file.'), + ', '.join(duplicated_drivers)) + + enabled_drivers = set(CONF.enabled_drivers) + # NOTE(deva): Drivers raise "DriverLoadError" if they are unable to be # loaded, eg. due to missing external dependencies. # We capture that exception, and, only if it is for an @@ -147,13 +160,13 @@ class DriverFactory(object): def _catch_driver_not_found(mgr, ep, exc): # NOTE(deva): stevedore loads plugins *before* evaluating # _check_func, so we need to check here, too. - if ep.name in CONF.enabled_drivers: + if ep.name in enabled_drivers: if not isinstance(exc, exception.DriverLoadError): raise exception.DriverLoadError(driver=ep.name, reason=exc) raise exc def _check_func(ext): - return ext.name in CONF.enabled_drivers + return ext.name in enabled_drivers cls._extension_manager = ( dispatch.NameDispatchExtensionManager( @@ -164,10 +177,10 @@ class DriverFactory(object): # NOTE(deva): if we were unable to load any configured driver, perhaps # because it is not present on the system, raise an error. - if (sorted(CONF.enabled_drivers) != + if (sorted(enabled_drivers) != sorted(cls._extension_manager.names())): found = cls._extension_manager.names() - names = [n for n in CONF.enabled_drivers if n not in found] + names = [n for n in enabled_drivers if n not in found] # just in case more than one could not be found ... names = ', '.join(names) raise exception.DriverNotFound(driver_name=names) diff --git a/ironic/conductor/task_manager.py b/ironic/conductor/task_manager.py index 3aed594ae..caca8a17b 100644 --- a/ironic/conductor/task_manager.py +++ b/ironic/conductor/task_manager.py @@ -449,7 +449,7 @@ class TaskManager(object): # also makes it easier to test. fut.add_done_callback(self._thread_release_resources) # Don't unlock! The unlock will occur when the - # thread finshes. + # thread finishes. return except Exception as e: with excutils.save_and_reraise_exception(): diff --git a/ironic/db/api.py b/ironic/db/api.py index ca2715345..f8ab5a686 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -369,7 +369,7 @@ class Connection(object): def create_portgroup(self, values): """Create a new portgroup. - :param values: Dict of values with the the following keys: + :param values: Dict of values with the following keys: 'id' 'uuid' 'name' diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index 65aba00c5..0c9814bd8 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -486,6 +486,10 @@ class AgentVendorInterface(agent_base_vendor.BaseAgentVendor): else: image_info['deploy_boot_mode'] = 'bios' image_info['boot_option'] = boot_option + disk_label = deploy_utils.get_disk_label(node) + if disk_label is not None: + image_info['disk_label'] = disk_label + image_info['node_uuid'] = node.uuid # Tell the client to download and write the image with the given args self._client.prepare_image(node, image_info) diff --git a/ironic/drivers/modules/agent_base_vendor.py b/ironic/drivers/modules/agent_base_vendor.py index bf10d1a35..9df0cd392 100644 --- a/ironic/drivers/modules/agent_base_vendor.py +++ b/ironic/drivers/modules/agent_base_vendor.py @@ -22,6 +22,7 @@ import time from oslo_config import cfg from oslo_log import log from oslo_utils import excutils +from oslo_utils import strutils from oslo_utils import timeutils import retrying @@ -82,6 +83,13 @@ LOG = log.getLogger(__name__) # completing 'delete_configuration' of raid interface. POST_CLEAN_STEP_HOOKS = {} +VENDOR_PROPERTIES = { + 'deploy_forces_oob_reboot': _( + 'Whether Ironic should force a reboot of the Node via the out-of-band ' + 'channel after deployment is complete. Provides compatiblity with ' + 'older deploy ramdisks. Defaults to False. Optional.') +} + def _get_client(): client = agent_client.AgentClient() @@ -178,9 +186,7 @@ class BaseAgentVendor(base.VendorInterface): :returns: dictionary of <property name>:<property description> entries. """ - # NOTE(jroll) all properties are set by the driver, - # not by the operator. - return {} + return VENDOR_PROPERTIES def validate(self, task, method, **kwargs): """Validate the driver-specific Node deployment info. @@ -688,18 +694,38 @@ class BaseAgentVendor(base.VendorInterface): return task.driver.power.get_power_state(task) node = task.node + # Whether ironic should power off the node via out-of-band or + # in-band methods + oob_power_off = strutils.bool_from_string( + node.driver_info.get('deploy_forces_oob_reboot', False)) try: - try: - self._client.power_off(node) - _wait_until_powered_off(task) - except Exception as e: - LOG.warning( - _LW('Failed to soft power off node %(node_uuid)s ' - 'in at least %(timeout)d seconds. Error: %(error)s'), - {'node_uuid': node.uuid, - 'timeout': (wait * (attempts - 1)) / 1000, - 'error': e}) + if not oob_power_off: + try: + self._client.power_off(node) + _wait_until_powered_off(task) + except Exception as e: + LOG.warning( + _LW('Failed to soft power off node %(node_uuid)s ' + 'in at least %(timeout)d seconds. ' + 'Error: %(error)s'), + {'node_uuid': node.uuid, + 'timeout': (wait * (attempts - 1)) / 1000, + 'error': e}) + else: + # Flush the file system prior to hard rebooting the node + result = self._client.sync(node) + error = result.get('faultstring') + if error: + if 'Unknown command' in error: + error = _('The version of the IPA ramdisk used in ' + 'the deployment do not support the ' + 'command "sync"') + LOG.warning(_LW( + 'Failed to flush the file system prior to hard ' + 'rebooting the node %(node)s. Error: %(error)s'), + {'node': node.uuid, 'error': error}) + manager_utils.node_power_action(task, states.REBOOT) except Exception as e: msg = (_('Error rebooting node %(node)s after deploy. ' diff --git a/ironic/drivers/modules/agent_client.py b/ironic/drivers/modules/agent_client.py index 86c985e59..d27c8dbd1 100644 --- a/ironic/drivers/modules/agent_client.py +++ b/ironic/drivers/modules/agent_client.py @@ -174,3 +174,10 @@ class AgentClient(object): return self._command(node=node, method='standby.power_off', params={}) + + def sync(self, node): + """Flush file system buffers forcing changed blocks to disk.""" + return self._command(node=node, + method='standby.sync', + params={}, + wait=True) diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 7b615a9b8..7baa13cb6 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -71,6 +71,10 @@ deploy_opts = [ deprecated_group='agent', default=1, help=_('Number of iterations to be run for erasing devices.')), + cfg.BoolOpt('power_off_after_deploy_failure', + default=True, + help=_('Whether to power off a node after deploy failure. ' + 'Defaults to True.')), ] CONF = cfg.CONF CONF.register_opts(deploy_opts, group='deploy') @@ -492,15 +496,15 @@ def set_failed_state(task, msg): % {'node': node.uuid, 'state': node.provision_state}) LOG.exception(msg2) - try: - manager_utils.node_power_action(task, states.POWER_OFF) - except Exception: - msg2 = (_LE('Node %s failed to power off while handling deploy ' - 'failure. This may be a serious condition. Node ' - 'should be removed from Ironic or put in maintenance ' - 'mode until the problem is resolved.') % node.uuid) - LOG.exception(msg2) - + if CONF.deploy.power_off_after_deploy_failure: + try: + manager_utils.node_power_action(task, states.POWER_OFF) + except Exception: + msg2 = (_LE('Node %s failed to power off while handling deploy ' + 'failure. This may be a serious condition. Node ' + 'should be removed from Ironic or put in maintenance ' + 'mode until the problem is resolved.') % node.uuid) + LOG.exception(msg2) # NOTE(deva): node_power_action() erases node.last_error # so we need to set it here. node.last_error = msg @@ -623,7 +627,7 @@ def agent_execute_clean_step(task, step): def agent_add_clean_params(task): - """Add required config parameters to node's driver_interal_info. + """Add required config parameters to node's driver_internal_info. Adds the required conf options to node's driver_internal_info. It is Required to pass the information to IPA. diff --git a/ironic/drivers/modules/ilo/vendor.py b/ironic/drivers/modules/ilo/vendor.py index d991a145d..981afa599 100644 --- a/ironic/drivers/modules/ilo/vendor.py +++ b/ironic/drivers/modules/ilo/vendor.py @@ -57,9 +57,6 @@ class IloVirtualMediaAgentVendorInterface(agent.AgentVendorInterface): class VendorPassthru(iscsi_deploy.VendorPassthru): """Vendor-specific interfaces for iLO deploy drivers.""" - def get_properties(self): - return {} - def validate(self, task, method, **kwargs): """Validate vendor-specific actions. diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index 0206b738d..bc111643e 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -701,9 +701,6 @@ class ISCSIDeploy(base.DeployInterface): class VendorPassthru(agent_base_vendor.BaseAgentVendor): """Interface to mix IPMI and PXE vendor-specific interfaces.""" - def get_properties(self): - return {} - def validate(self, task, method, **kwargs): """Validates the inputs for a vendor passthru. diff --git a/ironic/tests/unit/api/test_hooks.py b/ironic/tests/unit/api/test_hooks.py index c4372a3c4..e29ea298f 100644 --- a/ironic/tests/unit/api/test_hooks.py +++ b/ironic/tests/unit/api/test_hooks.py @@ -285,6 +285,26 @@ class TestContextHook(base.BaseApiTest): is_admin=False, roles=headers['X-Roles'].split(',')) + @mock.patch.object(context, 'RequestContext') + def test_context_hook_after_add_request_id(self, mock_ctx): + headers = fake_headers(admin=True) + reqstate = FakeRequestState(headers=headers) + reqstate.set_context() + reqstate.request.context.request_id = 'fake-id' + context_hook = hooks.ContextHook(None) + context_hook.after(reqstate) + self.assertIn('Openstack-Request-Id', + reqstate.response.headers) + self.assertEqual( + 'fake-id', + reqstate.response.headers['Openstack-Request-Id']) + + def test_context_hook_after_miss_context(self): + response = self.get_json('/bad/path', + expect_errors=True) + self.assertNotIn('Openstack-Request-Id', + response.headers) + class TestTrustedCallHook(base.BaseApiTest): def test_trusted_call_hook_not_admin(self): diff --git a/ironic/tests/unit/common/test_driver_factory.py b/ironic/tests/unit/common/test_driver_factory.py index fae28f46a..e42fe1541 100644 --- a/ironic/tests/unit/common/test_driver_factory.py +++ b/ironic/tests/unit/common/test_driver_factory.py @@ -64,6 +64,14 @@ class DriverLoadTestCase(base.TestCase): driver_factory.DriverFactory._init_extension_manager() self.assertEqual(2, mock_em.call_count) + @mock.patch.object(driver_factory.LOG, 'warning', autospec=True) + def test_driver_duplicated_entry(self, mock_log): + self.config(enabled_drivers=['fake', 'fake']) + driver_factory.DriverFactory._init_extension_manager() + self.assertEqual( + ['fake'], driver_factory.DriverFactory._extension_manager.names()) + self.assertTrue(mock_log.called) + class GetDriverTestCase(base.TestCase): def setUp(self): diff --git a/ironic/tests/unit/common/test_hash_ring.py b/ironic/tests/unit/common/test_hash_ring.py index ae38250b8..1966b9e8e 100644 --- a/ironic/tests/unit/common/test_hash_ring.py +++ b/ironic/tests/unit/common/test_hash_ring.py @@ -259,5 +259,5 @@ class HashRingManagerTestCase(db_base.DbTestCase): self.ring_manager.__getitem__, 'driver1') self.register_conductors() - self.ring_manager.updated_at = time.time() - 30 + self.ring_manager.updated_at = time.time() - 31 self.ring_manager.__getitem__('driver1') diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 48cf4c038..98a54635f 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -249,13 +249,15 @@ class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin, node = obj_utils.create_test_node(self.context, driver='fake', instance_uuid=None, power_state=states.NOSTATE) - node.instance_uuid = 'fake-uuid' + uuid1 = uuidutils.generate_uuid() + uuid2 = uuidutils.generate_uuid() + node.instance_uuid = uuid1 self.service.update_node(self.context, node) # Check if the change was applied - node.instance_uuid = 'meow' + node.instance_uuid = uuid2 node.refresh() - self.assertEqual('fake-uuid', node.instance_uuid) + self.assertEqual(uuid1, node.instance_uuid) def test_associate_node_powered_off(self): self._test_associate_node(states.POWER_OFF) @@ -2548,8 +2550,8 @@ class DestroyNodeTestCase(mgr_utils.ServiceSetUpMixin, def test_destroy_node_associated(self): self._start_service() - node = obj_utils.create_test_node(self.context, - instance_uuid='fake-uuid') + node = obj_utils.create_test_node( + self.context, instance_uuid=uuidutils.generate_uuid()) exc = self.assertRaises(messaging.rpc.ExpectedException, self.service.destroy_node, @@ -2577,10 +2579,9 @@ class DestroyNodeTestCase(mgr_utils.ServiceSetUpMixin, def test_destroy_node_allowed_in_maintenance(self): self._start_service() - node = obj_utils.create_test_node(self.context, - instance_uuid='fake-uuid', - provision_state=states.ACTIVE, - maintenance=True) + node = obj_utils.create_test_node( + self.context, instance_uuid=uuidutils.generate_uuid(), + provision_state=states.ACTIVE, maintenance=True) self.service.destroy_node(self.context, node.uuid) self.assertRaises(exception.NodeNotFound, self.dbapi.get_node_by_uuid, @@ -3803,7 +3804,8 @@ class ManagerTestProperties(tests_db_base.DbTestCase): self._check_driver_properties("fake_ssh", expected) def test_driver_properties_fake_pxe(self): - expected = ['deploy_kernel', 'deploy_ramdisk'] + expected = ['deploy_kernel', 'deploy_ramdisk', + 'deploy_forces_oob_reboot'] self._check_driver_properties("fake_pxe", expected) def test_driver_properties_fake_seamicro(self): @@ -3824,34 +3826,37 @@ class ManagerTestProperties(tests_db_base.DbTestCase): 'ipmi_transit_address', 'ipmi_target_channel', 'ipmi_target_address', 'ipmi_local_address', 'deploy_kernel', 'deploy_ramdisk', 'ipmi_protocol_version', - 'ipmi_force_boot_device' - ] + 'ipmi_force_boot_device', 'deploy_forces_oob_reboot'] self._check_driver_properties("pxe_ipmitool", expected) def test_driver_properties_pxe_ipminative(self): expected = ['ipmi_address', 'ipmi_password', 'ipmi_username', 'deploy_kernel', 'deploy_ramdisk', - 'ipmi_terminal_port', 'ipmi_force_boot_device'] + 'ipmi_terminal_port', 'ipmi_force_boot_device', + 'deploy_forces_oob_reboot'] self._check_driver_properties("pxe_ipminative", expected) def test_driver_properties_pxe_ssh(self): expected = ['deploy_kernel', 'deploy_ramdisk', 'ssh_address', 'ssh_username', 'ssh_virt_type', 'ssh_key_contents', 'ssh_key_filename', - 'ssh_password', 'ssh_port', 'ssh_terminal_port'] + 'ssh_password', 'ssh_port', 'ssh_terminal_port', + 'deploy_forces_oob_reboot'] self._check_driver_properties("pxe_ssh", expected) def test_driver_properties_pxe_seamicro(self): expected = ['deploy_kernel', 'deploy_ramdisk', 'seamicro_api_endpoint', 'seamicro_password', 'seamicro_server_id', 'seamicro_username', - 'seamicro_api_version', 'seamicro_terminal_port'] + 'seamicro_api_version', 'seamicro_terminal_port', + 'deploy_forces_oob_reboot'] self._check_driver_properties("pxe_seamicro", expected) def test_driver_properties_pxe_snmp(self): expected = ['deploy_kernel', 'deploy_ramdisk', 'snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version', - 'snmp_community', 'snmp_security', 'snmp_outlet'] + 'snmp_community', 'snmp_security', 'snmp_outlet', + 'deploy_forces_oob_reboot'] self._check_driver_properties("pxe_snmp", expected) def test_driver_properties_fake_ilo(self): @@ -3862,13 +3867,15 @@ class ManagerTestProperties(tests_db_base.DbTestCase): def test_driver_properties_ilo_iscsi(self): expected = ['ilo_address', 'ilo_username', 'ilo_password', 'client_port', 'client_timeout', 'ilo_deploy_iso', - 'console_port', 'ilo_change_password'] + 'console_port', 'ilo_change_password', + 'deploy_forces_oob_reboot'] self._check_driver_properties("iscsi_ilo", expected) def test_driver_properties_agent_ilo(self): expected = ['ilo_address', 'ilo_username', 'ilo_password', 'client_port', 'client_timeout', 'ilo_deploy_iso', - 'console_port', 'ilo_change_password'] + 'console_port', 'ilo_change_password', + 'deploy_forces_oob_reboot'] self._check_driver_properties("agent_ilo", expected) def test_driver_properties_fail(self): diff --git a/ironic/tests/unit/drivers/modules/cimc/test_common.py b/ironic/tests/unit/drivers/modules/cimc/test_common.py index a32c9f2ab..03aa45d7e 100644 --- a/ironic/tests/unit/drivers/modules/cimc/test_common.py +++ b/ironic/tests/unit/drivers/modules/cimc/test_common.py @@ -16,6 +16,7 @@ import mock from oslo_config import cfg from oslo_utils import importutils +from oslo_utils import uuidutils from ironic.common import exception from ironic.conductor import task_manager @@ -39,7 +40,7 @@ class CIMCBaseTestCase(db_base.DbTestCase): self.context, driver='fake_cimc', driver_info=db_utils.get_test_cimc_info(), - instance_uuid="fake_uuid") + instance_uuid=uuidutils.generate_uuid()) CONF.set_override('max_retry', 2, 'cimc') CONF.set_override('action_interval', 0, 'cimc') diff --git a/ironic/tests/unit/drivers/modules/ilo/test_power.py b/ironic/tests/unit/drivers/modules/ilo/test_power.py index 41b2f4dcc..4df4a8ab8 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_power.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_power.py @@ -18,6 +18,7 @@ import mock from oslo_config import cfg from oslo_utils import importutils +from oslo_utils import uuidutils from ironic.common import boot_devices from ironic.common import exception @@ -47,7 +48,7 @@ class IloPowerInternalMethodsTestCase(db_base.DbTestCase): self.node = db_utils.create_test_node( driver='fake_ilo', driver_info=driver_info, - instance_uuid='instance_uuid_123') + instance_uuid=uuidutils.generate_uuid()) CONF.set_override('power_retry', 2, 'ilo') CONF.set_override('power_wait', 0, 'ilo') diff --git a/ironic/tests/unit/drivers/modules/irmc/test_power.py b/ironic/tests/unit/drivers/modules/irmc/test_power.py index 4d3af5e2b..7f01d2993 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_power.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_power.py @@ -18,6 +18,7 @@ Test class for iRMC Power Driver import mock from oslo_config import cfg +from oslo_utils import uuidutils from ironic.common import exception from ironic.common import states @@ -45,7 +46,7 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase): self.node = db_utils.create_test_node( driver='fake_irmc', driver_info=driver_info, - instance_uuid='instance_uuid_123') + instance_uuid=uuidutils.generate_uuid()) @mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed') def test__set_power_state_power_on_ok( diff --git a/ironic/tests/unit/drivers/modules/test_agent.py b/ironic/tests/unit/drivers/modules/test_agent.py index 147612e3e..c22a4c1e4 100644 --- a/ironic/tests/unit/drivers/modules/test_agent.py +++ b/ironic/tests/unit/drivers/modules/test_agent.py @@ -690,7 +690,8 @@ class TestAgentVendor(db_base.DbTestCase): i_info['image_type'] = 'partition' i_info['root_mb'] = 10240 i_info['deploy_boot_mode'] = 'bios' - i_info['capabilities'] = '{"boot_option": "local"}' + i_info['capabilities'] = {"boot_option": "local", + "disk_label": "msdos"} self.node.instance_info = i_info driver_internal_info = self.node.driver_internal_info driver_internal_info['is_whole_disk_image'] = False @@ -700,6 +701,7 @@ class TestAgentVendor(db_base.DbTestCase): expected_image_info = { 'urls': [test_temp_url], 'id': 'fake-image', + 'node_uuid': self.node.uuid, 'checksum': 'checksum', 'disk_format': 'qcow2', 'container_format': 'bare', @@ -715,7 +717,8 @@ class TestAgentVendor(db_base.DbTestCase): 'image_type': 'partition', 'root_mb': 10240, 'boot_option': 'local', - 'deploy_boot_mode': 'bios' + 'deploy_boot_mode': 'bios', + 'disk_label': 'msdos' } client_mock = mock.MagicMock(spec_set=['prepare_image']) diff --git a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py index b4c1ded17..29f4ee94b 100644 --- a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py +++ b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py @@ -684,6 +684,60 @@ class TestBaseAgentVendor(db_base.DbTestCase): self.assertEqual(states.DEPLOYFAIL, task.node.provision_state) self.assertEqual(states.ACTIVE, task.node.target_provision_state) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'sync', + spec=types.FunctionType) + def test_reboot_and_finish_deploy_power_action_oob_power_off( + self, sync_mock, node_power_action_mock): + # Enable force power off + driver_info = self.node.driver_info + driver_info['deploy_forces_oob_reboot'] = True + self.node.driver_info = driver_info + + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.passthru.reboot_and_finish_deploy(task) + + sync_mock.assert_called_once_with(task.node) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + + @mock.patch.object(agent_base_vendor.LOG, 'warning', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + @mock.patch.object(agent_client.AgentClient, 'sync', + spec=types.FunctionType) + def test_reboot_and_finish_deploy_power_action_oob_power_off_failed( + self, sync_mock, node_power_action_mock, log_mock): + # Enable force power off + driver_info = self.node.driver_info + driver_info['deploy_forces_oob_reboot'] = True + self.node.driver_info = driver_info + + self.node.provision_state = states.DEPLOYING + self.node.target_provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + sync_mock.return_value = {'faultstring': 'Unknown command: blah'} + self.passthru.reboot_and_finish_deploy(task) + + sync_mock.assert_called_once_with(task.node) + node_power_action_mock.assert_called_once_with( + task, states.REBOOT) + self.assertEqual(states.ACTIVE, task.node.provision_state) + self.assertEqual(states.NOSTATE, task.node.target_provision_state) + log_error = ('The version of the IPA ramdisk used in the ' + 'deployment do not support the command "sync"') + log_mock.assert_called_once_with( + 'Failed to flush the file system prior to hard rebooting the ' + 'node %(node)s. Error: %(error)s', + {'node': task.node.uuid, 'error': log_error}) + @mock.patch.object(agent_client.AgentClient, 'install_bootloader', autospec=True) @mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True) @@ -1276,3 +1330,7 @@ class TestRefreshCleanSteps(TestBaseAgentVendor): task) client_mock.assert_called_once_with(mock.ANY, task.node, task.ports) + + def test_get_properties(self): + expected = agent_base_vendor.VENDOR_PROPERTIES + self.assertEqual(expected, self.passthru.get_properties()) diff --git a/ironic/tests/unit/drivers/modules/test_agent_client.py b/ironic/tests/unit/drivers/modules/test_agent_client.py index fe5be2dba..aead8915a 100644 --- a/ironic/tests/unit/drivers/modules/test_agent_client.py +++ b/ironic/tests/unit/drivers/modules/test_agent_client.py @@ -236,3 +236,9 @@ class TestAgentClient(base.TestCase): self.client.power_off(self.node) self.client._command.assert_called_once_with( node=self.node, method='standby.power_off', params={}) + + def test_sync(self): + self.client._command = mock.MagicMock(spec_set=[]) + self.client.sync(self.node) + self.client._command.assert_called_once_with( + node=self.node, method='standby.sync', params={}, wait=True) diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index 8a916b11a..8362c07bb 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -1252,7 +1252,7 @@ class OtherFunctionTestCase(db_base.DbTestCase): autospec=True) def _test_set_failed_state(self, mock_event, mock_power, mock_log, event_value=None, power_value=None, - log_calls=None): + log_calls=None, poweroff=True): err_msg = 'some failure' mock_event.side_effect = event_value mock_power.side_effect = power_value @@ -1260,9 +1260,12 @@ class OtherFunctionTestCase(db_base.DbTestCase): shared=False) as task: utils.set_failed_state(task, err_msg) mock_event.assert_called_once_with(task, 'fail') - mock_power.assert_called_once_with(task, states.POWER_OFF) + if poweroff: + mock_power.assert_called_once_with(task, states.POWER_OFF) + else: + self.assertFalse(mock_power.called) self.assertEqual(err_msg, task.node.last_error) - if log_calls: + if (log_calls and poweroff): mock_log.exception.assert_has_calls(log_calls) else: self.assertFalse(mock_log.called) @@ -1283,6 +1286,23 @@ class OtherFunctionTestCase(db_base.DbTestCase): power_value=iter([exc_param] * len(calls)), log_calls=calls) + def test_set_failed_state_no_poweroff(self): + cfg.CONF.deploy.power_off_after_deploy_failure = False + exc_state = exception.InvalidState('invalid state') + exc_param = exception.InvalidParameterValue('invalid parameter') + mock_call = mock.call(mock.ANY) + self._test_set_failed_state(poweroff=False) + calls = [mock_call] + self._test_set_failed_state(event_value=iter([exc_state] * len(calls)), + log_calls=calls, poweroff=False) + calls = [mock_call] + self._test_set_failed_state(power_value=iter([exc_param] * len(calls)), + log_calls=calls, poweroff=False) + calls = [mock_call, mock_call] + self._test_set_failed_state(event_value=iter([exc_state] * len(calls)), + power_value=iter([exc_param] * len(calls)), + log_calls=calls, poweroff=False) + def test_get_boot_option(self): self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} result = utils.get_boot_option(self.node) diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index 745626240..9e023ea18 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -32,6 +32,7 @@ from ironic.common.glance_service import base_image_service from ironic.common import pxe_utils from ironic.common import states from ironic.conductor import task_manager +from ironic.drivers.modules import agent_base_vendor from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import pxe from ironic.tests.unit.conductor import mgr_utils @@ -549,6 +550,7 @@ class PXEBootTestCase(db_base.DbTestCase): def test_get_properties(self): expected = pxe.COMMON_PROPERTIES + expected.update(agent_base_vendor.VENDOR_PROPERTIES) with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: self.assertEqual(expected, task.driver.get_properties()) diff --git a/releasenotes/notes/add-support-for-no-poweroff-on-failure-86e43b3e39043990.yaml b/releasenotes/notes/add-support-for-no-poweroff-on-failure-86e43b3e39043990.yaml new file mode 100644 index 000000000..4808ad400 --- /dev/null +++ b/releasenotes/notes/add-support-for-no-poweroff-on-failure-86e43b3e39043990.yaml @@ -0,0 +1,12 @@ +--- +features: + - Operators can now set + deploy.power_off_after_deploy_failure to leave nodes + powered on when a deployment fails. This is useful + for troubleshooting deployment issues. As a note, + Nova will still attempt to delete a node after a failed + deployment, so deploy.power_off_after_deploy_failure + may not be very effective in non-standalone + deployments until a similar patch to ironic's driver in + nova is proposed. + diff --git a/releasenotes/notes/disk-label-fix-7580de913835ff44.yaml b/releasenotes/notes/disk-label-fix-7580de913835ff44.yaml new file mode 100644 index 000000000..b7a8d5314 --- /dev/null +++ b/releasenotes/notes/disk-label-fix-7580de913835ff44.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fixes the bug where the user specified + disk_label is ignored for the agent + drivers for partition images. diff --git a/releasenotes/notes/duplicated-driver-entry-775370ad84736206.yaml b/releasenotes/notes/duplicated-driver-entry-775370ad84736206.yaml new file mode 100644 index 000000000..bb7be7e55 --- /dev/null +++ b/releasenotes/notes/duplicated-driver-entry-775370ad84736206.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fixes a problem which causes the conductor to error out on startup + in case there's a duplicated entry in the enabled_drivers configuration + option. diff --git a/releasenotes/notes/oob-power-off-7bbdf5947ed24bf8.yaml b/releasenotes/notes/oob-power-off-7bbdf5947ed24bf8.yaml new file mode 100644 index 000000000..ddbbf152a --- /dev/null +++ b/releasenotes/notes/oob-power-off-7bbdf5947ed24bf8.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - Fixes a problem where some hardware/firmware (specially faulty ones) + won't come back online after an in-band ACPI soft power off by adding + a new driver property called "deploy_forces_oob_reboot" that can be set + to the nodes being deployed by the IPA ramdisk. If the value of this + property is True, Ironic will power cycle the node via out-of-band. diff --git a/releasenotes/notes/opentack-baremetal-request-id-daa72b785eaaaa8d.yaml b/releasenotes/notes/opentack-baremetal-request-id-daa72b785eaaaa8d.yaml new file mode 100644 index 000000000..fcc50f077 --- /dev/null +++ b/releasenotes/notes/opentack-baremetal-request-id-daa72b785eaaaa8d.yaml @@ -0,0 +1,4 @@ +--- +features: + - Append request_id as ``Openstack-Request-Id`` header to the response. + diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 02f2a659e..09be841aa 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -5,8 +5,9 @@ .. toctree:: :maxdepth: 1 - Current (4.3.0 - unreleased) <current-series> - Mitaka (4.3.0 - unreleased) <mitaka> + Current (unreleased) <current-series> + Newton (unreleased) <newton> + Mitaka (4.3.0 - 5.1.x) <mitaka> Liberty (4.0.0 - 4.2.x) <liberty> Kilo (2015.1) <https://wiki.openstack.org/wiki/Ironic/ReleaseNotes/Kilo> Juno (2014.2) <https://wiki.openstack.org/wiki/Ironic/ReleaseNotes/Juno> diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst index b66086b8b..05ed3e48e 100644 --- a/releasenotes/source/mitaka.rst +++ b/releasenotes/source/mitaka.rst @@ -1,6 +1,6 @@ -============================ +=========================== Mitaka Series Release Notes -============================ +=========================== .. release-notes:: - :branch: origin/master + :branch: origin/stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..55006ebac --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +=========================== +Newton Series Release Notes +=========================== + +.. release-notes:: + :branch: origin/master |