From c8e3d3c94bacc4cdd28be5cacd2dfa7fffa4ebb5 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Thu, 17 Sep 2020 15:39:59 +0000 Subject: [PATCH] Allign project structure with OpenStack charms --- .stestr.conf | 4 + LICENSE | 202 ++++++++++++++++++ README.md | 2 +- lib/__init__.py => docs/.keep | 0 rebuild | 5 + requirements.txt | 11 + src/README.md | 1 + config.yaml => src/config.yaml | 11 +- icon.svg => src/icon.svg | 0 layer.yaml => src/layer.yaml | 0 {lib/charm => src/lib}/__init__.py | 0 .../openstack => src/lib/charm}/__init__.py | 0 .../lib/charm/openstack}/__init__.py | 0 .../lib/charm/openstack/ironic}/__init__.py | 0 .../openstack/ironic/controller_utils.py | 69 +----- .../lib}/charm/openstack/ironic/ironic.py | 51 ++--- metadata.yaml => src/metadata.yaml | 0 src/reactive/__init__.py | 0 {reactive => src/reactive}/ironic_handlers.py | 41 ++-- .../templates}/etc_default_tftpd-hpa | 0 .../etc_ironic_rootwrap.d_ironic-lib.filters | 0 ...etc_ironic_rootwrap.d_ironic-utils.filters | 0 .../templates}/etc_nginx_nginx.conf | 0 .../templates}/parts/keystone-authtoken | 0 .../templates}/parts/section-deploy | 0 .../templates}/parts/section-pxe | 2 +- .../templates}/parts/service-auth | 0 .../templates}/tftpboot_grub_grub.cfg | 0 .../templates}/tftpboot_map-file | 0 .../templates}/train/ironic.conf | 0 .../templates}/train/rootwrap.conf | 0 src/test-requirements.txt | 8 + {tests => src/tests}/00-setup | 0 {tests => src/tests}/10-deploy | 0 src/tox.ini | 50 +++++ test-requirements.txt | 24 +++ tox.ini | 97 +++++++++ unit_tests/__init__.py | 11 + unit_tests/test_ironic_conductor_handlers.py | 119 +++++++++++ 39 files changed, 580 insertions(+), 128 deletions(-) create mode 100644 .stestr.conf create mode 100644 LICENSE mode change 100644 => 120000 README.md rename lib/__init__.py => docs/.keep (100%) create mode 100644 rebuild create mode 100644 requirements.txt create mode 100644 src/README.md rename config.yaml => src/config.yaml (85%) rename icon.svg => src/icon.svg (100%) rename layer.yaml => src/layer.yaml (100%) rename {lib/charm => src/lib}/__init__.py (100%) rename {lib/charm/openstack => src/lib/charm}/__init__.py (100%) rename {lib/charm/openstack/ironic => src/lib/charm/openstack}/__init__.py (100%) rename {reactive => src/lib/charm/openstack/ironic}/__init__.py (100%) rename {lib => src/lib}/charm/openstack/ironic/controller_utils.py (59%) rename {lib => src/lib}/charm/openstack/ironic/ironic.py (75%) rename metadata.yaml => src/metadata.yaml (100%) create mode 100644 src/reactive/__init__.py rename {reactive => src/reactive}/ironic_handlers.py (57%) rename {templates => src/templates}/etc_default_tftpd-hpa (100%) rename {templates => src/templates}/etc_ironic_rootwrap.d_ironic-lib.filters (100%) rename {templates => src/templates}/etc_ironic_rootwrap.d_ironic-utils.filters (100%) rename {templates => src/templates}/etc_nginx_nginx.conf (100%) rename {templates => src/templates}/parts/keystone-authtoken (100%) rename {templates => src/templates}/parts/section-deploy (100%) rename {templates => src/templates}/parts/section-pxe (94%) rename {templates => src/templates}/parts/service-auth (100%) rename {templates => src/templates}/tftpboot_grub_grub.cfg (100%) rename {templates => src/templates}/tftpboot_map-file (100%) rename {templates => src/templates}/train/ironic.conf (100%) rename {templates => src/templates}/train/rootwrap.conf (100%) create mode 100644 src/test-requirements.txt rename {tests => src/tests}/00-setup (100%) rename {tests => src/tests}/10-deploy (100%) create mode 100644 src/tox.ini create mode 100644 test-requirements.txt create mode 100644 tox.ini create mode 100644 unit_tests/__init__.py create mode 100644 unit_tests/test_ironic_conductor_handlers.py diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..2808535 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ + 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.md b/README.md deleted file mode 100644 index 5b51c5b..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# Ironic API juju charm diff --git a/README.md b/README.md new file mode 120000 index 0000000..351df1d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +src/README.md \ No newline at end of file diff --git a/lib/__init__.py b/docs/.keep similarity index 100% rename from lib/__init__.py rename to docs/.keep diff --git a/rebuild b/rebuild new file mode 100644 index 0000000..7ec14a8 --- /dev/null +++ b/rebuild @@ -0,0 +1,5 @@ +# This file is used to trigger rebuilds +# when dependencies of the charm change, +# but nothing in the charm needs to. +# simply change the uuid to something new +da9e9b3c-9030-42c1-ac3a-4c873a7c56d4 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..aaaa3e0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. See the 'global' dir contents for available +# choices of *requirements.txt files for OpenStack Charms: +# https://github.com/openstack-charmers/release-tools +# +setuptools<50.0.0 # https://github.com/pypa/setuptools/commit/04e3df22df840c6bb244e9b27bc56750c44b7c85 +# Build requirements +charm-tools>=2.4.4 +# importlib-resources 1.1.0 removed Python 3.5 support +importlib-resources<1.1.0 +simplejson diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..5b51c5b --- /dev/null +++ b/src/README.md @@ -0,0 +1 @@ +# Ironic API juju charm diff --git a/config.yaml b/src/config.yaml similarity index 85% rename from config.yaml rename to src/config.yaml index a665d96..2989312 100644 --- a/config.yaml +++ b/src/config.yaml @@ -90,5 +90,14 @@ options: bare metal nodes after they have been releases. This option is mandatory to allow Neutron network interfaces. The same network may be used for both cleaning and provisioning. - + extra-pxe-params: + default: "" + type: string + description: | + Additional kernel command line parameters to pass to the deployment kernel. + Options must be space delimited and will be passed as is to the deployment image. + Aside from regular linux kernel command line parameters, you can also configure + the ironic python agent (IPA) from within the deployment image. See the IPA + documentation for a list of command line parameters which can be passed via + pxe_append_params. diff --git a/icon.svg b/src/icon.svg similarity index 100% rename from icon.svg rename to src/icon.svg diff --git a/layer.yaml b/src/layer.yaml similarity index 100% rename from layer.yaml rename to src/layer.yaml diff --git a/lib/charm/__init__.py b/src/lib/__init__.py similarity index 100% rename from lib/charm/__init__.py rename to src/lib/__init__.py diff --git a/lib/charm/openstack/__init__.py b/src/lib/charm/__init__.py similarity index 100% rename from lib/charm/openstack/__init__.py rename to src/lib/charm/__init__.py diff --git a/lib/charm/openstack/ironic/__init__.py b/src/lib/charm/openstack/__init__.py similarity index 100% rename from lib/charm/openstack/ironic/__init__.py rename to src/lib/charm/openstack/__init__.py diff --git a/reactive/__init__.py b/src/lib/charm/openstack/ironic/__init__.py similarity index 100% rename from reactive/__init__.py rename to src/lib/charm/openstack/ironic/__init__.py diff --git a/lib/charm/openstack/ironic/controller_utils.py b/src/lib/charm/openstack/ironic/controller_utils.py similarity index 59% rename from lib/charm/openstack/ironic/controller_utils.py rename to src/lib/charm/openstack/ironic/controller_utils.py index 7b17eb0..550ceb2 100644 --- a/lib/charm/openstack/ironic/controller_utils.py +++ b/src/lib/charm/openstack/ironic/controller_utils.py @@ -4,7 +4,6 @@ import shutil from charmhelpers.core.templating import render import charmhelpers.core.host as ch_host import charmhelpers.fetch as fetch -import charmhelpers.core.hookenv as hookenv _IRONIC_USER = "ironic" @@ -55,6 +54,7 @@ class PXEBootBase(object): self.TFTP_CONFIG: [self.TFTPD_SERVICE,], self.MAP_FILE: [self.TFTPD_SERVICE,], self.GRUB_CFG: [self.TFTPD_SERVICE,], + self.HTTP_SERVER_CONF: [self.HTTPD_SERVICE_NAME,], } def determine_packages(self): @@ -89,64 +89,6 @@ class PXEBootBase(object): ch_host.chownr( self.HTTP_ROOT, _IRONIC_USER, _IRONIC_GROUP, chowntopdir=True) - # def _create_file_map(self): - # self._ensure_folders() - # render(source='tftp-file-map', - # target=self.MAP_FILE, - # owner=_IRONIC_USER, - # perms=0o664, - # context={}) - - # def _create_grub_cfg(self): - # self._ensure_folders() - # render(source='grub-efi', - # target=os.path.join(self.GRUB_DIR, "grub.cfg"), - # owner="root", - # perms=0o644, - # context={ - # "tftpboot": self.TFTP_ROOT, - # }) - - # def _create_tftp_config(self): - # cfg_dir = os.path.dirname(self.TFTP_CONFIG) - # if os.path.isdir(cfg_dir) is False: - # raise Exception("Could not find %s" % cfg_dir) - # render(source='tftpd-hpa', - # target=self.TFTP_CONFIG, - # owner="root", - # perms=0o644, - # context={ - # "tftpboot": self.TFTP_ROOT, - # "max_tftp_block_size": self._config.get( - # "max_tftp_block_size", 0) - # }) - - # def _enable_ipxe_webserver(self): - # if self._config.get("use_ipxe", False) is False: - # return - # fetch.apt_install(self.HTTPD_PACKAGES, fatal=True) - # render(source='nginx.conf', - # target=self.HTTP_SERVER_CONF, - # owner="root", - # perms=0o644, - # context={ - # "ironic_user": _IRONIC_USER, - # "ipxe_http_port": self._config.get( - # "ipxe_http_port", 8080), - # "httpboot": self.HTTP_ROOT, - # }) - - # def configure_resources(self): - # # On Ubuntu 20.04, if IPv6 is not available on the system, - # # the tftp-hpa package fails to install properly. We create the - # # config beforehand, forcing IPv4. - # self._create_tftp_config() - # self._create_file_map() - # fetch.apt_install(self.TFTP_PACKAGES, fatal=True) - # fetch.apt_install(self.PACKAGES, fatal=True) - # self._copy_resources() - # self._create_grub_cfg() - class PXEBootBionic(PXEBootBase): @@ -164,11 +106,10 @@ class PXEBootBionic(PXEBootBase): def get_pxe_config_class(charm_config): - # In the future, we may need to make slight adjustments to package - # names and/or configuration files, based on the version of Ubuntu - # we are installing on. This function serves as a factory which will - # return an instance of the proper class to the charm. For now we only - # have one class. + # We may need to make slight adjustments to package names and/or + # configuration files, based on the version of Ubuntu we are installing + # on. This function serves as a factory which will return an instance + # of the proper class to the charm. For now we only have one class. series = ch_host.get_distrib_codename() if series == "bionic": return PXEBootBionic(charm_config) diff --git a/lib/charm/openstack/ironic/ironic.py b/src/lib/charm/openstack/ironic/ironic.py similarity index 75% rename from lib/charm/openstack/ironic/ironic.py rename to src/lib/charm/openstack/ironic/ironic.py index d4738a0..9d8614c 100644 --- a/lib/charm/openstack/ironic/ironic.py +++ b/src/lib/charm/openstack/ironic/ironic.py @@ -13,6 +13,8 @@ from charms_openstack.adapters import ( ) import charm.openstack.ironic.controller_utils as controller_utils +import charms_openstack.adapters as adapters +import charmhelpers.contrib.network.ip as ch_ip PACKAGES = [ 'ironic-conductor', @@ -46,30 +48,14 @@ OPENSTACK_RELEASE_KEY = 'ironic-charm.openstack-release-version' charms_openstack.charm.use_defaults('charm.default-select-release') -def restart_all(): - IronicConductorCharm.singleton.restart_all() +@adapters.config_property +def deployment_interface_ip(args): + return ch_ip.get_relation_ip("deployment") -def assess_status(): - IronicConductorCharm.singleton.assess_status() - - -def request_endpoint_information(keystone): - charm = IronicConductorCharm.singleton - keystone.request_credentials( - charm.name, region=charm.region) - - -def request_amqp_access(amqp): - charm = IronicConductorCharm.singleton - user, vhost = charm.get_amqp_credentials() - amqp.request_access(username=user, vhost=vhost) - - -def setup_database(database): - charm = IronicConductorCharm.singleton - for db in charm.get_database_setup(): - database.configure(**db) +@adapters.config_property +def internal_interface_ip(args): + return ch_ip.get_relation_ip("internal") class IronicAdapters(OpenStackRelationAdapters): @@ -111,6 +97,7 @@ class IronicConductorCharm(charms_openstack.charm.OpenStackCharm): 'ironic-common': collections.OrderedDict([ ('13', 'train'), ('15', 'ussuri'), + ('16', 'victoria'), ]), } @@ -121,32 +108,18 @@ class IronicConductorCharm(charms_openstack.charm.OpenStackCharm): self.pxe_config = controller_utils.get_pxe_config_class( self.config) self.packages.extend(self.pxe_config.determine_packages()) - self._configure_ipxe_webserver() self.config["tftpboot"] = self.pxe_config.TFTP_ROOT self.config["httpboot"] = self.pxe_config.HTTP_ROOT self.config["ironic_user"] = self.pxe_config.IRONIC_USER self.config["ironic_group"] = self.pxe_config.IRONIC_GROUP self.restart_map.update(self.pxe_config.get_restart_map()) - - def _configure_ipxe_webserver(self): - httpd_svc_name = self.pxe_config.HTTPD_SERVICE_NAME - self.services.append(httpd_svc_name) - self.restart_map[self.pxe_config.HTTP_SERVER_CONF] = [httpd_svc_name,] - - def upgrade_charm(self): - self.install() - super().upgrade_charm() - self.assess_status() - - def config_changed(self): - self.install() - self.pxe_config._copy_resources() - super().config_changed() - self.assess_status() + self.services.append( + self.pxe_config.HTTPD_SERVICE_NAME) def install(self): self.configure_source() super().install() + self.pxe_config._copy_resources() self.assess_status() def get_amqp_credentials(self): diff --git a/metadata.yaml b/src/metadata.yaml similarity index 100% rename from metadata.yaml rename to src/metadata.yaml diff --git a/src/reactive/__init__.py b/src/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reactive/ironic_handlers.py b/src/reactive/ironic_handlers.py similarity index 57% rename from reactive/ironic_handlers.py rename to src/reactive/ironic_handlers.py index 8aa1c6c..8b96f49 100644 --- a/reactive/ironic_handlers.py +++ b/src/reactive/ironic_handlers.py @@ -7,8 +7,6 @@ import charms_openstack.charm as charm import charm.openstack.ironic.ironic as ironic # noqa from charmhelpers.core.templating import render -import charmhelpers.contrib.network.ip as ch_ip -import charms_openstack.adapters as adapters # Use the charms.openstack defaults for common states and hooks @@ -16,44 +14,43 @@ charm.use_defaults( 'charm.installed', 'upgrade-charm', 'config.changed', - 'update-status') + 'update-status', + 'certificates.available') @reactive.when('shared-db.available') @reactive.when('ironic-api.available') @reactive.when('identity-credentials.available') @reactive.when('amqp.available') -def render_stuff(*args): +def render(*args): hookenv.log("about to call the render_configs with {}".format(args)) with charm.provide_charm_instance() as ironic_charm: ironic_charm.render_with_interfaces( charm.optional_interfaces(args)) + ironic_charm.configure_tls() ironic_charm.assess_status() reactive.set_state('config.complete') @reactive.when('identity-credentials.connected') -def setup_endpoint(keystone): - ironic.request_endpoint_information(keystone) - ironic.assess_status() +def request_keystone_credentials(keystone): + with charm.provide_charm_instance() as ironic_charm: + keystone.request_credentials( + ironic_charm.name, region=ironic_charm.region) + ironic_charm.assess_status() + @reactive.when('amqp.connected') def request_amqp_access(amqp): - ironic.request_amqp_access(amqp) - ironic.assess_status() + with charm.provide_charm_instance() as ironic_charm: + user, vhost = ironic_charm.get_amqp_credentials() + amqp.request_access(username=user, vhost=vhost) + ironic_charm.assess_status() @reactive.when('shared-db.connected') -def setup_database(database): - ironic.setup_database(database) - ironic.assess_status() - - -@adapters.config_property -def deployment_interface_ip(args): - return ch_ip.get_relation_ip("deployment") - - -@adapters.config_property -def internal_interface_ip(args): - return ch_ip.get_relation_ip("internal") +def request_database_access(database): + with charm.provide_charm_instance() as ironic_charm: + for db in ironic_charm.get_database_setup(): + database.configure(**db) + ironic_charm.assess_status() diff --git a/templates/etc_default_tftpd-hpa b/src/templates/etc_default_tftpd-hpa similarity index 100% rename from templates/etc_default_tftpd-hpa rename to src/templates/etc_default_tftpd-hpa diff --git a/templates/etc_ironic_rootwrap.d_ironic-lib.filters b/src/templates/etc_ironic_rootwrap.d_ironic-lib.filters similarity index 100% rename from templates/etc_ironic_rootwrap.d_ironic-lib.filters rename to src/templates/etc_ironic_rootwrap.d_ironic-lib.filters diff --git a/templates/etc_ironic_rootwrap.d_ironic-utils.filters b/src/templates/etc_ironic_rootwrap.d_ironic-utils.filters similarity index 100% rename from templates/etc_ironic_rootwrap.d_ironic-utils.filters rename to src/templates/etc_ironic_rootwrap.d_ironic-utils.filters diff --git a/templates/etc_nginx_nginx.conf b/src/templates/etc_nginx_nginx.conf similarity index 100% rename from templates/etc_nginx_nginx.conf rename to src/templates/etc_nginx_nginx.conf diff --git a/templates/parts/keystone-authtoken b/src/templates/parts/keystone-authtoken similarity index 100% rename from templates/parts/keystone-authtoken rename to src/templates/parts/keystone-authtoken diff --git a/templates/parts/section-deploy b/src/templates/parts/section-deploy similarity index 100% rename from templates/parts/section-deploy rename to src/templates/parts/section-deploy diff --git a/templates/parts/section-pxe b/src/templates/parts/section-pxe similarity index 94% rename from templates/parts/section-pxe rename to src/templates/parts/section-pxe index a0f9031..d1832b7 100644 --- a/templates/parts/section-pxe +++ b/src/templates/parts/section-pxe @@ -9,7 +9,7 @@ tftp_root={{tftpboot}} # value) tftp_server = {{ options.deployment_interface_ip }} -pxe_append_params = nofb nomodeset vga=normal console=tty0 console=ttyS0,115200n8 +pxe_append_params = nofb nomodeset vga=normal console=tty0 console=ttyS0,115200n8 {{ options.extra_pxe_params }} {% if options.use_ipxe -%} # Enable iPXE boot. (boolean value) diff --git a/templates/parts/service-auth b/src/templates/parts/service-auth similarity index 100% rename from templates/parts/service-auth rename to src/templates/parts/service-auth diff --git a/templates/tftpboot_grub_grub.cfg b/src/templates/tftpboot_grub_grub.cfg similarity index 100% rename from templates/tftpboot_grub_grub.cfg rename to src/templates/tftpboot_grub_grub.cfg diff --git a/templates/tftpboot_map-file b/src/templates/tftpboot_map-file similarity index 100% rename from templates/tftpboot_map-file rename to src/templates/tftpboot_map-file diff --git a/templates/train/ironic.conf b/src/templates/train/ironic.conf similarity index 100% rename from templates/train/ironic.conf rename to src/templates/train/ironic.conf diff --git a/templates/train/rootwrap.conf b/src/templates/train/rootwrap.conf similarity index 100% rename from templates/train/rootwrap.conf rename to src/templates/train/rootwrap.conf diff --git a/src/test-requirements.txt b/src/test-requirements.txt new file mode 100644 index 0000000..d3c9be8 --- /dev/null +++ b/src/test-requirements.txt @@ -0,0 +1,8 @@ +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. See the 'global' dir contents for available +# choices of *requirements.txt files for OpenStack Charms: +# https://github.com/openstack-charmers/release-tools +# +# Functional Test Requirements (let Zaza's dependencies solve all dependencies here!) +git+https://github.com/openstack-charmers/zaza.git#egg=zaza +git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack diff --git a/tests/00-setup b/src/tests/00-setup similarity index 100% rename from tests/00-setup rename to src/tests/00-setup diff --git a/tests/10-deploy b/src/tests/10-deploy similarity index 100% rename from tests/10-deploy rename to src/tests/10-deploy diff --git a/src/tox.ini b/src/tox.ini new file mode 100644 index 0000000..07a7adc --- /dev/null +++ b/src/tox.ini @@ -0,0 +1,50 @@ +# Source charm (with zaza): ./src/tox.ini +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. See the 'global' dir contents for available +# choices of tox.ini for OpenStack Charms: +# https://github.com/openstack-charmers/release-tools + +[tox] +envlist = pep8 +skipsdist = True +# NOTE: Avoid build/test env pollution by not enabling sitepackages. +sitepackages = False +# NOTE: Avoid false positives by not skipping missing interpreters. +skip_missing_interpreters = False + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 +whitelist_externals = juju +passenv = HOME TERM CS_* OS_* TEST_* +deps = -r{toxinidir}/test-requirements.txt +install_command = + pip install {opts} {packages} + +[testenv:pep8] +basepython = python3 +deps=charm-tools +commands = charm-proof + +[testenv:func-noop] +basepython = python3 +commands = + functest-run-suite --help + +[testenv:func] +basepython = python3 +commands = + functest-run-suite --keep-model + +[testenv:func-smoke] +basepython = python3 +commands = + functest-run-suite --keep-model --smoke + +[testenv:func-target] +basepython = python3 +commands = + functest-run-suite --keep-model --bundle {posargs} + +[testenv:venv] +commands = {posargs} diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..d078e27 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,24 @@ +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. See the 'global' dir contents for available +# choices of *requirements.txt files for OpenStack Charms: +# https://github.com/openstack-charmers/release-tools +# +setuptools<50.0.0 # https://github.com/pypa/setuptools/commit/04e3df22df840c6bb244e9b27bc56750c44b7c85 +# Lint and unit test requirements +flake8>=2.2.4 +stestr>=2.2.0 +requests>=2.18.4 +charms.reactive +mock>=1.2 +nose>=1.3.7 +coverage>=3.6 +git+https://github.com/openstack/charms.openstack.git#egg=charms.openstack +# +# Revisit for removal / mock improvement: +netifaces # vault +psycopg2-binary # vault +tenacity # vault +pbr # vault +cryptography # vault, keystone-saml-mellon +lxml # keystone-saml-mellon +hvac # vault, barbican-vault diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c91922e --- /dev/null +++ b/tox.ini @@ -0,0 +1,97 @@ +# Source charm: ./tox.ini +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. See the 'global' dir contents for available +# choices of tox.ini for OpenStack Charms: +# https://github.com/openstack-charmers/release-tools + +[tox] +skipsdist = True +envlist = pep8,py3 +# NOTE: Avoid build/test env pollution by not enabling sitepackages. +sitepackages = False +# NOTE: Avoid false positives by not skipping missing interpreters. +skip_missing_interpreters = False + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 + TERM=linux + LAYER_PATH={toxinidir}/layers + INTERFACE_PATH={toxinidir}/interfaces + JUJU_REPOSITORY={toxinidir}/build +passenv = http_proxy https_proxy INTERFACE_PATH LAYER_PATH JUJU_REPOSITORY +install_command = + pip install {opts} {packages} +deps = + -r{toxinidir}/requirements.txt + +[testenv:build] +basepython = python3 +commands = + charm-build --log-level DEBUG -o {toxinidir}/build src {posargs} + +[testenv:py3] +basepython = python3 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:py35] +basepython = python3.5 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:py36] +basepython = python3.6 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:py37] +basepython = python3.7 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:py38] +basepython = python3.8 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:pep8] +basepython = python3 +deps = -r{toxinidir}/test-requirements.txt +commands = flake8 {posargs} src unit_tests + +[testenv:cover] +# Technique based heavily upon +# https://github.com/openstack/nova/blob/master/tox.ini +basepython = python3 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +setenv = + {[testenv]setenv} + PYTHON=coverage run +commands = + coverage erase + stestr run --slowest {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + +[coverage:run] +branch = True +concurrency = multiprocessing +parallel = True +source = + . +omit = + .tox/* + */charmhelpers/* + unit_tests/* + +[testenv:venv] +basepython = python3 +commands = {posargs} + +[flake8] +# E402 ignore necessary for path append before sys module import in actions +ignore = E402,W503,W504 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..186c1d9 --- /dev/null +++ b/unit_tests/__init__.py @@ -0,0 +1,11 @@ +import sys +import mock + +sys.path.append('src') +sys.path.append('src/lib') + +# Mock out charmhelpers so that we can test without it. +import charms_openstack.test_mocks # noqa +charms_openstack.test_mocks.mock_charmhelpers() +sys.modules['charmhelpers.core.decorators'] = ( + charms_openstack.test_mocks.charmhelpers.core.decorators) diff --git a/unit_tests/test_ironic_conductor_handlers.py b/unit_tests/test_ironic_conductor_handlers.py new file mode 100644 index 0000000..bc53b15 --- /dev/null +++ b/unit_tests/test_ironic_conductor_handlers.py @@ -0,0 +1,119 @@ +# Copyright 2020 Canonical Ltd +# +# 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 +import mock + +from charm.openstack.ironic import ironic +import reactive.ironic_handlers as handlers + +import charms_openstack.test_utils as test_utils + + +class TestRegisteredHooks(test_utils.TestRegisteredHooks): + + def test_hooks(self): + defaults = [ + 'charm.installed', + 'config.changed', + 'update-status', + 'upgrade-charm', + ] + hook_set = { + 'when': { + 'render': ('shared-db.available', + 'ironic-api.available', + 'identity-credentials.available', + 'amqp.available',), + 'request_keystone_credentials': ( + 'identity-credentials.connected',), + 'request_amqp_access': ( + 'amqp.connected',), + 'request_database_access': ( + 'shared-db.connected',), + }, + 'hook': { + 'upgrade_charm': ('upgrade-charm',), + }, + } + # test that the hooks were registered via the + # reactive.ironic_handlers + self.registered_hooks_test_helper(handlers, hook_set, defaults) + + +class TestIronicHandlers(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(ironic.IronicConductorCharm.release) + self.ironic_charm = mock.MagicMock() + self.patch_object(handlers.charm, 'provide_charm_instance', + new=mock.MagicMock()) + self.provide_charm_instance().__enter__.return_value = \ + self.ironic_charm + self.provide_charm_instance().__exit__.return_value = None + + def test_request_keystone_credentials(self): + keystone = mock.MagicMock() + handlers.request_keystone_credentials(keystone) + keystone.request_credentials.assert_called_once_with( + self.ironic_charm.name, + region=self.ironic_charm.region) + self.ironic_charm.assess_status.assert_called_once_with() + + def test_render(self): + self.patch('charms.reactive.set_state', 'set_state') + self.patch_object(handlers.charm, 'optional_interfaces') + self.optional_interfaces.return_value = ('fake', 'interface', 'list') + handlers.render('arg1', 'arg2') + self.ironic_charm.render_with_interfaces.assert_called_once_with( + ('fake', 'interface', 'list')) + self.optional_interfaces.assert_called_once_with( + ('arg1', 'arg2')) + self.ironic_charm.configure_tls.assert_called_once_with() + self.ironic_charm.assess_status.assert_called_once_with() + self.set_state.assert_called_once_with('config.complete') + + def test_request_amqp_access(self): + amqp = mock.MagicMock() + config = { + 'rabbit-user': 'ironic', + 'rabbit-vhost': 'openstack', + } + self.ironic_charm.get_amqp_credentials.return_value = list( + config.values()) + handlers.request_amqp_access(amqp) + amqp.request_access.assert_called_once_with( + username=config['rabbit-user'], + vhost=config['rabbit-vhost']) + self.ironic_charm.assess_status.assert_called_once_with() + + def test_request_database_access(self): + database = mock.MagicMock() + dbs = [{ + "database": "ironic", + "username": "ironic", + }, + # Ironic only needs one DB, but the code can handle more, + # so we test it. + { + "database": "second_db", + "username": "second_user", + }] + self.ironic_charm.get_database_setup.return_value = dbs + calls = [mock.call(**i) for i in dbs] + + handlers.request_database_access(database) + database.configure.assert_has_calls(calls, any_order=True) + self.ironic_charm.assess_status.assert_called_once_with()