From cbd986cf87a5da72d77e4300dc38cd408061a9c9 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 2 Oct 2017 16:52:41 -0500 Subject: [PATCH] Add OSC plugin job Also remove legacy bits from openstackclient-check-plugins Change-Id: I5ef187f96d62fcf9813c98c568f549ecb8fc86ba --- .zuul.yaml | 43 +++++ .../openstackclient-check-plugins/run.yaml | 20 ++ tests/check_osc_commands.py | 181 ++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 .zuul.yaml create mode 100644 playbooks/openstackclient-check-plugins/run.yaml create mode 100755 tests/check_osc_commands.py diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 0000000..5c8622e --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,43 @@ +# from o-z-j/zuul.d/zuul-legacy-jobs.yaml +- job: + name: openstackclient-check-plugins + parent: unittests + run: playbooks/openstackclient-check-plugins/run + timeout: 1800 + required-projects: + - openstack/openstackclient + - openstack/python-openstackclient + - openstack/python-barbicanclient + - openstack/python-cloudkittyclient + - openstack/python-congressclient + - openstack/python-designateclient + - openstack/python-heatclient + - openstack/python-ironic-inspector-client + - openstack/python-ironicclient + - openstack/python-karborclient + - openstack/python-mistralclient + - openstack/python-muranoclient + - openstack/python-neutronclient + - openstack/python-octaviaclient + - openstack/python-pankoclient + - openstack/python-rsdclient + - openstack/python-saharaclient + - openstack/python-searchlightclient + - openstack/python-senlinclient + - openstack/python-tripleoclient + - openstack/python-troveclient + - openstack/python-vitrageclient + - openstack/python-watcherclient + - openstack/python-zaqarclient + +- project-template: + name: openstackclient-plugin-jobs + check: + jobs: + - openstackclient-check-plugins: + voting: false + +- project: + name: openstack/openstackclient + templates: + - openstackclient-plugin-jobs diff --git a/playbooks/openstackclient-check-plugins/run.yaml b/playbooks/openstackclient-check-plugins/run.yaml new file mode 100644 index 0000000..e7a980e --- /dev/null +++ b/playbooks/openstackclient-check-plugins/run.yaml @@ -0,0 +1,20 @@ +- hosts: all + name: Run openstackclient plugin check + tasks: + + - shell: + cmd: | + virtualenv osc_plugins + + - shell: + cmd: | + osc_plugins/bin/pip install -e src/git.openstack.org/openstack/{{ item }} + with_items: "{{ zuul.projects | selectattr('required') | list }}" + + - shell: + cmd: | + osc_plugins/bin/pip freeze + # Finally install the change under test + osc_plugins/bin/pip install -e src/{{ zuul.project.canonical_name }} + osc_plugins/bin/openstack --version + osc_plugins/bin/python tools/check_osc_commands.py diff --git a/tests/check_osc_commands.py b/tests/check_osc_commands.py new file mode 100755 index 0000000..7daf5c8 --- /dev/null +++ b/tests/check_osc_commands.py @@ -0,0 +1,181 @@ +#! /usr/bin/env python +# +# 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. + +""" +This module will use `pkg_resources` to scan commands for all OpenStackClient +plugins with the purpose of detecting duplicate commands. +""" + +import pkg_resources +import traceback + + +def find_duplicates(): + """Find duplicates commands. + + Here we use `pkg_resources` to find all modules. There will be many modules + on a system, so we filter them out based on "openstack" since that is the + prefix that OpenStackClient plugins will have. + + Each module has various entry points, each OpenStackClient command will + have an entrypoint. Each entry point has a short name (ep.name) which + is the command the user types, as well as a long name (ep.module_name) + which indicates from which module the entry point is from. + + For example, the entry point and module for v3 user list is:: + + module => openstackclient.identity.v3 + ep.name => user_list + ep.module_name => openstackclient.identity.v3.user + + We keep a running tally of valid commands, duplicate commands and commands + that failed to load. + + The resultant data structure for valid commands should look like:: + + {'user_list': + ['openstackclient.identity.v3.user', + 'openstackclient.identity.v2.0.user'] + 'flavor_list': + [openstackclient.compute.v2.flavor'] + } + + The same can be said for the duplicate and failed commands. + """ + + valid_cmds = {} + duplicate_cmds = {} + failed_cmds = {} + + # find all modules on the system + modules = set() + for dist in pkg_resources.working_set: + entry_map = pkg_resources.get_entry_map(dist) + modules.update(set(entry_map.keys())) + + for module in modules: + # OpenStackClient plugins are prefixed with "openstack", skip otherwise + if not module.startswith('openstack'): + continue + + # Iterate over all entry points + for ep in pkg_resources.iter_entry_points(module): + + # Check for a colon, since valid entrypoints will have one, for + # example: quota_show = openstackclient.common.quota:ShowQuota + # and plugin entrypoints will not, for + # example: orchestration = heatclient.osc.plugin + if ':' not in str(ep): + continue + + # cliff does a mapping between spaces and underscores + ep_name = ep.name.replace(' ', '_') + + try: + ep.load() + except Exception: + exc_string = traceback.format_exc() + message = "{}\n{}".format(ep.module_name, exc_string) + failed_cmds.setdefault(ep_name, []).append(message) + + if _is_valid_command(ep_name, ep.module_name, valid_cmds): + valid_cmds.setdefault(ep_name, []).append(ep.module_name) + else: + duplicate_cmds.setdefault(ep_name, []).append(ep.module_name) + + if duplicate_cmds: + print("Duplicate commands found...") + print(duplicate_cmds) + return True + if failed_cmds: + print("Some commands failed to load...") + print(failed_cmds) + return True + + overlap_cmds = _check_command_overlap(valid_cmds) + if overlap_cmds: + print("WARNING: Some commands overlap...") + print(overlap_cmds) + # FIXME(stevemar): when we determine why commands are overlapping + # we can uncomment the line below. + # return True + + # Safely return False here with the full set of commands + print("Final set of commands...") + print(valid_cmds) + print("Found no duplicate or overlapping commands, OK to merge!") + return False + + +def _check_command_overlap(valid_cmds): + """Determine if the entry point overlaps with another command. + + For example, if one plugin creates the command "object1 action", + and another plugin creates the command "object1 action object2", + the object2 command is unreachable since it overlaps the + namespace. + """ + overlap_cmds = {} + for ep_name, ep_mods in valid_cmds.iteritems(): + # Skip openstack.cli.base client entry points + for ep_mod in ep_mods: + for ep_name_search in valid_cmds.keys(): + if ep_name_search.startswith(ep_name + "_"): + overlap_cmds.setdefault(ep_name, []).append(ep_name_search) + return overlap_cmds + + +def _is_valid_command(ep_name, ep_module_name, valid_cmds): + """Determine if the entry point is valid. + + Aside from a simple check to see if the entry point short name is in our + tally, we also need to check for allowed duplicates. For instance, in the + case of supporting multiple versions, then we want to allow for duplicate + commands. Both the identity v2 and v3 APIs support `user_list`, so these + are fine. + + In order to determine if an entry point is a true duplicate we can check to + see if the module name roughly matches the module name of the entry point + that was initially added to the set of valid commands. + + The following should trigger a match:: + + openstackclient.identity.v3.user and openstackclient.identity.v*.user + + Whereas, the following should fail:: + + openstackclient.identity.v3.user and openstackclient.baremetal.v3.user + + """ + + if ep_name not in valid_cmds: + return True + else: + # there already exists an entry in the dictionary for the command... + module_parts = ep_module_name.split(".") + for valid_module_name in valid_cmds[ep_name]: + valid_module_parts = valid_module_name.split(".") + if (module_parts[0] == valid_module_parts[0] and + module_parts[1] == valid_module_parts[1] and + module_parts[3] == valid_module_parts[3]): + return True + return False + + +if __name__ == '__main__': + print("Checking 'openstack' plug-ins") + if find_duplicates(): + exit(1) + else: + exit(0)