diff --git a/setup.cfg b/setup.cfg index 4e7aebba1..b726f8d52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,10 @@ classifier = Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 +[entry_points] +console_scripts = + shade-inventory = shade.cmd.inventory:main + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/shade/__init__.py b/shade/__init__.py index 836facb39..bbe05bcbd 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -1612,6 +1612,7 @@ class OpenStackCloud(object): return self.get_openstack_vars(server) def get_server_meta(self, server): + # TODO(mordred) remove once ansible has moved to Inventory interface server_vars = meta.get_hostvars_from_server(self, server) groups = meta.get_groups_from_server(self, server, server_vars) return dict(server_vars=server_vars, groups=groups) diff --git a/shade/cmd/__init__.py b/shade/cmd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/shade/cmd/inventory.py b/shade/cmd/inventory.py new file mode 100755 index 000000000..c4d396f46 --- /dev/null +++ b/shade/cmd/inventory.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# 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 argparse +import json +import sys +import yaml + +import shade +import shade.inventory + + +def output_format_dict(data, use_yaml): + if use_yaml: + return yaml.safe_dump(data, default_flow_style=False) + else: + return json.dumps(data, sort_keys=True, indent=2) + + +def parse_args(): + parser = argparse.ArgumentParser(description='OpenStack Inventory Module') + parser.add_argument('--refresh', action='store_true', + help='Refresh cached information') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specific host') + parser.add_argument('--yaml', action='store_true', default=False, + help='Output data in nicely readable yaml') + parser.add_argument('--debug', action='store_true', default=False, + help='Enable debug output') + return parser.parse_args() + + +def main(): + args = parse_args() + try: + shade.simple_logging(debug=args.debug) + inventory = shade.inventory.OpenStackInventory( + refresh=args.refresh) + if args.list: + output = inventory.list_hosts() + elif args.host: + output = inventory.get_host(args.host) + print(output_format_dict(output, args.yaml)) + except shade.OpenStackCloudException as e: + sys.stderr.write(e.message + '\n') + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/shade/inventory.py b/shade/inventory.py new file mode 100644 index 000000000..d66808a84 --- /dev/null +++ b/shade/inventory.py @@ -0,0 +1,61 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# 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_client_config + +import shade +from shade import _utils + + +class OpenStackInventory(object): + + def __init__( + self, config_files=[], refresh=False): + config = os_client_config.config.OpenStackConfig( + config_files=os_client_config.config.CONFIG_FILES + config_files) + + self.clouds = [ + shade.OpenStackCloud( + cloud=f.name, + cache_interval=config.get_cache_max_age(), + cache_class=config.get_cache_class(), + cache_arguments=config.get_cache_arguments(), + **f.config) + for f in config.get_all_clouds() + ] + + # Handle manual invalidation of entire persistent cache + if refresh: + for cloud in self.clouds: + cloud._cache.invalidate() + + def list_hosts(self): + hostvars = [] + + for cloud in self.clouds: + + # Cycle on servers + for server in cloud.list_servers(): + + meta = cloud.get_openstack_vars(server) + hostvars.append(meta) + + return hostvars + + def search_hosts(self, name_or_id=None, filters=None): + hosts = self.list_hosts() + return _utils._filter_list(hosts, name_or_id, filters) + + def get_host(self, name_or_id, filters=None): + return _utils._get_entity(self.search_hosts, name_or_id, filters) diff --git a/shade/tests/functional/test_inventory.py b/shade/tests/functional/test_inventory.py new file mode 100644 index 000000000..3a4bfcdb8 --- /dev/null +++ b/shade/tests/functional/test_inventory.py @@ -0,0 +1,73 @@ +# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. +# +# 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. + +""" +test_inventory +---------------------------------- + +Functional tests for `shade` inventory methods. +""" + +from shade import openstack_cloud +from shade import inventory + +from shade.tests import base +from shade.tests.functional.util import pick_flavor, pick_image + + +class TestInventory(base.TestCase): + def setUp(self): + super(TestInventory, self).setUp() + # Shell should have OS-* envvars from openrc, typically loaded by job + self.cloud = openstack_cloud() + self.inventory = inventory.OpenStackInventory() + self.server_name = 'test_inventory_server' + self.nova = self.cloud.nova_client + self.flavor = pick_flavor(self.nova.flavors.list()) + if self.flavor is None: + self.assertTrue(False, 'no sensible flavor available') + self.image = pick_image(self.nova.images.list()) + if self.image is None: + self.assertTrue(False, 'no sensible image available') + self.addCleanup(self._cleanup_servers) + self.cloud.create_server( + name=self.server_name, image=self.image, flavor=self.flavor, + wait=True, auto_ip=True) + + def _cleanup_servers(self): + for i in self.nova.servers.list(): + if i.name.startswith(self.server_name): + self.nova.servers.delete(i) + + def _test_host_content(self, host): + self.assertEquals(host['image']['id'], self.image.id) + self.assertNotIn('links', host['image']) + self.assertEquals(host['flavor']['id'], self.flavor.id) + self.assertNotIn('links', host['flavor']) + self.assertNotIn('links', host) + self.assertIsInstance(host['volumes'], list) + self.assertIsInstance(host['metadata'], dict) + self.assertIn('interface_ip', host) + + def test_get_host(self): + host = self.inventory.get_host(self.server_name) + self.assertIsNotNone(host) + self.assertEquals(host['name'], self.server_name) + self._test_host_content(host) + host_found = False + for host in self.inventory.list_hosts(): + if host['name'] == self.server_name: + host_found = True + self._test_host_content(host) + self.assertTrue(host_found)