From be2fb9459b65bb8b07b23adebadc16530d26e231 Mon Sep 17 00:00:00 2001 From: Janne Suominen Date: Wed, 8 May 2019 15:24:30 +0300 Subject: [PATCH] Seed code for ironic_virtmedia_driver Seed code for ironic_virtmedia_driver Change-Id: Ibb72095b1214900867513b35bf0392049161f5e3 Signed-off-by: Janne Suominen --- LICENSE | 202 ++++++++++ README.rst | 135 +++++++ ironic-virtmedia-driver.spec | 58 +++ src/ironic_virtmedia_driver/__init__.py | 15 + src/ironic_virtmedia_driver/conf/__init__.py | 22 + src/ironic_virtmedia_driver/conf/virtmedia.py | 28 ++ src/ironic_virtmedia_driver/ipmi_virtmedia.py | 24 ++ src/ironic_virtmedia_driver/ssh_virtmedia.py | 23 ++ src/ironic_virtmedia_driver/vendors/__init__.py | 15 + .../vendors/dell/__init__.py | 0 src/ironic_virtmedia_driver/vendors/dell/dell.py | 161 ++++++++ src/ironic_virtmedia_driver/vendors/hp/__init__.py | 15 + src/ironic_virtmedia_driver/vendors/hp/hp.py | 149 +++++++ .../vendors/ironic_virtmedia_hw.py | 46 +++ .../vendors/nokia/__init__.py | 15 + src/ironic_virtmedia_driver/vendors/nokia/hw17.py | 70 ++++ .../vendors/nokia/nokia_hw.py | 126 ++++++ src/ironic_virtmedia_driver/vendors/nokia/oe19.py | 31 ++ src/ironic_virtmedia_driver/vendors/nokia/or18.py | 419 +++++++++++++++++++ src/ironic_virtmedia_driver/vendors/nokia/rm18.py | 419 +++++++++++++++++++ src/ironic_virtmedia_driver/virtmedia.py | 447 +++++++++++++++++++++ src/ironic_virtmedia_driver/virtmedia_exception.py | 20 + src/ironic_virtmedia_driver/virtmedia_ipmi_boot.py | 144 +++++++ src/ironic_virtmedia_driver/virtmedia_ssh_boot.py | 272 +++++++++++++ src/setup.py | 44 ++ tox.ini | 22 + 26 files changed, 2922 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 ironic-virtmedia-driver.spec create mode 100644 src/ironic_virtmedia_driver/__init__.py create mode 100644 src/ironic_virtmedia_driver/conf/__init__.py create mode 100644 src/ironic_virtmedia_driver/conf/virtmedia.py create mode 100644 src/ironic_virtmedia_driver/ipmi_virtmedia.py create mode 100644 src/ironic_virtmedia_driver/ssh_virtmedia.py create mode 100644 src/ironic_virtmedia_driver/vendors/__init__.py create mode 100644 src/ironic_virtmedia_driver/vendors/dell/__init__.py create mode 100644 src/ironic_virtmedia_driver/vendors/dell/dell.py create mode 100644 src/ironic_virtmedia_driver/vendors/hp/__init__.py create mode 100644 src/ironic_virtmedia_driver/vendors/hp/hp.py create mode 100644 src/ironic_virtmedia_driver/vendors/ironic_virtmedia_hw.py create mode 100644 src/ironic_virtmedia_driver/vendors/nokia/__init__.py create mode 100644 src/ironic_virtmedia_driver/vendors/nokia/hw17.py create mode 100644 src/ironic_virtmedia_driver/vendors/nokia/nokia_hw.py create mode 100644 src/ironic_virtmedia_driver/vendors/nokia/oe19.py create mode 100644 src/ironic_virtmedia_driver/vendors/nokia/or18.py create mode 100644 src/ironic_virtmedia_driver/vendors/nokia/rm18.py create mode 100644 src/ironic_virtmedia_driver/virtmedia.py create mode 100644 src/ironic_virtmedia_driver/virtmedia_exception.py create mode 100644 src/ironic_virtmedia_driver/virtmedia_ipmi_boot.py create mode 100644 src/ironic_virtmedia_driver/virtmedia_ssh_boot.py create mode 100644 src/setup.py create mode 100644 tox.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..1f41156 --- /dev/null +++ b/README.rst @@ -0,0 +1,135 @@ +:: + + Copyright 2019 Nokia + + Licensed under the Apache License, Version 2.0 (the "License"); + + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +============================================================= +Ironic Drivers for Virtual media based baremetal provisioning +============================================================= + +:Author: + Chandra Shekar Rangavajjula (chandra.s.rangavajjula@nokia.com) + Janne Suominen (janne.suominen@nokia.com) + +:Version: 1.0 2019.05 +:Copyright: 2019 Nokia. All rights reserved. + +Introduction +============ +This project contains Ironic drivers for baremetal provisioning using Virtual media for Quanta Hardware and Virtual environment. The main motivation for writing own drivers is to avoid L2 Network dependency and to support L3 based deployment. + +These drivers are implimented inline with new specification *"hardware types"* + +*Ref*: "https://docs.openstack.org/ironic/latest/install/enabling-drivers.html" + +Effort was to reuse existing hardware interfaces to the maximum, and impliment only those which specific/different in case of Quanta hardware. +Below is a breif listing/description on needed interfaces. + +1. **Management**:- Boot order settings etc. We use existing hardware interfaces **ipmi/ssh**. + + enabled_management_interfaces = ipmitool, ssh + + enabled_hardware_types = ipmi_virtmedia, ssh_virtmedia + +2. **Power**:- For power managment. We use existing hardware interfaces **ipmi/ssh**. + + enabled_power_interfaces = ipmitool, ssh + + enabled_hardware_types = ipmi_virtmedia, ssh_virtmedia + +3. **Deploy**:- Defines how the image gets transferred to the target disk. We use existing hardware interface **Agent/direct** mode to deploy. This reduces load on ironic coductor and scales better with larger number of nodes parallely provisioning. + + enabled_deploy_interfaces = direct + + enabled_hardware_types = ipmi_virtmedia, ssh_virtmedia + +4. **Console**:- Manages access to the console of a baremetal target node. We use existing interface **ipmitool-shellinabox/ssh-shellinabox**. At the moment only ipmitool-shellinabox is confiured and used on real environments. This redirects the console to a webpage. + + enabled_console_interfaces = ipmitool-shellinabox, ssh-shellinabox + + enabled_hardware_types = ipmi_virtmedia, ssh_virtmedia + + *Ref*: "https://docs.openstack.org/ironic/latest/admin/console.html" + +5. **Boot**:- Manages booting of the deploy ramdisk on the baremetal node. We have in house developed hardware interfaces **virtmedia_ipmi_boot/virtmedia_ssh_boot** for booting baremetal nodes for deployment. This expects an iso containing ironic-pyton-agent Ramdisk and a kernel. + + *Ref*: "https://gerrit.akraino.org/r/#/admin/projects/ta/ipa-deployer/tree/master" to check ironic-deploy.iso creation procedure. This driver creates a floppy image per baremetal node with config-data (IP, Interface, GW etc,.). Quanta hardware does not support attaching 2 Virtual media devices over NFS using IPMI. Hence this floppy image is appended to the end of node iso to make it a single iso image. The consolidated image is then attached to the target. When the target is booted from ISO it first configures its IP using "virtmedia-netconfig.service" + +Below is the example output of the driver info: + +# ironic driver-list + ++---------------------+----------------+ +| Supported driver(s) | Active host(s) | ++---------------------+----------------+ +| ipmi_virtmedia | controller-1 | ++---------------------+----------------+ +| ssh_virtmedia | controller-1 | ++---------------------+----------------+ + +# ironic driver-properties ipmi_virtmedia + ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| Property | Description | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| deploy_forces_oob_reboot | Whether Ironic should force a reboot of the Node via the out-of-band channel after deployment is complete.| +| | Provides compatibility with older deploy ramdisks. Defaults to False. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| deploy_kernel | UUID (from Glance) of the deployment kernel. Required. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| deploy_ramdisk | UUID (from Glance) of the ramdisk with agent that is used at deploy time. Required. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| image_http_proxy | URL of a proxy server for HTTP connections. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| image_https_proxy | URL of a proxy server for HTTPS connections. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| image_no_proxy | A comma-separated list of host names, IP addresses and domain names (with optional :port) that will be | +| | excluded from proxying. To denote a doman name, use a dot to prefix the domain name. This value will be | +| | ignored if ``image_http_proxy`` and ``image_https_proxy`` are not specified. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| **ipmi_address** | **IP address or hostname of the node. Required.** | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_bridging | bridging_type; default is "no". One of "single", "dual", "no". Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_force_boot_device | Whether Ironic should specify the boot device to the BMC each time the server is turned on, eg. because | +| | the BMC is not capable of remembering the selected boot device across power cycles; default value is False| +| | Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_local_address | local IPMB address for bridged requests. Used only if ipmi_bridging is set to "single" or "dual". Optional| ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| **ipmi_password** | **password. Optional.** | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_port | remote IPMI RMCP port. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_priv_level | privilege level; default is ADMINISTRATOR. One of ADMINISTRATOR, CALLBACK, OPERATOR, USER. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_protocol_version | the version of the IPMI protocol; default is "2.0". One of "1.5", "2.0". Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_target_address | destination address for bridged request. Required only if ipmi_bridging is set to "single" or "dual". | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_target_channel | destination channel for bridged request. Required only if ipmi_bridging is set to "single" or "dual". | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| **ipmi_terminal_port** | **node's UDP port to connect to. Only required for console access.** | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_transit_address | transit address for bridged request. Required only if ipmi_bridging is set to "dual". | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_transit_channel | transit channel for bridged request. Required only if ipmi_bridging is set to "dual". | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| ipmi_username | username; default is NULL user. Optional. | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| **virtmedia_deploy_iso** | **Deployment ISO image file name. Required.** | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ +| **nfs_server** | **NFS server IP hosting deployment ISO and metadata Floppy images. Required.** | ++--------------------------+-----------------------------------------------------------------------------------------------------------+ diff --git a/ironic-virtmedia-driver.spec b/ironic-virtmedia-driver.spec new file mode 100644 index 0000000..cd0f243 --- /dev/null +++ b/ironic-virtmedia-driver.spec @@ -0,0 +1,58 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +Name: ironic-virtmedia-driver +Version: %{_version} +Release: 1%{?dist} +Summary: Contains ironic drivers for virtualmedia based deployment +License: %{_platform_licence} +Source0: %{name}-%{version}.tar.gz +Vendor: %{_platform_vendor} +BuildArch: noarch + +Requires: python-cliff python-pip openstack-ironic-common +BuildRequires: python +BuildRequires: python-setuptools + + +%description +This RPM contains ironic drivers for virtualmedia based deployment + +%prep +%autosetup + +%build + +%install +cd src && python setup.py install --root %{buildroot} --no-compile --install-purelib %{_python_site_packages_path} && cd - + + +%files +%{_python_site_packages_path}/ironic_virtmedia_driver* + +%pre + +%post + + +%preun + +%postun + +%clean +rm -rf %{buildroot} + +# TIPS: +# File /usr/lib/rpm/macros contains useful variables which can be used for example to define target directory for man page. diff --git a/src/ironic_virtmedia_driver/__init__.py b/src/ironic_virtmedia_driver/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/ironic_virtmedia_driver/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/src/ironic_virtmedia_driver/conf/__init__.py b/src/ironic_virtmedia_driver/conf/__init__.py new file mode 100644 index 0000000..5833b13 --- /dev/null +++ b/src/ironic_virtmedia_driver/conf/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from oslo_config import cfg + +from ironic_virtmedia_driver.conf import virtmedia + +CONF = cfg.CONF + +virtmedia.register_opts(CONF) diff --git a/src/ironic_virtmedia_driver/conf/virtmedia.py b/src/ironic_virtmedia_driver/conf/virtmedia.py new file mode 100644 index 0000000..7c47d37 --- /dev/null +++ b/src/ironic_virtmedia_driver/conf/virtmedia.py @@ -0,0 +1,28 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from oslo_config import cfg + +from ironic.common.i18n import _ + +opts = [ + cfg.StrOpt('remote_image_share_root', + default='/remote_image_share_root', + help=_('Ironic conductor node\'s "NFS" root path')), +] + + +def register_opts(conf): + conf.register_opts(opts) diff --git a/src/ironic_virtmedia_driver/ipmi_virtmedia.py b/src/ironic_virtmedia_driver/ipmi_virtmedia.py new file mode 100644 index 0000000..b952ca5 --- /dev/null +++ b/src/ironic_virtmedia_driver/ipmi_virtmedia.py @@ -0,0 +1,24 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ironic.drivers import ipmi +from ironic_virtmedia_driver import virtmedia_ipmi_boot + + +class IPMIVirtmediaHardware(ipmi.IPMIHardware): + @property + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + return [virtmedia_ipmi_boot.VirtualMediaAndIpmiBoot] diff --git a/src/ironic_virtmedia_driver/ssh_virtmedia.py b/src/ironic_virtmedia_driver/ssh_virtmedia.py new file mode 100644 index 0000000..7a14b35 --- /dev/null +++ b/src/ironic_virtmedia_driver/ssh_virtmedia.py @@ -0,0 +1,23 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ironic.drivers import ipmi +from ironic_virtmedia_driver import virtmedia_ssh_boot + +class SSHVirtmediaHardware(ipmi.IPMIHardware): + @property + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + return [virtmedia_ssh_boot.VirtualMediaAndSSHBoot] diff --git a/src/ironic_virtmedia_driver/vendors/__init__.py b/src/ironic_virtmedia_driver/vendors/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/src/ironic_virtmedia_driver/vendors/dell/__init__.py b/src/ironic_virtmedia_driver/vendors/dell/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ironic_virtmedia_driver/vendors/dell/dell.py b/src/ironic_virtmedia_driver/vendors/dell/dell.py new file mode 100644 index 0000000..e4fb255 --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/dell/dell.py @@ -0,0 +1,161 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ironic.common import boot_devices +from ironic.common.i18n import _ +from ironic.conductor import utils as manager_utils +from ironic.drivers.modules import ipmitool +from ironic_virtmedia_driver.vendors.ironic_virtmedia_hw import IronicVirtMediaHW +from ironic_virtmedia_driver import virtmedia_exception + +from redfish import redfish_client, AuthMethod +from redfish.rest.v1 import ServerDownOrUnreachableError + +class DELL(IronicVirtMediaHW): + def __init__(self, log): + super(DELL, self).__init__(log) + self.remote_share = '/bootimages/' + self.idrac_location = '/redfish/v1/Managers/iDRAC.Embedded.1/' + + def _init_connection(self, driver_info): + """Get connection info and init rest_object""" + host = 'https://' + driver_info['address'] + user = driver_info['username'] + password = driver_info['password'] + redfishclient = None + self.log.debug("Init connection: user: %s, passwd: %s, host: %s", user, password, host) + try: + redfishclient = redfish_client(base_url=host, \ + username=user, password=password) + redfishclient.login(auth=AuthMethod.SESSION) + except ServerDownOrUnreachableError as error: + operation = _("iDRAC not responding") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=error) + except Exception as error: + operation = _("Failed to login to iDRAC") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=error) + return redfishclient + + @staticmethod + def _check_success(response): + if response.status >= 200 and response.status < 300: + return True + else: + try: + _ = response.dict + raise virtmedia_exception.VirtmediaOperationError("Response status is %d, %s"% (response.status, response.dict["error"]["@Message.ExtendedInfo"][0]["MessageId"].split("."))) + except Exception: + raise virtmedia_exception.VirtmediaOperationError("Response status is not 200, %s"% response) + + def _check_supported_idrac_version(self, connection): + response = connection.get('%s/VirtualMedia/CD'%self.idrac_location) + self._check_success(response) + data = response.dict + for i in data.get('Actions', []): + if i == "#VirtualMedia.InsertMedia" or i == "#VirtualMedia.EjectMedia": + return True + raise virtmedia_exception.VirtmediaOperationError("Unsupported version of iDRAC, please update before continuing") + + def _get_virtual_media_devices(self, connection): + idr = connection.get("%s" % self.idrac_location) + self._check_success(idr) + try: + virtual_media = connection.get(idr.dict["VirtualMedia"]["@odata.id"]) + self._check_success(virtual_media) + except KeyError: + self.log.error("Cannot find a single virtual media device") + raise virtmedia_exception.VirtmediaOperationError("Cannot find any virtual media device on the server") + return virtual_media.dict["Members"] + + def _umount_virtual_device(self, connection, media_uri): + self.log.debug("Unmount") + unmount_location = media_uri + "/Actions/VirtualMedia.EjectMedia" + resp = connection.post(unmount_location, body={}) + self._check_success(resp) + + def _mount_virtual_device(self, connection, media_uri, image_location): + self.log.debug("Mount") + mount_location = media_uri + "/Actions/VirtualMedia.InsertMedia" + payload = {'Image': image_location, 'Inserted':True, 'WriteProtected':True} + resp = connection.post(mount_location, body=payload) + self._check_success(resp) + + def _unmount_all(self, connection): + medias = self._get_virtual_media_devices(connection) + for media in medias: + uri = media.get("@odata.id", None) + if not uri or connection.get(uri).dict["ConnectedVia"] == "NotConnected": + continue + self._umount_virtual_device(connection, uri) + + def _find_first_media(self, connection, typeinfo): + medias = self._get_virtual_media_devices(connection) + for media in medias: + response = connection.get(media["@odata.id"]) + if typeinfo in response.dict["MediaTypes"]: + return media["@odata.id"] + return None + + def _mount_virtual_cd(self, connection, image_location): + self._unmount_all(connection) + self.log.debug("Mount") + media_uri = self._find_first_media(connection, "DVD") + self._mount_virtual_device(connection, media_uri, image_location) + + def attach_virtual_cd(self, image_filename, driver_info, task): + connection = None + try: + self.log.debug("attach_virtual_cd") + connection = self._init_connection(driver_info) + self._check_supported_idrac_version(connection) + image_location = 'http://' + driver_info['provisioning_server'] + ':' + driver_info['provisioning_server_http_port'] + self.remote_share + image_filename + self._mount_virtual_cd(connection, image_location) + + connection.logout() + return True + except Exception: + if connection: + connection.logout() + raise + + def detach_virtual_cd(self, driver_info, task): + connection = None + try: + self.log.debug("detach_virtual_cd") + connection = self._init_connection(driver_info) + self._check_supported_idrac_version(connection) + self._unmount_all(connection) + connection.logout() + return True + except Exception: + if connection: + connection.logout() + raise + + def set_boot_device(self, task): + try: + #BMC boot flag valid bit clearing 1f -> all bit set + #P 420 of ipmi spec + # https://www.intel.com/content/www/us/en/servers/ipmi/ipmi-second-gen-interface-spec-v2-rev1-1.html + cmd = '0x00 0x08 0x03 0x1f' + ipmitool.send_raw(task, cmd) + self.log.info('Disable timeout for booting') + except Exception as err: + self.log.warning('Failed to disable booting options: %s', str(err)) + #For time being lets do the boot order with ipmitool since, well dell doesn't provide open support + #for this. + manager_utils.node_set_boot_device(task, boot_devices.CDROM, persistent=False) diff --git a/src/ironic_virtmedia_driver/vendors/hp/__init__.py b/src/ironic_virtmedia_driver/vendors/hp/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/hp/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/src/ironic_virtmedia_driver/vendors/hp/hp.py b/src/ironic_virtmedia_driver/vendors/hp/hp.py new file mode 100644 index 0000000..a1ff242 --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/hp/hp.py @@ -0,0 +1,149 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json + +from ironic.common.i18n import _ + +from ironic_virtmedia_driver.vendors.ironic_virtmedia_hw import IronicVirtMediaHW +from ironic_virtmedia_driver import virtmedia_exception + +import redfish.ris.tpdefs +from redfish import AuthMethod, redfish_client +from redfish.rest.v1 import ServerDownOrUnreachableError + +class HP(IronicVirtMediaHW): + def __init__(self, log): + super(HP, self).__init__(log) + self.remote_share = '/bootimages/' + self.typepath = None + + def _init_connection(self, driver_info): + """Get connection info and init rest_object""" + host = 'https://' + driver_info['address'] + user = driver_info['username'] + password = driver_info['password'] + redfishclient = None + try: + redfishclient = redfish_client(base_url=host, \ + username=user, password=password, \ + default_prefix="/redfish/v1") + redfishclient.login(auth=AuthMethod.SESSION) + self._init_typepath(redfishclient) + except ServerDownOrUnreachableError as error: + operation = _("iLO not responding") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=error) + except Exception as error: + operation = _("Failed to login to iLO") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=error) + return redfishclient + + def _init_typepath(self, connection): + typepath = redfish.ris.tpdefs.Typesandpathdefines() + typepath.getgen(url=connection.get_base_url()) + typepath.defs.redfishchange() + self.typepath = typepath + + def _search_for_type(self, typename, resources): + instances = [] + nosettings = [item for item in resources["resources"] if "/settings/" not in item["@odata.id"]] + for item in nosettings: + if "@odata.type" in item and \ + typename.lower() in item["@odata.type"].lower(): + instances.append(item) + return instances + + def _get_instances(self, connection): + resources = {} + + response = connection.get("/redfish/v1/resourcedirectory/") + if response.status == 200: + resources["resources"] = response.dict["Instances"] + else: + return [] + + return self._search_for_type("Manager.", resources) + + def _get_error(self, response): + message = json.loads(response.text) + error = message["error"]["@Message.ExtendedInfo"][0]["MessageId"].split(".") + return error + + + def _umount_virtual_cd(self, connection, cd_location): + unmount_path = cd_location + unmount_body = {"Action": "EjectVirtualMedia", "Target": self.typepath.defs.oempath} + resp = connection.post(path=unmount_path, body=unmount_body) + if resp.status != 200: + self.log.error("Unmounting cd failed: %r, cd location: %r", resp, unmount_path) + operation = _("Failed to unmount image") + error = self._get_error(resp) + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=error) + + + def _get_virtual_media_devices(self, connection, instance): + rsp = connection.get(instance["@odata.id"]) + rsp = connection.get(rsp.dict["VirtualMedia"]["@odata.id"]) + return rsp.dict['Members'] + + def _mount_virtual_cd(self, connection, image_location): + instances = self._get_instances(connection) + for instance in instances: + for vmlink in self._get_virtual_media_devices(connection, instance): + response = connection.get(vmlink["@odata.id"]) + + if response.status == 200 and "DVD" in response.dict["MediaTypes"]: + if response.dict['Inserted']: + self._umount_virtual_cd(connection, vmlink["@odata.id"]) + + body = {"Image": image_location} + + if image_location: + body["Oem"] = {self.typepath.defs.oemhp: {"BootOnNextServerReset": \ + True}} + + response = connection.patch(path=vmlink["@odata.id"], body=body) + elif response.status != 200: + self.log.error("Failed to mount image") + error = self._get_error(response) + operation = _("Failed to mount image") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=error) + + def attach_virtual_cd(self, image_filename, driver_info, task): + connection = self._init_connection(driver_info) + image_location = 'http://' + driver_info['provisioning_server'] + ':' + driver_info['provisioning_server_http_port'] + self.remote_share + image_filename + self._mount_virtual_cd(connection, image_location) + connection.logout() + return True + + def detach_virtual_cd(self, driver_info, task): + connection = self._init_connection(driver_info) + instances = self._get_instances(connection) + for instance in instances: + for vmlink in self._get_virtual_media_devices(connection, instance): + response = connection.get(vmlink["@odata.id"]) + if response.status == 200 and "DVD" in response.dict["MediaTypes"]: + if response.dict['Inserted']: + self._umount_virtual_cd(connection, vmlink["@odata.id"]) + connection.logout() + return True + + def set_boot_device(self, task): + """ This is done during the mounting""" + pass diff --git a/src/ironic_virtmedia_driver/vendors/ironic_virtmedia_hw.py b/src/ironic_virtmedia_driver/vendors/ironic_virtmedia_hw.py new file mode 100644 index 0000000..4dd75ab --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/ironic_virtmedia_hw.py @@ -0,0 +1,46 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class IronicVirtMediaHW(object): + def __init__(self, log): + self.log = log + + def attach_virtual_cd(self, image_filename, driver_info, task): + """Attaches the given image as virtual media on the node. + + :param image_filename: the filename of the image to be attached. + :param driver_info: the information about the node that the media + is being attached to. (provisioning_server, ipmi params etc.) + :param task: a TaskManager instance. + :raises: VirtmediaOperationError if attaching virtual media failed. + """ + + raise NotImplementedError + + def detach_virtual_cd(self, driver_info, task): + """Detach virtual cd/dvd from a node + + :param task: a TaskManager instance. + :raises: VirtmediaOperationError if attaching virtual media failed. + """ + raise NotImplementedError + + def set_boot_device(self, task): + """Set virtual boot device from a node + + :param task: a TaskManager instance. + :raises: VirtmediaOperationError if attaching virtual media failed. + """ + raise NotImplementedError diff --git a/src/ironic_virtmedia_driver/vendors/nokia/__init__.py b/src/ironic_virtmedia_driver/vendors/nokia/__init__.py new file mode 100644 index 0000000..f035b4a --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/nokia/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/src/ironic_virtmedia_driver/vendors/nokia/hw17.py b/src/ironic_virtmedia_driver/vendors/nokia/hw17.py new file mode 100644 index 0000000..278cea1 --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/nokia/hw17.py @@ -0,0 +1,70 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time + +from ironic.drivers.modules import ipmitool +from ironic.conductor import utils as manager_utils +from ironic.common import exception +from ironic.common import boot_devices + +from .nokia_hw import NokiaIronicVirtMediaHW + +class HW17(NokiaIronicVirtMediaHW): + def __init__(self, log): + super(HW17, self).__init__(log) + + def get_disk_attachment_status(self, task): + # Check NFS Service Status + (out, err) = ipmitool.send_raw(task, '0x3c 0x03') + self.log.debug("get_disk_attachment_status: NFS service status: error:%r, output:%r" %(err, out)) + if out == ' 00\n': + return 'mounted' + elif out == ' 64\n': + return 'mounting' + elif out == ' 20\n': + return 'nfserror' + else: + return 'dismounted' + + def attach_virtual_cd(self, image_filename, driver_info, task): + + # Stop virtual device and Clear NFS configuration + ipmitool.send_raw(task, '0x3c 0x0') + # Set NFS Configurations + # NFS server IP + ipmitool.send_raw(task, '0x3c 0x01 0x00 %s 0x00' %(self.hex_convert(driver_info['provisioning_server']))) + # Set NFS Mount Root path + ipmitool.send_raw(task, '0x3c 0x01 0x01 %s 0x00' %(self.hex_convert(self.remote_share))) + # Set Image Name + ipmitool.send_raw(task, '0x3c 0x01 0x02 %s 0x00' %(self.hex_convert(image_filename))) + # Start NFS Service + ipmitool.send_raw(task, '0x3c 0x02 0x01') + + time.sleep(1) + + return self.check_and_wait_for_cd_mounting(image_filename, task, driver_info) + + def detach_virtual_cd(self, driver_info, task): + """Detaches virtual cdrom on the node. + + :param task: an ironic task object. + """ + # Stop virtual device and Clear NFS configuration + self.log.debug("detach_virtual_cd") + ipmitool.send_raw(task, '0x3c 0x00') + + def set_boot_device(self, task): + manager_utils.node_set_boot_device(task, boot_devices.FLOPPY, persistent=True) diff --git a/src/ironic_virtmedia_driver/vendors/nokia/nokia_hw.py b/src/ironic_virtmedia_driver/vendors/nokia/nokia_hw.py new file mode 100644 index 0000000..9596804 --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/nokia/nokia_hw.py @@ -0,0 +1,126 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time + +from ironic.drivers.modules import ipmitool +from ironic.common.i18n import _translators +from oslo_concurrency import processutils +from ironic.common import exception + +from ..ironic_virtmedia_hw import IronicVirtMediaHW + +class NokiaIronicVirtMediaHW(IronicVirtMediaHW): + def __init__(self, log): + super(NokiaIronicVirtMediaHW, self).__init__(log) + self.remote_share = '/remote_image_share_root/' + + def attach_virtual_cd(self, image_filename, driver_info, task): + """ see ironic_virtmedia_hw.py""" + raise NotImplementedError + + def detach_virtual_cd(self, driver_info, task): + """ see ironic_virtmedia_hw.py""" + raise NotImplementedError + + def set_boot_device(self, task): + """ see ironic_virtmedia_hw.py""" + raise NotImplementedError + + def get_disk_attachment_status(self, task): + """ Get the disk attachment status. + :param task: a TaskManager instance. + :returns: : 'mounting' if operation is ongoing + 'nfserror' if failed + 'mounted' if the disk is successfully mounted + """ + raise NotImplementedError + + @staticmethod + def hex_convert(string_value, padding=False, length=0): + hex_value = '0x' + hex_value += ' 0x'.join(x.encode('hex') for x in string_value) + if padding and (len(string_value) 0: + self.log.debug('Virtual %s count is %d expecting %d' % (devicetype, _conf_device_num, devicecount)) + time.sleep(5) + _conf_device_num = self._get_virtual_media_device_count(task, devicetype) + _tries = _tries -1 + + except Exception as err: + self.log.warning('Exception when setting virtual media device count, error: %s' % str(err)) + return False + return True + + def _check_virtual_media_started(self, task): + service_status = None + # check virtmedia service status + try: + cmd = '0x32 0xca 0x08' + out, err = ipmitool.send_raw(task, cmd) + service_status = out.strip() + self.log.warning('Virtual media service status: %s' % str(service_status)) + except Exception as err: + self.log.warning('Exception when checking virtual media service: %s' % str(err)) + if service_status == '01': + return True + return False + + def _start_virtual_media(self, task): + # Enable "Remote Media Support" in GUI (p145) + try: + cmd = '0x32 0xcb 0x08 0x01' + self.log.debug('Start virtual media service') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when starting virtual media service: %s' % str(err)) + + def _restart_virtual_media_service(self, task): + try: + cmd = '0x32 0xcb 0x0a 0x01' + self.log.debug('Restart virtual media service') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when restarting virtual media service: %s' % str(err)) + + def _restart_ris(self, task): + try: + self.log.debug('Restart RIS') + cmd = '0x32 0x9f 0x08 0x0b' + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when restarting RIS: %s' % str(err)) + return False + return True + + def _restart_ris_cd(self, task): + try: + self.log.debug('Restart RIS CD media') + cmd = '0x32 0x9f 0x01 0x0b 0x01' + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when restarting RIS CD media: %s' % str(err)) + return False + return True + + def _enable_virtual_media(self, task): + # Speed up things if it service is already running + if self._check_virtual_media_started(task): + self.log.debug('Virtual media service already running.') + # Service is already started + return True + + _max_tries = 6 + _try = 1 + self._start_virtual_media(task) + # Just enabling the service doe not seem to start it (in all HW) + # Resetting it after enabling helps + self._restart_virtual_media_service(task) + while (not self._check_virtual_media_started(task)): + if _try > _max_tries: + self.log.warning('Ensure virtual media service start failed, attempts exceeded.') + return False + time.sleep(5) + _try = _try + 1 + return True + + def _set_nfs_server_ip(self, driver_info, task): + try: + cmd = '0x32 0x9f 0x01 0x02 0x00 %s' % (self.hex_convert(driver_info['provisioning_server'], True, 63)) + self.log.debug('Virtual media server "%s"' % driver_info['provisioning_server']) + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when setting virtual media server: %s' % str(err)) + raise err + + def _set_share_type(self, task): + try: + cmd = '0x32 0x9f 0x01 0x05 0x00 0x6e 0x66 0x73 0x00 0x00 0x00' + self.log.debug('Virtual media share type to NFS.') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when setting virtual media service type NFS: %s' % str(err)) + raise err + + def _set_nfs_root_path(self, driver_info, task): + try: + self.log.debug('Virtual media path to "%s"' % self.remote_share) + # set progress bit (hmm. seems to return error if it is already set.. So should check..) + # Welp there is no way checking this. As workaround clearing it first ( does not seem to + # return error even if alreay cleared). + # clear progress bit + cmd = '0x32 0x9f 0x01 0x01 0x00 0x00' + ipmitool.send_raw(task, cmd) + + # set progress bit + cmd = '0x32 0x9f 0x01 0x01 0x00 0x01' + ipmitool.send_raw(task, cmd) + time.sleep(2) + cmd = '0x32 0x9f 0x01 0x01 0x01 %s' % (self.hex_convert(self.remote_share, True, 64)) + ipmitool.send_raw(task, cmd) + time.sleep(2) + # clear progress bit + cmd = '0x32 0x9f 0x01 0x01 0x00 0x00' + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when setting virtual media path: %s' % str(err)) + return False + + def _set_setup_nfs(self, driver_info, task): + try: + # Set share type NFS + self._set_share_type(task) + # NFS server IP + self._set_nfs_server_ip(driver_info, task) + # Set NFS Mount Root path + self._set_nfs_root_path(driver_info, task) + return True + + except Exception: + return False + + def _toggle_virtual_device(self, enabled, task): + # Enable "Mount CD/DVD" in GUI (p144) should cause vmedia restart withing 2 seconds. + # Seems "Mount CD/DVD" need to be enabled (or toggled) after config. refresh/vmedia restart + # is not enough(?) + try: + # cmd = '0x32 0xcb 0x00 0x0%s' %(str(int(enabled))) + if enabled: + _stat = '01' + else: + _stat = '00' + + cmd = '0x32 0xcb 0x00 0x%s' % _stat + self.log.debug('Set mount CD/DVD enable status %s' % str(enabled)) + ipmitool.send_raw(task, cmd) + + _max_tries = 6 + _try = 1 + _status = '00' + self.log.debug('Ensure CD/DVD enable status is %s' % str(enabled)) + while (_status != _stat): + if _try > _max_tries: + self.log.warning('Ensure virtual media status failed, attempts exceeded. Ignoring.') + return True + time.sleep(2) + cmd = '0x32 0xca 0x00' + out, err = ipmitool.send_raw(task, cmd) + _status = out.strip() + self.log.debug('CD/DVD enable status is "%s"' % str(_status)) + _try = _try + 1 + + except Exception as err: + self.log.warning('Exception when CD/DVD virtual media new firmware? ignoring... Error: %s' % str(err)) + return True + + def _mount_virtual_device(self, task): + return self._toggle_virtual_device(True, task) + + def _demount_virtual_device(self, task): + return self._toggle_virtual_device(False, task) + + def _get_mounted_image_count(self, task): + count = 0 + try: + cmd = '0x32 0xd8 0x00 0x01' + out, err = ipmitool.send_raw(task, cmd) + out = out.strip() + data = out[3:5] + count = int(data, 16) + self.log.debug('Available image count: %d' % count) + except Exception as err: + self.log.debug('Exception when trying to get the image count: %s' % str(err)) + return count + + def _set_image_name(self, image_filename, task): + try: + #cmd = '0x32 0xd7 0x01 0x01 0x01 0x01 %s' % (self.hex_convert(image_filename)) + cmd = '0x32 0xd7 0x01 0x01 0x01 0x01 %s' % (self.hex_convert(image_filename, True, 64)) + self.log.debug('Setting virtual media image: %s' % image_filename) + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.debug('Exception when setting virtual media image: %s' % str(err)) + return False + return True + + def _stop_remote_redirection(self, task): + try: + # Get num of enabled devices + _num_inst = self._get_virtual_media_device_count(task, 'CD') + for driveindex in range(0, _num_inst): + cmd = '0x32 0xd7 0x00 0x01 0x01 0x00 %s' % hex(driveindex) + self.log.debug('Stop redirection CD/DVD drive index %d' % driveindex) + out, err = ipmitool.send_raw(task, cmd) + self.log.debug('ipmitool out = %s' % (out)) + except Exception as err: + # Drive might not be mounted to start with + self.log.debug('_stop_remote_redirection: Ignoring exception when stopping redirection CD/DVD drive index %d error: %s' % (driveindex, str(err))) + pass + + def _clear_ris_configuration(self, task): + # Clear RIS configuration + try: + cmd = '0x32 0x9f 0x01 0x0d' + self.log.debug('Clear RIS configuration.') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when clearing RIS NFS configuration: %s' % str(err)) + return False + return True + + def _wait_for_mount_count(self, task): + # Poll until we got some images from server + _max_tries = 12 + _try = 1 + while self._get_mounted_image_count(task) == 0: + self.log.debug('Check available images count try %d/%d' % (_try, _max_tries)) + if _try > _max_tries: + self.log.warning('Available images count 0, attempts exceeded.') + return False + time.sleep(10) + _try = _try + 1 + return True + + def attach_virtual_cd(self, image_filename, driver_info, task): + + #Enable virtual media + if not self._enable_virtual_media(task): + self.log.error("Failed to enable virtual media") + return False + + #Enable CD/DVD device + if not self._toggle_virtual_device(True, task): + self.log.error("Failed to enable virtual device") + return False + + #Clear RIS configuration + if not self._clear_ris_configuration(task): + self.log.error("Failed to clear RIS configuration") + return False + + #Setup nfs + if not self._set_setup_nfs(driver_info, task): + self.log.error("Failed to setup nfs") + return False + + # Restart Remote Image CD + if not self._restart_ris_cd(task): + self.log.error("Failed to restart RIS CD") + return False + + #Wait for device to be mounted + if not self._wait_for_mount_count(task): + self.log.error("Failed when waiting for the device to appear") + return False + + # Set Image Name + if not self._set_image_name(image_filename, task): + self.log.error("Failed to set image name") + return False + + return self.check_and_wait_for_cd_mounting(image_filename, task, driver_info) + + def detach_virtual_cd(self, driver_info, task): + """Detaches virtual cdrom on the node. + + :param task: an ironic task object + """ + #Enable virtual media + if not self._enable_virtual_media(task): + self.log.error("detach_virtual_cd: Failed to enable virtual media") + return False + + # Restart Remote Image Service + if not self._restart_ris(task): + self.log.error("Failed to restart RIS") + return False + + # Stop redirection + self._stop_remote_redirection(task) + + #Clear RIS configuration + if not self._clear_ris_configuration(task): + self.log.error("detach_virtual_cd: Failed to clear RIS configuration") + return False + + #Demount virtual device + if not self._demount_virtual_device(task): + self.log.error('detach_virtual_cd: Exception when disabling CD/DVD virtual media') + return False + + # Reduce the number of virtual devices (both HD and CD default to 4 devices each) + if not self._set_virtual_media_device_count(task, 'HD', 0): + return False + if not self._set_virtual_media_device_count(task, 'CD', 1): + return False + + return True + + def set_boot_device(self, task): + manager_utils.node_set_boot_device(task, boot_devices.CDROM, persistent=True) +# try: +# #Set boot device to virtual remote CD/DVD persistenly +# #0xC0 persisten +# #0x20 remote cdrom +# #P 422 of ipmi spec +# cmd = '0x00 0x08 0x05 0xC0 0x20 0x00 0x00 0x00' +# out, err = ipmitool.send_raw(task, cmd) +# #BMC boot flag valid bit clearing 1f -> all bit set +# #P 420 of ipmi spec +# cmd = '0x00 0x08 0x03 0x1f' +# out, err = ipmitool.send_raw(task, cmd) +# self.log.info('Set the boot device to remote cd') +# except Exception as err: +# self.log.warning('Error when setting boot device to remote cd') +# diff --git a/src/ironic_virtmedia_driver/vendors/nokia/rm18.py b/src/ironic_virtmedia_driver/vendors/nokia/rm18.py new file mode 100644 index 0000000..bdc8281 --- /dev/null +++ b/src/ironic_virtmedia_driver/vendors/nokia/rm18.py @@ -0,0 +1,419 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import time + +from ironic.conductor import utils as manager_utils +from ironic.common import boot_devices +from ironic.common import exception +from ironic.drivers.modules import ipmitool + +from .nokia_hw import NokiaIronicVirtMediaHW + +class RM18(NokiaIronicVirtMediaHW): + def __init__(self, log): + super(RM18, self).__init__(log) + + def get_disk_attachment_status(self, task): + # Check NFS Service Status + try: + out, err = ipmitool.send_raw(task, '0x32 0xd8 0x06 0x01 0x01 0x00') + _image_name = str(bytearray.fromhex(out.replace('\n', '').strip())) + return 'mounted' + except Exception: + return 'nfserror' + + + def _get_virtual_media_device_count(self, task, devicetype): + try: + _num_inst = 0 + # Get num of enabled devices + if devicetype == 'CD': + _devparam = '0x04' + self.log.debug('Get virtual CD count') + elif devicetype == 'FD': + _devparam = '0x05' + self.log.debug('Get virtual FD count') + elif devicetype == 'HD': + _devparam = '0x06' + self.log.debug('Get virtual HD count') + else: + self.log.warning('Unknown device type "%s"' % devicetype) + return _num_inst + + cmd = '0x32 0xca %s' % _devparam + out, err = ipmitool.send_raw(task, cmd) + _num_inst = int(out.strip()) + self.log.debug('Number of enabled %s devices is %d' % (devicetype, _num_inst)) + return _num_inst + except Exception as err: + # Drive might not be mounted to start with + self.log.debug('Exception when getting number of enabled %s devices. error: %s' % (devicetype, str(err))) + + + def _set_virtual_media_device_count(self, task, devicetype, devicecount): + # Chapter 46.2 page 181 + if not 0 <= devicecount <= 4: + self.log.warning('Number of devices must be in range 0 to 4') + return False + + if devicetype == 'CD': + _devparam = '0x04' + self.log.debug('Setting virtual CD count to %d' % devicecount) + elif devicetype == 'HD': + _devparam = '0x06' + self.log.debug('Setting virtual HD count to %d' % devicecount) + else: + self.log.warning('_set_virtual_media_device_count: Unknown device type "%s"' % devicetype) + return False + + try: + cmd = '0x32 0xcb %s 0x%s' % (_devparam, str(devicecount)) + ipmitool.send_raw(task, cmd) + + _conf_device_num = self._get_virtual_media_device_count(task, devicetype) + _tries = 4 + while _conf_device_num != devicecount and _tries > 0: + self.log.debug('Virtual %s count is %d expecting %d' % (devicetype, _conf_device_num, devicecount)) + time.sleep(5) + _conf_device_num = self._get_virtual_media_device_count(task, devicetype) + _tries = _tries -1 + + except Exception as err: + self.log.warning('Exception when setting virtual media device count, error: %s' % str(err)) + return False + return True + + def _check_virtual_media_started(self, task): + service_status = None + # check virtmedia service status + try: + cmd = '0x32 0xca 0x08' + out, err = ipmitool.send_raw(task, cmd) + service_status = out.strip() + self.log.warning('Virtual media service status: %s' % str(service_status)) + except Exception as err: + self.log.warning('Exception when checking virtual media service: %s' % str(err)) + if service_status == '01': + return True + return False + + def _start_virtual_media(self, task): + # Enable "Remote Media Support" in GUI (p145) + try: + cmd = '0x32 0xcb 0x08 0x01' + self.log.debug('Start virtual media service') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when starting virtual media service: %s' % str(err)) + + def _restart_virtual_media_service(self, task): + try: + cmd = '0x32 0xcb 0x0a 0x01' + self.log.debug('Restart virtual media service') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when restarting virtual media service: %s' % str(err)) + + def _restart_ris(self, task): + try: + self.log.debug('Restart RIS') + cmd = '0x32 0x9f 0x08 0x0b' + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when restarting RIS: %s' % str(err)) + return False + return True + + def _restart_ris_cd(self, task): + try: + self.log.debug('Restart RIS CD media') + cmd = '0x32 0x9f 0x01 0x0b 0x01' + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when restarting RIS CD media: %s' % str(err)) + return False + return True + + def _enable_virtual_media(self, task): + # Speed up things if it service is already running + if self._check_virtual_media_started(task): + self.log.debug('Virtual media service already running.') + # Service is already started + return True + + _max_tries = 6 + _try = 1 + self._start_virtual_media(task) + # Just enabling the service doe not seem to start it (in all HW) + # Resetting it after enabling helps + self._restart_virtual_media_service(task) + while (not self._check_virtual_media_started(task)): + if _try > _max_tries: + self.log.warning('Ensure virtual media service start failed, attempts exceeded.') + return False + time.sleep(5) + _try = _try + 1 + return True + + def _set_nfs_server_ip(self, driver_info, task): + try: + cmd = '0x32 0x9f 0x01 0x02 0x00 %s' % (self.hex_convert(driver_info['provisioning_server'], True, 63)) + self.log.debug('Virtual media server "%s"' % driver_info['provisioning_server']) + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when setting virtual media server: %s' % str(err)) + raise err + + def _set_share_type(self, task): + try: + cmd = '0x32 0x9f 0x01 0x05 0x00 0x6e 0x66 0x73 0x00 0x00 0x00' + self.log.debug('Virtual media share type to NFS.') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when setting virtual media service type NFS: %s' % str(err)) + raise err + + def _set_nfs_root_path(self, driver_info, task): + try: + self.log.debug('Virtual media path to "%s"' % self.remote_share) + # set progress bit (hmm. seems to return error if it is already set.. So should check..) + # Welp there is no way checking this. As workaround clearing it first ( does not seem to + # return error even if alreay cleared). + # clear progress bit + cmd = '0x32 0x9f 0x01 0x01 0x00 0x00' + ipmitool.send_raw(task, cmd) + + # set progress bit + cmd = '0x32 0x9f 0x01 0x01 0x00 0x01' + ipmitool.send_raw(task, cmd) + time.sleep(2) + cmd = '0x32 0x9f 0x01 0x01 0x01 %s' % (self.hex_convert(self.remote_share, True, 64)) + ipmitool.send_raw(task, cmd) + time.sleep(2) + # clear progress bit + cmd = '0x32 0x9f 0x01 0x01 0x00 0x00' + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when setting virtual media path: %s' % str(err)) + return False + + def _set_setup_nfs(self, driver_info, task): + try: + # Set share type NFS + self._set_share_type(task) + # NFS server IP + self._set_nfs_server_ip(driver_info, task) + # Set NFS Mount Root path + self._set_nfs_root_path(driver_info, task) + return True + + except Exception: + return False + + def _toggle_virtual_device(self, enabled, task): + # Enable "Mount CD/DVD" in GUI (p144) should cause vmedia restart withing 2 seconds. + # Seems "Mount CD/DVD" need to be enabled (or toggled) after config. refresh/vmedia restart + # is not enough(?) + try: + # cmd = '0x32 0xcb 0x00 0x0%s' %(str(int(enabled))) + if enabled: + _stat = '01' + else: + _stat = '00' + + cmd = '0x32 0xcb 0x00 0x%s' % _stat + self.log.debug('Set mount CD/DVD enable status %s' % str(enabled)) + ipmitool.send_raw(task, cmd) + + _max_tries = 6 + _try = 1 + _status = '00' + self.log.debug('Ensure CD/DVD enable status is %s' % str(enabled)) + while (_status != _stat): + if _try > _max_tries: + self.log.warning('Ensure virtual media status failed, attempts exceeded.') + return False + time.sleep(2) + cmd = '0x32 0xca 0x00' + out, err = ipmitool.send_raw(task, cmd) + _status = out.strip() + self.log.debug('CD/DVD enable status is "%s"' % str(_status)) + _try = _try + 1 + + except Exception as err: + self.log.warning('Exception when CD/DVD virtual media new firmware? ignoring... Error: %s' % str(err)) + return True + + def _mount_virtual_device(self, task): + return self._toggle_virtual_device(True, task) + + def _demount_virtual_device(self, task): + return self._toggle_virtual_device(False, task) + + def _get_mounted_image_count(self, task): + count = 0 + try: + cmd = '0x32 0xd8 0x00 0x01' + out, err = ipmitool.send_raw(task, cmd) + out = out.strip() + data = out[3:5] + count = int(data, 16) + self.log.debug('Available image count: %d' % count) + except Exception as err: + self.log.debug('Exception when trying to get the image count: %s' % str(err)) + return count + + def _set_image_name(self, image_filename, task): + try: + #cmd = '0x32 0xd7 0x01 0x01 0x01 0x01 %s' % (self.hex_convert(image_filename)) + cmd = '0x32 0xd7 0x01 0x01 0x01 0x01 %s' % (self.hex_convert(image_filename, True, 64)) + self.log.debug('Setting virtual media image: %s' % image_filename) + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.debug('Exception when setting virtual media image: %s' % str(err)) + return False + return True + + def _stop_remote_redirection(self, task): + try: + # Get num of enabled devices + _num_inst = self._get_virtual_media_device_count(task, 'CD') + for driveindex in range(0, _num_inst): + cmd = '0x32 0xd7 0x00 0x01 0x01 0x00 %s' % hex(driveindex) + self.log.debug('Stop redirection CD/DVD drive index %d' % driveindex) + out, err = ipmitool.send_raw(task, cmd) + self.log.debug('ipmitool out = %s' % (out)) + except Exception as err: + # Drive might not be mounted to start with + self.log.debug('_stop_remote_redirection: Ignoring exception when stopping redirection CD/DVD drive index %d error: %s' % (driveindex, str(err))) + pass + + def _clear_ris_configuration(self, task): + # Clear RIS configuration + try: + cmd = '0x32 0x9f 0x01 0x0d' + self.log.debug('Clear RIS configuration.') + ipmitool.send_raw(task, cmd) + except Exception as err: + self.log.warning('Exception when clearing RIS NFS configuration: %s' % str(err)) + return False + return True + + def _wait_for_mount_count(self, task): + # Poll until we got some images from server + _max_tries = 12 + _try = 1 + while self._get_mounted_image_count(task) == 0: + self.log.debug('Check available images count try %d/%d' % (_try, _max_tries)) + if _try > _max_tries: + self.log.warning('Available images count 0, attempts exceeded.') + return False + time.sleep(10) + _try = _try + 1 + return True + + def attach_virtual_cd(self, image_filename, driver_info, task): + + #Enable virtual media + if not self._enable_virtual_media(task): + self.log.error("Failed to enable virtual media") + return False + + #Enable CD/DVD device + if not self._toggle_virtual_device(True, task): + self.log.error("Failed to enable virtual device") + return False + + #Clear RIS configuration + if not self._clear_ris_configuration(task): + self.log.error("Failed to clear RIS configuration") + return False + + #Setup nfs + if not self._set_setup_nfs(driver_info, task): + self.log.error("Failed to setup nfs") + return False + + # Restart Remote Image CD + if not self._restart_ris_cd(task): + self.log.error("Failed to restart RIS CD") + return False + + #Wait for device to be mounted + if not self._wait_for_mount_count(task): + self.log.error("Failed when waiting for the device to appear") + return False + + # Set Image Name + if not self._set_image_name(image_filename, task): + self.log.error("Failed to set image name") + return False + + return self.check_and_wait_for_cd_mounting(image_filename, task, driver_info) + + def detach_virtual_cd(self, driver_info, task): + """Detaches virtual cdrom on the node. + + :param task: an ironic task object + """ + #Enable virtual media + if not self._enable_virtual_media(task): + self.log.error("detach_virtual_cd: Failed to enable virtual media") + return False + + # Restart Remote Image Service + if not self._restart_ris(task): + self.log.error("Failed to restart RIS") + return False + + # Stop redirection + self._stop_remote_redirection(task) + + #Clear RIS configuration + if not self._clear_ris_configuration(task): + self.log.error("detach_virtual_cd: Failed to clear RIS configuration") + return False + + #Demount virtual device + if not self._demount_virtual_device(task): + self.log.error('detach_virtual_cd: Exception when disabling CD/DVD virtual media') + return False + + # Reduce the number of virtual devices (both HD and CD default to 4 devices each) + if not self._set_virtual_media_device_count(task, 'HD', 0): + return False + if not self._set_virtual_media_device_count(task, 'CD', 1): + return False + + return True + + def set_boot_device(self, task): + manager_utils.node_set_boot_device(task, boot_devices.FLOPPY, persistent=True) +# try: +# #Set boot device to virtual remote CD/DVD persistenly +# #0xC0 persisten +# #0x20 remote cdrom +# #P 422 of ipmi spec +# cmd = '0x00 0x08 0x05 0xC0 0x20 0x00 0x00 0x00' +# out, err = ipmitool.send_raw(task, cmd) +# #BMC boot flag valid bit clearing 1f -> all bit set +# #P 420 of ipmi spec +# cmd = '0x00 0x08 0x03 0x1f' +# out, err = ipmitool.send_raw(task, cmd) +# self.log.info('Set the boot device to remote cd') +# except Exception as err: +# self.log.warning('Error when setting boot device to remote cd') +# diff --git a/src/ironic_virtmedia_driver/virtmedia.py b/src/ironic_virtmedia_driver/virtmedia.py new file mode 100644 index 0000000..d42b4b5 --- /dev/null +++ b/src/ironic_virtmedia_driver/virtmedia.py @@ -0,0 +1,447 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import shutil +import tempfile +import tarfile + +from ironic_lib import metrics_utils +from ironic_lib import utils as ironic_utils +from oslo_log import log as logging +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common.i18n import _, _translators +from ironic.common import images +from ironic.common import states +from ironic.common import utils +from ironic.conductor import utils as manager_utils +from ironic_virtmedia_driver.conf import CONF +from ironic.drivers import base +from ironic.drivers.modules import deploy_utils +from ironic_virtmedia_driver import virtmedia_exception + +LOG = logging.getLogger(__name__) + +METRICS = metrics_utils.get_metrics_logger(__name__) + +REQUIRED_PROPERTIES = { + 'virtmedia_deploy_iso': _("Deployment ISO image file name. " + "Required."), +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES + + +def _parse_config_option(): + """Parse config file options. + + This method checks config file options validity. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + error_msgs = [] + if not os.path.isdir(CONF.remote_image_share_root): + error_msgs.append( + _("Value '%s' for remote_image_share_root isn't a directory " + "or doesn't exist.") % + CONF.remote_image_share_root) + if error_msgs: + msg = (_("The following errors were encountered while parsing " + "config file:%s") % error_msgs) + raise exception.InvalidParameterValue(msg) + + +def _parse_driver_info(node): + """Gets the driver specific Node deployment info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required or optional information properly + for this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: the driver_info values of the node. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + d_info = node.driver_info + deploy_info = {} + + deploy_info['virtmedia_deploy_iso'] = d_info.get('virtmedia_deploy_iso') + error_msg = _("Error validating virtual media deploy. Some parameters" + " were missing in node's driver_info") + deploy_utils.check_for_missing_params(deploy_info, error_msg) + + if service_utils.is_image_href_ordinary_file_name( + deploy_info['virtmedia_deploy_iso']): + deploy_iso = os.path.join(CONF.remote_image_share_root, + deploy_info['virtmedia_deploy_iso']) + if not os.path.isfile(deploy_iso): + msg = (_("Deploy ISO file, %(deploy_iso)s, " + "not found for node: %(node)s.") % + {'deploy_iso': deploy_iso, 'node': node.uuid}) + raise exception.InvalidParameterValue(msg) + + return deploy_info + +def _parse_deploy_info(node): + """Gets the instance and driver specific Node deployment info. + + This method validates whether the 'instance_info' and 'driver_info' + property of the supplied node contains the required information for + this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: a dict with the instance_info and driver_info values. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + deploy_info = {} + deploy_info.update(deploy_utils.get_image_instance_info(node)) + deploy_info.update(_parse_driver_info(node)) + + return deploy_info + +def _get_deploy_iso_name(node): + """Returns the deploy ISO file name for a given node. + + :param node: the node for which ISO file name is to be provided. + """ + return "deploy-%s.iso" % node.name + +def _get_boot_iso_name(node): + """Returns the boot ISO file name for a given node. + + :param node: the node for which ISO file name is to be provided. + """ + return "boot-%s.iso" % node.uuid + +def _get_floppy_image_name(node): + """Returns the floppy image name for a given node. + + :param node: the node for which image name is to be provided. + """ + return "image-%s.img" % node.name + + +def _prepare_floppy_image(task, params): + """Prepares the floppy image for passing the parameters. + + This method prepares a temporary vfat filesystem image, which + contains the parameters to be passed to the ramdisk. + Then it uploads the file NFS or CIFS server. + + :param task: a TaskManager instance containing the node to act on. + :param params: a dictionary containing 'parameter name'->'value' mapping + to be passed to the deploy ramdisk via the floppy image. + :returns: floppy image filename + :raises: ImageCreationFailed, if it failed while creating the floppy image. + :raises: VirtmediaOperationError, if copying floppy image file failed. + """ + floppy_filename = _get_floppy_image_name(task.node) + floppy_fullpathname = os.path.join( + CONF.remote_image_share_root, floppy_filename) + + with tempfile.NamedTemporaryFile() as vfat_image_tmpfile_obj: + images.create_vfat_image(vfat_image_tmpfile_obj.name, + parameters=params) + try: + shutil.copyfile(vfat_image_tmpfile_obj.name, + floppy_fullpathname) + except IOError as e: + operation = _("Copying floppy image file") + raise virtmedia_exception.VirtmediaOperationError( + operation=operation, error=e) + + return floppy_filename + +def _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename): + """ Quanta HW cannot attach 2 Virtual media at the moment. + Preparing CD which has floppy content at the end of it as + 64K block tar file. + """ + boot_iso_full_path = CONF.remote_image_share_root + bootable_iso_filename + floppy_image_full_path = CONF.remote_image_share_root + floppy_image_filename + tar_file_path = CONF.remote_image_share_root + floppy_image_filename + '.tar.gz' + + # Prepare a temporary Tar file + tar = tarfile.open(tar_file_path, "w:gz") + tar.add(floppy_image_full_path, arcname=os.path.basename(floppy_image_full_path)) + tar.close() + + # Using dd append Tar to iso and remove Tar file + ironic_utils.dd(tar_file_path, boot_iso_full_path, 'bs=64k', 'conv=notrunc,sync', 'oflag=append') + + os.remove(tar_file_path) + +def _remove_share_file(share_filename): + """Remove given file from the share file system. + + :param share_filename: a file name to be removed. + """ + share_fullpathname = os.path.join( + CONF.remote_image_share_root, share_filename) + LOG.debug(_translators.log_info("_remove_share_file: Unlinking %s"), share_fullpathname) + ironic_utils.unlink_without_raise(share_fullpathname) + +class VirtmediaBoot(base.BootInterface): + """Implementation of a boot interface using Virtual Media.""" + + def __init__(self): + """Constructor of VirtualMediaBoot. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + super(VirtmediaBoot, self).__init__() + + def get_properties(self): + return COMMON_PROPERTIES + + @METRICS.timer('VirtualMediaBoot.validate') + def validate(self, task): + """Validate the deployment information for the task's node. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue, if config option has invalid value. + :raises: InvalidParameterValue, if some information is invalid. + :raises: MissingParameterValue if 'kernel_id' and 'ramdisk_id' are + missing in the Glance image, or if 'kernel' and 'ramdisk' are + missing in the Non Glance image. + """ + d_info = _parse_deploy_info(task.node) + if task.node.driver_internal_info.get('is_whole_disk_image'): + props = [] + elif service_utils.is_glance_image(d_info['image_source']): + props = ['kernel_id', 'ramdisk_id'] + else: + props = ['kernel', 'ramdisk'] + deploy_utils.validate_image_properties(task.context, d_info, + props) + + @METRICS.timer('VirtualMediaBoot.prepare_ramdisk') + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the deploy ramdisk using virtual media. + + Prepares the options for the deployment ramdisk, sets the node to boot + from virtual media cdrom. + + :param task: a TaskManager instance containing the node to act on. + :param ramdisk_params: the options to be passed to the deploy ramdisk. + :raises: ImageRefValidationFailed if no image service can handle + specified href. + :raises: ImageCreationFailed, if it failed while creating the floppy + image. + :raises: InvalidParameterValue if the validation of the + PowerInterface or ManagementInterface fails. + :raises: VirtmediaOperationError, if some operation fails. + """ + + # NOTE(TheJulia): If this method is being called by something + # aside from deployment and clean, such as conductor takeover, we + # should treat this as a no-op and move on otherwise we would modify + # the state of the node due to virtual media operations. + + if (task.node.provision_state != states.DEPLOYING and + task.node.provision_state != states.CLEANING): + return + + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) + ramdisk_params['BOOTIF'] = deploy_nic_mac + os_net_config = task.node.driver_info.get('os_net_config') + if os_net_config: + ramdisk_params['os_net_config'] = os_net_config + + self._setup_deploy_iso(task, ramdisk_params) + + @METRICS.timer('VirtualMediaBoot.clean_up_ramdisk') + def clean_up_ramdisk(self, task): + """Cleans up the boot of ironic ramdisk. + + This method cleans up the environment that was setup for booting the + deploy ramdisk. + + :param task: a task from TaskManager. + :returns: None + :raises: VirtmediaOperationError if operation failed. + """ + self._cleanup_vmedia_boot(task) + + @METRICS.timer('VirtualMediaBoot.prepare_instance') + def prepare_instance(self, task): + """Prepares the boot of instance. + + This method prepares the boot of the instance after reading + relevant information from the node's database. + + :param task: a task from TaskManager. + :returns: None + """ + self._cleanup_vmedia_boot(task) + + node = task.node + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if deploy_utils.get_boot_option(node) == "local" or iwdi: + manager_utils.node_set_boot_device(task, boot_devices.DISK, + persistent=True) + else: + driver_internal_info = node.driver_internal_info + root_uuid_or_disk_id = driver_internal_info['root_uuid_or_disk_id'] + self._configure_vmedia_boot(task, root_uuid_or_disk_id) + + @METRICS.timer('VirtualMediaBoot.clean_up_instance') + def clean_up_instance(self, task): + """Cleans up the boot of instance. + + This method cleans up the environment that was setup for booting + the instance. + + :param task: a task from TaskManager. + :returns: None + :raises: VirtmediaOperationError if operation failed. + """ + _remove_share_file(_get_boot_iso_name(task.node)) + driver_internal_info = task.node.driver_internal_info + driver_internal_info.pop('root_uuid_or_disk_id', None) + task.node.driver_internal_info = driver_internal_info + task.node.save() + self._cleanup_vmedia_boot(task) + + def _configure_vmedia_boot(self, task, root_uuid_or_disk_id): + """Configure vmedia boot for the node.""" + return + + def _set_deploy_boot_device(self, task): + """Set the boot device for deployment""" + manager_utils.node_set_boot_device(task, boot_devices.CDROM) + + def _setup_deploy_iso(self, task, ramdisk_options): + """Attaches virtual media and sets it as boot device. + + This method attaches the given deploy ISO as virtual media, prepares the + arguments for ramdisk in virtual media floppy. + + :param task: a TaskManager instance containing the node to act on. + :param ramdisk_options: the options to be passed to the ramdisk in virtual + media floppy. + :raises: ImageRefValidationFailed if no image service can handle specified + href. + :raises: ImageCreationFailed, if it failed while creating the floppy image. + :raises: VirtmediaOperationError, if some operation on failed. + :raises: InvalidParameterValue if the validation of the + PowerInterface or ManagementInterface fails. + """ + d_info = task.node.driver_info + + deploy_iso_href = d_info['virtmedia_deploy_iso'] + if service_utils.is_image_href_ordinary_file_name(deploy_iso_href): + deploy_iso_file = deploy_iso_href + else: + deploy_iso_file = _get_deploy_iso_name(task.node) + deploy_iso_fullpathname = os.path.join( + CONF.remote_image_share_root, deploy_iso_file) + images.fetch(task.context, deploy_iso_href, deploy_iso_fullpathname) + + self._setup_vmedia_for_boot(task, deploy_iso_file, ramdisk_options) + self._set_deploy_boot_device(task) + + def _setup_vmedia_for_boot(self, task, bootable_iso_filename, parameters=None): + """Sets up the node to boot from the boot ISO image. + + This method attaches a boot_iso on the node and passes + the required parameters to it via a virtual floppy image. + + :param task: a TaskManager instance containing the node to act on. + :param bootable_iso_filename: a bootable ISO image to attach to. + The iso file should be present in NFS/CIFS server. + :param parameters: the parameters to pass in a virtual floppy image + in a dictionary. This is optional. + :raises: ImageCreationFailed, if it failed while creating a floppy image. + :raises: VirtmediaOperationError, if attaching a virtual media failed. + """ + LOG.info(_translators.log_info("Setting up node %s to boot from virtual media"), + task.node.uuid) + + self._detach_virtual_cd(task) + self._detach_virtual_fd(task) + + floppy_image_filename = None + if parameters: + floppy_image_filename = _prepare_floppy_image(task, parameters) + self._attach_virtual_fd(task, floppy_image_filename) + + if floppy_image_filename: + _append_floppy_to_cd(bootable_iso_filename, floppy_image_filename) + + self._attach_virtual_cd(task, bootable_iso_filename) + + def _cleanup_vmedia_boot(self, task): + """Cleans a node after a virtual media boot. + + This method cleans up a node after a virtual media boot. + It deletes floppy and cdrom images if they exist in NFS/CIFS server. + It also ejects both the virtual media cdrom and the virtual media floppy. + + :param task: a TaskManager instance containing the node to act on. + :raises: VirtmediaOperationError if ejecting virtual media failed. + """ + LOG.debug("Cleaning up node %s after virtual media boot", task.node.uuid) + + node = task.node + self._detach_virtual_cd(task) + self._detach_virtual_fd(task) + + _remove_share_file(_get_floppy_image_name(node)) + _remove_share_file(_get_deploy_iso_name(node)) + + def _attach_virtual_cd(self, task, bootable_iso_filename): + """Attaches the given url as virtual media on the node. + + :param node: an ironic node object. + :param bootable_iso_filename: a bootable ISO image to attach to. + The iso file should be present in NFS/CIFS server. + :raises: VirtmediaOperationError if attaching virtual media failed. + """ + return + + def _detach_virtual_cd(self, task): + """Detaches virtual cdrom on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if eject virtual cdrom failed. + """ + return + + def _attach_virtual_fd(self, task, floppy_image_filename): + """Attaches virtual floppy on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if insert virtual floppy failed. + """ + return + + def _detach_virtual_fd(self, task): + """Detaches virtual media floppy on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if eject virtual media floppy failed. + """ + return diff --git a/src/ironic_virtmedia_driver/virtmedia_exception.py b/src/ironic_virtmedia_driver/virtmedia_exception.py new file mode 100644 index 0000000..52b0cb9 --- /dev/null +++ b/src/ironic_virtmedia_driver/virtmedia_exception.py @@ -0,0 +1,20 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from ironic.common import exception +from ironic.common.i18n import _ + +class VirtmediaOperationError(exception.IronicException): + _msg_fmt = _('Virtmedia %(operation)s failed. Reason: %(error)s') diff --git a/src/ironic_virtmedia_driver/virtmedia_ipmi_boot.py b/src/ironic_virtmedia_driver/virtmedia_ipmi_boot.py new file mode 100644 index 0000000..b1bd4dc --- /dev/null +++ b/src/ironic_virtmedia_driver/virtmedia_ipmi_boot.py @@ -0,0 +1,144 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import sys +import time + +from oslo_log import log as logging + +from ironic.common.i18n import _ +from ironic.drivers.modules import ipmitool +from ironic.common import exception +from ironic.conductor import utils as manager_utils +from ironic_virtmedia_driver import virtmedia + +LOG = logging.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'provisioning_server': 'Provisioning server IP hosting deployment ISO and metadata Floppy images. Required.', + 'provisioning_server_http_port': 'Provisioning server port where the images can be obtained with http requests. Required.', + 'vendor': 'Vendor for the installed hardware. Required.', + 'product_family': 'Product family for the hardware. Required.' +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() + +def _parse_driver_info(node): + """Gets the information needed for accessing the node. + + :param node: the Node of interest. + :returns: dictionary of information. + :raises: InvalidParameterValue if any required parameters are incorrect. + :raises: MissingParameterValue if any required parameters are missing. + + """ + info = node.driver_info or {} + missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)] + if missing_info: + raise exception.MissingParameterValue(_( + "virtmedia_ipmi driver requires the following parameters to be set in " + "node's driver_info: %s.") % missing_info) + + provisioning_server = info.get('provisioning_server') + provisioning_server_http_port = info.get('provisioning_server_http_port') + vendor = info.get('vendor') + product_family = info.get('product_family') + ipmi_params = ipmitool._parse_driver_info(node) + res = { + 'provisioning_server': provisioning_server, + 'provisioning_server_http_port': provisioning_server_http_port, + 'vendor': vendor, + 'product_family': product_family, + } + return dict(ipmi_params.items() + res.items()) + +def _get_hw_library(driver_info): + try: + vendor = driver_info.get('vendor').lower() + product_family = driver_info.get('product_family').lower() + obj = driver_info.get('product_family') + + modulename = 'ironic_virtmedia_driver.vendors.%s.%s'%(vendor, product_family) + if modulename not in sys.modules: + modulej = __import__(modulename, fromlist=['']) + globals()[modulename] = modulej + + module = sys.modules[modulename] + + modj = None + if obj in dir(module): + modj = getattr(module, obj) + globals()[obj] = modj + else: + msg = "Cannot find driver for your hardware from the module Vendor: %s Product family: %s" % (vendor, product_family) + LOG.exception(msg) + raise exception.NotFound(msg) + + return modj(LOG) + + except ImportError as err: + msg = "Cannot import driver for your hardware Vendor: %s Product family: %s :: %s"% (vendor, product_family, str(err)) + LOG.exception(msg) + raise exception.NotFound(msg) + except KeyError as err: + LOG.exception("virtmedia has a problem with hw type") + raise exception.IronicException("Internal virtmedia error") + return None + +class VirtualMediaAndIpmiBoot(virtmedia.VirtmediaBoot): + def __init__(self): + """Constructor of VirtualMediaAndIpmiBoot. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + super(VirtualMediaAndIpmiBoot, self).__init__() + + def _attach_virtual_cd(self, task, image_filename): + """Attaches the given url as virtual media on the node. + + :param node: an ironic node object. + :param bootable_iso_filename: a bootable ISO image to attach to. + The iso file should be present in NFS/CIFS server. + :raises: VirtmediaOperationError if attaching virtual media failed. + """ + retry_count = 2 + driver_info = _parse_driver_info(task.node) + + hw = _get_hw_library(driver_info) + + while not hw.attach_virtual_cd(image_filename, driver_info, task) and retry_count: + retry_count -= 1 + time.sleep(1) + LOG.debug("Virtual media attachment failed. Retrying again") + + if not retry_count: + LOG.exception("Failed to attach Virtual media. Max retries exceeded") + raise exception.InstanceDeployFailure(reason='NFS mount failed!') + + def _detach_virtual_cd(self, task): + """Detaches virtual cdrom on the node. + + :param node: an ironic node object. + :raises: VirtmediaOperationError if eject virtual cdrom failed. + """ + driver_info = _parse_driver_info(task.node) + hw = _get_hw_library(driver_info) + hw.detach_virtual_cd(driver_info, task) + + def _set_deploy_boot_device(self, task): + """Set the boot device for deployment""" + driver_info = _parse_driver_info(task.node) + hw = _get_hw_library(driver_info) + hw.set_boot_device(task) diff --git a/src/ironic_virtmedia_driver/virtmedia_ssh_boot.py b/src/ironic_virtmedia_driver/virtmedia_ssh_boot.py new file mode 100644 index 0000000..d29d393 --- /dev/null +++ b/src/ironic_virtmedia_driver/virtmedia_ssh_boot.py @@ -0,0 +1,272 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os + +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import strutils + +import retrying +import paramiko +import six + +from ironic.common import exception +from ironic.common import utils +from ironic.common.i18n import _, _translators +from ironic.drivers import utils as driver_utils +from ironic.conf import CONF +from ironic_virtmedia_driver import virtmedia + +LOG = logging.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'ssh_address': _("IP address of the node to ssh into from where the VMs can be managed. " + "Required."), + 'ssh_username': _("username to authenticate as. Required."), + 'ssh_key_contents': _("private key(s). If ssh_password is also specified " + "it will be used for unlocking the private key. Do " + "not specify ssh_key_filename when this property is " + "specified.") +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +CONSOLE_PROPERTIES = { + 'ssh_terminal_port': _("node's UDP port to connect to. Only required for " + "console access and only applicable for 'virsh'.") +} + +def _get_command_sets(): + """Retrieves the virt_type-specific commands. + Returns commands are as follows: + + base_cmd: Used by most sub-commands as the primary executable + list_all: Lists all VMs (by virt_type identifier) that can be managed. + One name per line, must not be quoted. + get_disk_list: Gets the list of Block devices connected to the VM + attach_disk_device: Attaches a given disk device to VM + detach_disk_device: Detaches a disk device from a VM + """ + virt_type="virsh" + if virt_type == "virsh": + virsh_cmds = { + 'base_cmd': 'LC_ALL=C /usr/bin/virsh', + 'list_all': 'list --all --name', + 'get_disk_list': ( + "domblklist --domain {_NodeName_} | " + "grep var | awk -F \" \" '{print $1}'"), + 'attach_disk_device': 'attach-disk --domain {_NodeName_} --source /var/lib/libvirt/images/{_ImageName_} --target {_TargetDev_} --sourcetype file --mode readonly --type {_DevType_} --config', + 'detach_disk_device': 'detach-disk --domain {_NodeName_} --target {_TargetDev_} --config', + } + + return virsh_cmds + else: + raise exception.InvalidParameterValue(_( + "SSHPowerDriver '%(virt_type)s' is not a valid virt_type, ") % + {'virt_type': virt_type}) + +def _ssh_execute(ssh_obj, cmd_to_exec): + """Executes a command via ssh. + + Executes a command via ssh and returns a list of the lines of the + output from the command. + + :param ssh_obj: paramiko.SSHClient, an active ssh connection. + :param cmd_to_exec: command to execute. + :returns: list of the lines of output from the command. + :raises: SSHCommandFailed on an error from ssh. + + """ + LOG.debug(_translators.log_error("Executing SSH cmd: %r"), cmd_to_exec) + try: + output_list = processutils.ssh_execute(ssh_obj, + cmd_to_exec)[0].split('\n') + except Exception as e: + LOG.error(_translators.log_error("Cannot execute SSH cmd %(cmd)s. Reason: %(err)s."), + {'cmd': cmd_to_exec, 'err': e}) + raise exception.SSHCommandFailed(cmd=cmd_to_exec) + + return output_list + + +def _parse_driver_info(node): + """Gets the information needed for accessing the node. + + :param node: the Node of interest. + :returns: dictionary of information. + :raises: InvalidParameterValue if any required parameters are incorrect. + :raises: MissingParameterValue if any required parameters are missing. + + """ + info = node.driver_info or {} + missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)] + if missing_info: + raise exception.MissingParameterValue(_( + "SSHPowerDriver requires the following parameters to be set in " + "node's driver_info: %s.") % missing_info) + + address = info.get('ssh_address') + username = info.get('ssh_username') + key_contents = info.get('ssh_key_contents') + terminal_port = info.get('ssh_terminal_port') + + if terminal_port is not None: + terminal_port = utils.validate_network_port(terminal_port, + 'ssh_terminal_port') + + # NOTE(deva): we map 'address' from API to 'host' for common utils + res = { + 'host': address, + 'username': username, + 'port': 22, + 'uuid': node.uuid, + 'terminal_port': terminal_port + } + + cmd_set = _get_command_sets() + res['cmd_set'] = cmd_set + + if key_contents: + res['key_contents'] = key_contents + else: + raise exception.InvalidParameterValue(_( + "ssh_virtmedia Driver requires ssh_key_contents to be set.")) + return res + +def _get_ssh_connection(connection): + """Returns an SSH client connected to a node. + + :param node: the Node. + :returns: paramiko.SSHClient, an active ssh connection. + + """ + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + key_contents = connection.get('key_contents') + if key_contents: + data = six.StringIO(key_contents) + if "BEGIN RSA PRIVATE" in key_contents: + pkey = paramiko.RSAKey.from_private_key(data) + elif "BEGIN DSA PRIVATE" in key_contents: + pkey = paramiko.DSSKey.from_private_key(data) + else: + # Can't include the key contents - secure material. + raise ValueError(_("Invalid private key")) + else: + pkey = None + ssh.connect(connection.get('host'), + username=connection.get('username'), + password=None, + port=connection.get('port', 22), + pkey=pkey, + key_filename=connection.get('key_filename'), + timeout=connection.get('timeout', 10)) + + # send TCP keepalive packets every 20 seconds + ssh.get_transport().set_keepalive(20) + except Exception as e: + LOG.debug("SSH connect failed: %s", e) + raise exception.SSHConnectFailed(host=connection.get('host')) + + return ssh + +def _get_disk_attachment_status(driver_info, node_name, ssh_obj, target_disk='hda'): + cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], + driver_info['cmd_set']['get_disk_list']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) + blk_dev_list = _ssh_execute(ssh_obj, cmd_to_exec) + LOG.debug("Attached block devices list for node %(node_name)s: %(blk_dev_list)s", {'node_name': node_name, 'blk_dev_list': blk_dev_list}) + if target_disk in blk_dev_list: + return True + else: + return False + +def _get_sftp_connection(connection): + try: + key_contents = connection.get('key_contents') + if key_contents: + data = six.StringIO(key_contents) + if "BEGIN RSA PRIVATE" in key_contents: + pkey = paramiko.RSAKey.from_private_key(data) + elif "BEGIN DSA PRIVATE" in key_contents: + pkey = paramiko.DSSKey.from_private_key(data) + else: + # Can't include the key contents - secure material. + raise ValueError(_("Invalid private key")) + else: + pkey = None + + sftp_obj = paramiko.Transport((connection.get('host'), connection.get('port', 22))) + sftp_obj.connect(None, username=connection.get('username'), + password=None, pkey=pkey) + return sftp_obj + except Exception as e: + LOG.error(_translators.log_error("Cannot establish connection to sftp target. Reason: %(err)s."), + {'err': e}) + raise exception.CommunicationError(e) + +def _copy_media_to_virt_server(sftp_obj, media_file): + LOG.debug("Copying file: %s to target" %(media_file)) + sftp = paramiko.SFTPClient.from_transport(sftp_obj) + try: + sftp.put('/remote_image_share_root/%s' %media_file, '/var/lib/libvirt/images/%s' %media_file) + except Exception as e: + LOG.error(_translators.log_error("Cannot copy %(media_file)s to target. Reason: %(err)s."), + {'media_file': media_file, 'err': e}) + raise exception.CommunicationError(media_file) + +class VirtualMediaAndSSHBoot(virtmedia.VirtmediaBoot): + def __init__(self): + """Constructor of VirtualMediaAndSSHBoot. + + :raises: InvalidParameterValue, if config option has invalid value. + """ + super(VirtualMediaAndSSHBoot, self).__init__() + + def _attach_virtual_cd(self, task, image_filename): + driver_info = _parse_driver_info(task.node) + ssh_obj = _get_ssh_connection(driver_info) + sftp_obj = _get_sftp_connection(driver_info) + node_name = task.node.name + if _get_disk_attachment_status(driver_info, node_name, ssh_obj, 'hda'): + LOG.debug("A CD is already attached to node %s, not taking any action", node_name) + return + + _copy_media_to_virt_server(sftp_obj, image_filename) + cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], + driver_info['cmd_set']['attach_disk_device']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) + cmd_to_exec = cmd_to_exec.replace('{_ImageName_}', image_filename) + cmd_to_exec = cmd_to_exec.replace('{_TargetDev_}', 'hda') + cmd_to_exec = cmd_to_exec.replace('{_DevType_}', 'cdrom') + LOG.debug("Ironic node-name: %s, virsh domain name: %s, image_filename: %s" %(task.node.name, node_name, image_filename)) + + _ssh_execute(ssh_obj, cmd_to_exec) + + def _detach_virtual_cd(self, task): + driver_info = _parse_driver_info(task.node) + ssh_obj = _get_ssh_connection(driver_info) + node_name = task.node.name + + if not _get_disk_attachment_status(driver_info, node_name, ssh_obj, 'hda'): + LOG.debug("No CD is attached to node %s, not taking any action", node_name) + return + + cmd_to_exec = "%s %s" % (driver_info['cmd_set']['base_cmd'], + driver_info['cmd_set']['detach_disk_device']) + cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node_name) + cmd_to_exec = cmd_to_exec.replace('{_TargetDev_}', 'hda') + _ssh_execute(ssh_obj, cmd_to_exec) diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..53742e1 --- /dev/null +++ b/src/setup.py @@ -0,0 +1,44 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +VERSION = '0.1' +PROJECT = 'ironic_virtmedia_driver' + +from setuptools import setup, find_packages +setup( + name=PROJECT, + version=VERSION, + description='ironic-virtmedia-driver', + author='Chandra Rangavajjula, Janne Suominen', + author_email='chandra.s.rangavajjula@nokia.com, janne.suominen@nokia.com', + platforms=['Any'], + scripts=[], + provides=[], + install_requires=['openstack-ironic-common', 'ironic_virtmedia_driver'], + namespace_packages=[], + packages=find_packages(), + include_package_data=True, + entry_points={ + 'ironic.hardware.types': [ + 'ipmi_virtmedia = ironic_virtmedia_driver.ipmi_virtmedia:IPMIVirtmediaHardware', + 'ssh_virtmedia = ironic_virtmedia_driver.ssh_virtmedia:SSHVirtmediaHardware' + ], + 'ironic.hardware.interfaces.boot': [ + 'virtmedia_ipmi_boot = ironic_virtmedia_driver.virtmedia_ipmi_boot:VirtualMediaAndIpmiBoot', + 'virtmedia_ssh_boot = ironic_virtmedia_driver.virtmedia_ssh_boot:VirtualMediaAndSSHBoot' + ], + }, + zip_safe=False, +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4e245a4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,22 @@ +# Copyright 2019 Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +[tox] +envlist = pylint +skipsdist = True + +[testenv:pylint] +commands = + /bin/echo "pylint disabled" -- 2.16.6