diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf93fd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.tox +.stestr +*__pycache__* +*.pyc +build +.idea diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 0000000..7cb4023 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,4 @@ +- project: + templates: + - openstack-python3-train-jobs + - openstack-cover-jobs 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/rebuild b/rebuild new file mode 100644 index 0000000..538bb06 --- /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 +f57f307b-74f9-4f1d-bdc2-50e20aecbb63 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b1d4872 --- /dev/null +++ b/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 +# +# Build requirements +charm-tools>=2.4.4 +simplejson diff --git a/src/HACKING.md b/src/HACKING.md new file mode 100644 index 0000000..5e0548a --- /dev/null +++ b/src/HACKING.md @@ -0,0 +1,10 @@ +# Overview + +This charm is developed as part of the OpenStack Charms project, and as such you +should refer to the [OpenStack Charm Development Guide](https://opendev.org/openstack/charm-guide) for details on how +to contribute to this charm. + +You can find its source code here: . + + + diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..5aab7d5 --- /dev/null +++ b/src/README.md @@ -0,0 +1,43 @@ +# Overview + +This charm provides the Placement service for an OpenStack Cloud. + +OpenStack Train or later is required. + +# Usage + +As of Train, the placement API is managed by this charm and is no longer managed +by the nova-cloud-controller charm. + +Placement relies on mysql, keystone, and nova-cloud-controller charms: + + juju deploy --series bionic --config openstack-origin=cloud:bionic-train cs:placement + juju add-relation placement mysql + juju add-relation placement keystone + juju add-relation placement nova-cloud-controller + +If upgrading nova-cloud-controller to Train, the upgrade requires some coordination to +transition to the new API endpoints. Prior to upgrading nova-cloud-controller to Train, +the placement charm must be deployed for Train and related to the Stein-based +nova-cloud-controller. It is important that nova-cloud-controller is paused while the +API transition occurs (pause prior to adding relations for the placement charm) as the +placement charm will migrate existing placement tables from the nova_api database to a +new placement database. Once the new placement endpoints are registered, +nova-cloud-controller can be resumed. After all of the steps have completed, +nova-cloud-controller can then be upgraded to Train. Here's an example of the steps +that were just described: + + juju deploy --series bionic --config openstack-origin=cloud:bionic-train cs:placement + juju run-action nova-cloud-controller/0 pause + juju add-relation placement mysql + juju add-relation placement keystone + juju add-relation placement nova-cloud-controller + openstack endpoint list # ensure placement endpoints are listening on new placment IP address + juju run-action nova-cloud-controller/0 resume + juju config nova-cloud-controller openstack-origin=cloud:bionic-train + +# Bugs + +Please report bugs on [Launchpad](https://bugs.launchpad.net/charm-placement/+filebug). + +For general questions please refer to the OpenStack [Charm Guide](https://docs.openstack.org/charm-guide/latest/). diff --git a/src/copyright b/src/copyright new file mode 100644 index 0000000..8aa5eea --- /dev/null +++ b/src/copyright @@ -0,0 +1,6 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2019, Canonical Ltd +License: Apache-2.0 + diff --git a/src/layer.yaml b/src/layer.yaml new file mode 100644 index 0000000..51e7371 --- /dev/null +++ b/src/layer.yaml @@ -0,0 +1,17 @@ +includes: + - layer:leadership + - layer:openstack-api + - interface:mysql-shared + - interface:keystone + - interface:placement + - interface:hacluster + - interface:openstack-ha +options: + basic: + use_venv: True + include_system_packages: False + packages: [ 'libffi-dev', 'libssl-dev' ] + repo: https://opendev.org/openstack/charm-placement +config: + deletes: + - verbose diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. diff --git a/src/lib/charm/__init__.py b/src/lib/charm/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/charm/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. diff --git a/src/lib/charm/openstack/__init__.py b/src/lib/charm/openstack/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/charm/openstack/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. diff --git a/src/lib/charm/openstack/placement.py b/src/lib/charm/openstack/placement.py new file mode 100644 index 0000000..f342a46 --- /dev/null +++ b/src/lib/charm/openstack/placement.py @@ -0,0 +1,111 @@ +# Copyright 2019 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 collections +import subprocess + +import charmhelpers.core.hookenv as hookenv +import charmhelpers.core.host as host +import charms_openstack.charm +import charms_openstack.ip as os_ip + +PLACEMENT_CONF = '/etc/placement/placement.conf' +PLACEMENT_WSGI_CONF = '/etc/apache2/sites-available/placement-api.conf' +PLACEMENT_MIGRATE_DB_CONF = '/etc/placement/migrate-db.rc' + +charms_openstack.charm.use_defaults('charm.default-select-release') + + +class PlacementCharm(charms_openstack.charm.HAOpenStackCharm): + service_name = name = 'placement' + + release = 'train' + + packages = ['placement-api', 'python3-pymysql', 'mysql-client'] + + api_ports = { + 'placement-api': { + os_ip.PUBLIC: 8778, + os_ip.ADMIN: 8778, + os_ip.INTERNAL: 8778, + } + } + + group = 'placement' + service_type = 'placement' + default_service = 'placement-api' + services = ['apache2', 'haproxy'] + + required_relations = ['shared-db', 'identity-service'] + + restart_map = { + PLACEMENT_CONF: services, + PLACEMENT_WSGI_CONF: services, + PLACEMENT_MIGRATE_DB_CONF: services, + } + + ha_resources = ['vips', 'haproxy', 'dnsha'] + + release_pkg = 'placement-common' + + package_codenames = { + 'placement-common': collections.OrderedDict([ + ('2', 'train'), + ]), + } + + mysql_migrate_db = '/usr/share/placement/mysql-migrate-db.sh' + + migrate_cmd = [mysql_migrate_db, '--migrate', '--skip-locks', + PLACEMENT_MIGRATE_DB_CONF] + + sync_cmd = ['placement-manage', 'db', 'sync'] + + def get_database_setup(self): + return [ + dict(database='placement', + username='placement', + prefix='placement'), + dict(database='nova_api', + username='nova', + prefix='novaapi')] + + def db_migrate(self): + if not self.db_sync_done() and hookenv.is_leader(): + try: + subprocess.check_call(self.migrate_cmd) + except subprocess.CalledProcessError as error: + hookenv.log('{} returncode={} output={}'.format( + self.migrate_cmd, error.returncode, error.output), + level=hookenv.DEBUG) + if error.returncode == 4 or error.returncode == 3: + # 4: No data present in nova database (nothing to migrate) + # 3: Migration has already completed + return False + raise + return True + + def db_sync(self): + if not self.db_sync_done() and hookenv.is_leader(): + subprocess.check_call(self.sync_cmd) + hookenv.leader_set({'db-sync-done': True}) + self.restart_all() + + def disable_services(self): + for svc in self.services: + host.service_pause(svc) + + def enable_services(self): + for svc in self.services: + host.service_resume(svc) diff --git a/src/metadata.yaml b/src/metadata.yaml new file mode 100644 index 0000000..f3cbf15 --- /dev/null +++ b/src/metadata.yaml @@ -0,0 +1,23 @@ +name: placement +summary: OpenStack placement service +maintainer: OpenStack Charmers +description: | + OpenStack Placement provides an HTTP service for managing, selecting, + and claiming providers of classes of inventory representing available + resources in a cloud. + . + OpenStack Train or later is required. +tags: + - openstack +series: + - bionic + - eoan +subordinate: false +requires: + shared-db: + interface: mysql-shared + identity-service: + interface: keystone +provides: + placement: + interface: placement diff --git a/src/reactive/__init__.py b/src/reactive/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/reactive/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. diff --git a/src/reactive/placement_handlers.py b/src/reactive/placement_handlers.py new file mode 100644 index 0000000..d362aa9 --- /dev/null +++ b/src/reactive/placement_handlers.py @@ -0,0 +1,66 @@ +# Copyright 2019 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 charms.reactive as reactive + +import charms_openstack.bus +import charms_openstack.charm + +charms_openstack.bus.discover() + + +charms_openstack.charm.use_defaults( + 'charm.installed', + 'shared-db.connected', + 'identity-service.connected', + 'identity-service.available', + 'config.changed', + 'update-status', + 'upgrade-charm', + 'certificates.available', +) + + +@reactive.when('shared-db.available') +@reactive.when('identity-service.available') +def render_config(*args): + with charms_openstack.charm.provide_charm_instance() as placement_charm: + placement_charm.render_with_interfaces(args) + placement_charm.assess_status() + reactive.set_state('config.rendered') + + +@reactive.when('config.rendered') +@reactive.when('placement.available') +@reactive.when_not('db.synced') +def init_db(): + with charms_openstack.charm.provide_charm_instance() as placement_charm: + placement = reactive.endpoint_from_flag('placement.available') + disabled = placement.get_nova_placement_disabled() + if disabled: + placement_charm.disable_services() + placement_charm.db_migrate() + placement_charm.db_sync() + placement_charm.enable_services() + placement_charm.assess_status() + placement.set_placement_enabled() + reactive.set_state('db.synced') + + +@reactive.when('ha.connected') +def cluster_connected(hacluster): + """Configure HA resources in corosync""" + with charms_openstack.charm.provide_charm_instance() as placement_charm: + placement_charm.configure_ha_resources(hacluster) + placement_charm.assess_status() diff --git a/src/templates/train/migrate-db.rc b/src/templates/train/migrate-db.rc new file mode 100644 index 0000000..2ce20ff --- /dev/null +++ b/src/templates/train/migrate-db.rc @@ -0,0 +1,13 @@ +NOVA_API_DB="nova_api" +NOVA_API_USER="nova" +{% if shared_db.host -%} +NOVA_API_PASS="{{ shared_db.get_password(prefix='novaapi') }}" +NOVA_API_DB_HOST="{{ shared_db.host }}" +{%- endif %} +PLACEMENT_DB="placement" +PLACEMENT_USER="placement" +{% if shared_db.host -%} +PLACEMENT_PASS="{{ shared_db.get_password(prefix='placement') }}" +PLACEMENT_DB_HOST="{{ shared_db.host }}" +{%- endif %} +MIGRATE_TABLES="allocations placement_aggregates consumers inventories projects resource_classes resource_provider_aggregates resource_provider_traits resource_providers traits users" diff --git a/src/templates/train/placement-api.conf b/src/templates/train/placement-api.conf new file mode 100644 index 0000000..de302fe --- /dev/null +++ b/src/templates/train/placement-api.conf @@ -0,0 +1,37 @@ +Listen {{ options.service_listen_info.placement_api.public_port }} + + + WSGIScriptAlias / /usr/bin/placement-api + WSGIDaemonProcess placement processes={{ options.wsgi_worker_context.processes }} threads=1 user=placement group=placement display-name=%{GROUP} + WSGIProcessGroup placement + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + LimitRequestBody 114688 + + = 2.4> + ErrorLogFormat "%{cu}t %M" + + + ErrorLog /var/log/apache2/placement_error.log + CustomLog /var/log/apache2/placement_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + + +Alias /placement /usr/bin/placement-api + + SetHandler wsgi-script + Options +ExecCGI + + WSGIProcessGroup placement + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + diff --git a/src/templates/train/placement.conf b/src/templates/train/placement.conf new file mode 100644 index 0000000..7129eb1 --- /dev/null +++ b/src/templates/train/placement.conf @@ -0,0 +1,15 @@ +[DEFAULT] +debug = {{ options.debug }} + +[api] +auth_strategy = keystone + +{% if shared_db.host -%} +[placement_database] +connection = {{ shared_db.get_uri(prefix='placement') }} +{%- endif %} + +{% include "parts/section-keystone-authtoken" %} + +[placement] +randomize_allocation_candidates = true diff --git a/src/test-requirements.txt b/src/test-requirements.txt new file mode 100644 index 0000000..3640c8b --- /dev/null +++ b/src/test-requirements.txt @@ -0,0 +1,11 @@ +# This file is managed centrally. If you find the need to modify this as a +# one-off, please don't. Intead, consult #openstack-charms and ask about +# requirements management in charms via bot-control. Thank you. +charm-tools>=2.4.4 +coverage>=3.6 +mock>=1.2 +flake8>=2.2.4,<=2.4.1 +stestr>=2.2.0 +requests>=2.18.4 +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/src/tests/bundles/bionic-train.yaml b/src/tests/bundles/bionic-train.yaml new file mode 100644 index 0000000..0dc7e48 --- /dev/null +++ b/src/tests/bundles/bionic-train.yaml @@ -0,0 +1,91 @@ +series: bionic +relations: +- [ keystone, mysql ] +- - nova-cloud-controller:shared-db + - mysql:shared-db +- - nova-cloud-controller:amqp + - rabbitmq-server:amqp +- [ nova-cloud-controller, glance ] +- [ nova-cloud-controller, keystone ] +- [ nova-compute, nova-cloud-controller ] +- - nova-compute + - rabbitmq-server:amqp +- [ nova-compute, glance ] +- [ glance, mysql ] +- [ glance, keystone ] +- [ glance, rabbitmq-server ] +- [ neutron-gateway, nova-cloud-controller ] +- [ "neutron-gateway:amqp", rabbitmq-server ] +- [ neutron-api, mysql ] +- [ neutron-api, rabbitmq-server ] +- [ neutron-api, nova-cloud-controller ] +- [ neutron-api, neutron-openvswitch ] +- [ neutron-api, keystone ] +- [ neutron-api, neutron-gateway ] +- [ neutron-openvswitch, nova-compute ] +- [ neutron-openvswitch, rabbitmq-server ] +- [ placement, mysql ] +- [ placement, keystone ] +- [ placement, nova-cloud-controller ] +applications: + rabbitmq-server: + charm: cs:~openstack-charmers-next/rabbitmq-server + num_units: 1 + constraints: mem=1G + glance: + charm: cs:~openstack-charmers-next/glance + num_units: 1 + constraints: mem=1G + options: + openstack-origin: cloud:bionic-train + keystone: + charm: cs:~openstack-charmers-next/keystone + num_units: 1 + options: + admin-password: openstack + openstack-origin: cloud:bionic-train + mysql: + charm: cs:~openstack-charmers-next/percona-cluster + num_units: 1 + options: + innodb-buffer-pool-size: 256M + max-connections: 1000 + neutron-api: + charm: cs:~openstack-charmers-next/neutron-api + num_units: 1 + options: + flat-network-providers: physnet1 + neutron-security-groups: true + openstack-origin: cloud:bionic-train + neutron-openvswitch: + charm: cs:~openstack-charmers-next/neutron-openvswitch + neutron-gateway: + charm: cs:~openstack-charmers-next/neutron-gateway + num_units: 1 + options: + bridge-mappings: physnet1:br-ex + openstack-origin: cloud:bionic-train + nova-cloud-controller: + charm: cs:~openstack-charmers-next/nova-cloud-controller + num_units: 1 + options: + network-manager: Neutron + openstack-origin: cloud:bionic-train + debug: true + nova-compute: + charm: cs:~openstack-charmers-next/nova-compute + num_units: 1 + constraints: mem=4G + options: + enable-live-migration: true + enable-resize: true + migration-auth-type: ssh + openstack-origin: cloud:bionic-train + debug: true + placement: + charm: ../../../placement + num_units: 1 + constraints: mem=1G + options: + openstack-origin: cloud:bionic-train + debug: true diff --git a/src/tests/tests.yaml b/src/tests/tests.yaml new file mode 100644 index 0000000..7378b40 --- /dev/null +++ b/src/tests/tests.yaml @@ -0,0 +1,13 @@ +charm_name: placement +tests: + - zaza.openstack.charm_tests.nova.tests.CirrosGuestCreateTest +configure: + - zaza.openstack.charm_tests.glance.setup.add_cirros_image + - zaza.openstack.charm_tests.glance.setup.add_lts_image + - zaza.openstack.charm_tests.neutron.setup.basic_overcloud_network + - zaza.openstack.charm_tests.nova.setup.create_flavors + - zaza.openstack.charm_tests.nova.setup.manage_ssh_key +gate_bundles: + - bionic-train +smoke_bundles: + - bionic-train diff --git a/src/tox.ini b/src/tox.ini new file mode 100644 index 0000000..00c1134 --- /dev/null +++ b/src/tox.ini @@ -0,0 +1,44 @@ +[tox] +envlist = pep8 +skipsdist = True +# NOTE(beisner): Avoid build/test env pollution by not enabling sitepackages. +sitepackages = False +# NOTE(beisner): 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_API_* OS_* +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 = + true + +[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..dd5e16a --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,14 @@ +# 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 +# +# Lint and unit test requirements +flake8>=2.2.4,<=2.4.1 +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 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2cc5a50 --- /dev/null +++ b/tox.ini @@ -0,0 +1,88 @@ +# Source charm: ./tox.ini +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. +[tox] +skipsdist = True +envlist = pep8,py3 +# NOTE(beisner): Avoid build/test env pollution by not enabling sitepackages. +sitepackages = False +# NOTE(beisner): 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 + JUJU_REPOSITORY={toxinidir}/build +passenv = http_proxy https_proxy INTERFACE_PATH +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 {posargs} + +[testenv:py35] +basepython = python3.5 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run {posargs} + +[testenv:py36] +basepython = python3.6 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run {posargs} + +[testenv:py37] +basepython = python3.7 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run {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 {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 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..0df0b6a --- /dev/null +++ b/unit_tests/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2019 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 sys + +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() diff --git a/unit_tests/test_lib_charm_openstack_placement.py b/unit_tests/test_lib_charm_openstack_placement.py new file mode 100644 index 0000000..e6b6de0 --- /dev/null +++ b/unit_tests/test_lib_charm_openstack_placement.py @@ -0,0 +1,79 @@ +# Copyright 2019 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. + +from __future__ import absolute_import +from __future__ import print_function + +from unittest.mock import patch, call + +import charms_openstack.test_utils as test_utils + +import charm.openstack.placement as placement + + +class Helper(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(placement.PlacementCharm.release) + + +class TestPlacementCharm(Helper): + + def test_get_database_setup(self): + c = placement.PlacementCharm() + result = c.get_database_setup() + self.assertEqual(result, [{'database': 'placement', + 'username': 'placement', + 'prefix': 'placement'}, + {'database': 'nova_api', + 'username': 'nova', + 'prefix': 'novaapi'}]) + + @patch.object(placement.PlacementCharm, 'db_sync_done') + @patch.object(placement.subprocess, 'check_call') + @patch.object(placement.hookenv, 'is_leader') + def test_db_migrate(self, is_leader, check_call, db_sync_done): + c = placement.PlacementCharm() + is_leader.return_value = True + db_sync_done.return_value = False + c.db_migrate() + check_call.assert_called_with( + ['/usr/share/placement/mysql-migrate-db.sh', + '--migrate', + '--skip-locks', + '/etc/placement/migrate-db.rc']) + + @patch.object(placement.PlacementCharm, 'db_sync_done') + @patch.object(placement.subprocess, 'check_call') + @patch.object(placement.hookenv, 'is_leader') + @patch.object(placement.hookenv, 'leader_set') + def test_db_sync(self, leader_set, is_leader, check_call, db_sync_done): + c = placement.PlacementCharm() + is_leader.return_value = True + db_sync_done.return_value = False + c.db_sync() + check_call.assert_called_with(['placement-manage', 'db', 'sync']) + + @patch.object(placement.host, 'service_pause') + def test_disable_services(self, service_pause): + c = placement.PlacementCharm() + c.disable_services() + service_pause.assert_has_calls([call('apache2'), call('haproxy')]) + + @patch.object(placement.host, 'service_resume') + def test_enable_services(self, service_resume): + c = placement.PlacementCharm() + c.enable_services() + service_resume.assert_has_calls([call('apache2'), call('haproxy')]) diff --git a/unit_tests/test_placement_handlers.py b/unit_tests/test_placement_handlers.py new file mode 100644 index 0000000..c0289b5 --- /dev/null +++ b/unit_tests/test_placement_handlers.py @@ -0,0 +1,97 @@ +# Copyright 2019 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. + +from __future__ import absolute_import +from __future__ import print_function + +import mock + +import reactive.placement_handlers as handlers + +import charms_openstack.test_utils as test_utils + + +class TestRegisteredHooks(test_utils.TestRegisteredHooks): + + def test_hooks(self): + defaults = [ + 'charm.installed', + 'shared-db.connected', + 'identity-service.connected', + 'identity-service.available', + 'config.changed', + 'update-status', + 'upgrade-charm', + 'certificates.available'] + hook_set = { + 'when': { + 'render_config': ('shared-db.available', + 'identity-service.available',), + 'init_db': ('config.rendered', + 'placement.available',), + 'cluster_connected': ('ha.connected',), + }, + 'when_not': { + 'init_db': ('db.synced',), + }, + } + # test that the hooks were registered via the + # reactive.barbican_handlers + self.registered_hooks_test_helper(handlers, hook_set, defaults) + + +class TestPlacementHandlers(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.placement_charm = mock.MagicMock() + self.patch_object(handlers.charms_openstack.charm, + 'provide_charm_instance', + new=mock.MagicMock()) + self.provide_charm_instance().__enter__.return_value = ( + self.placement_charm) + self.provide_charm_instance().__exit__.return_value = None + + def test_render_config(self): + self.patch_object(handlers.reactive, 'set_state') + + handlers.render_config('arg1', 'arg2') + self.placement_charm.render_with_interfaces.assert_called_once_with( + ('arg1', 'arg2')) + self.placement_charm.assess_status.assert_called_once_with() + self.set_state.assert_called_once_with('config.rendered') + + def test_init_db(self): + self.patch_object(handlers.reactive, 'set_state') + self.patch_object(handlers.reactive, 'endpoint_from_flag') + placement = mock.MagicMock() + self.endpoint_from_flag.return_value = placement + + handlers.init_db() + placement.get_nova_placement_disabled.assert_called_once_with() + self.placement_charm.disable_services.assert_called_once_with() + self.placement_charm.db_migrate.assert_called_once_with() + self.placement_charm.db_sync.assert_called_once_with() + self.placement_charm.enable_services.assert_called_once_with() + self.placement_charm.assess_status.assert_called_once_with() + placement.set_placement_enabled.assert_called_once_with() + self.set_state.assert_called_once_with('db.synced') + + def test_cluster_connected(self): + hacluster = mock.MagicMock() + + handlers.cluster_connected(hacluster) + self.placement_charm.configure_ha_resources.assert_called_once_with( + hacluster) + self.placement_charm.assess_status.assert_called_once_with()