From 9fe31995c540bc3a3dfe64cace8131ad27b3bffa Mon Sep 17 00:00:00 2001 From: Ben Nemec Date: Thu, 14 Jul 2016 12:23:41 -0500 Subject: [PATCH] Reorganize into package and add tox for testing Moves the functional code into an openstack_virtual_baremetal env and adds a tox configuration for testing. Existing unit tests for deploy.py are moved into the tests subpackage. Further unit tests for the other modules will be added in followup commits. Symlinks from the bin directory are left so the previous workflow should continue to work as before. --- .gitignore | 7 + .testr.conf | 4 + bin/build-nodes-json | 142 +------------ bin/deploy.py | 157 +-------------- bin/openstackbmc | 185 +---------------- openstack_virtual_baremetal/__init__.py | 0 .../build_nodes_json.py | 171 ++++++++++++++++ openstack_virtual_baremetal/deploy.py | 156 +++++++++++++++ openstack_virtual_baremetal/openstackbmc.py | 188 ++++++++++++++++++ openstack_virtual_baremetal/tests/__init__.py | 0 .../tests/test_deploy.py | 0 requirements.txt | 5 + setup.cfg | 33 +++ setup.py | 22 ++ test-requirements.txt | 7 + tox.ini | 29 +++ 16 files changed, 625 insertions(+), 481 deletions(-) create mode 100644 .testr.conf mode change 100755 => 120000 bin/build-nodes-json mode change 100755 => 120000 bin/deploy.py mode change 100755 => 120000 bin/openstackbmc create mode 100644 openstack_virtual_baremetal/__init__.py create mode 100755 openstack_virtual_baremetal/build_nodes_json.py create mode 100755 openstack_virtual_baremetal/deploy.py create mode 100755 openstack_virtual_baremetal/openstackbmc.py create mode 100644 openstack_virtual_baremetal/tests/__init__.py rename bin/test-deploy => openstack_virtual_baremetal/tests/test_deploy.py (100%) create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index dde06be..dbec74f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ nodes.json env.yaml bmc_bm_pairs +*.pyc +*.pyo +.coverage +cover +.testrepository +.tox +*.egg-info diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..c52af66 --- /dev/null +++ b/.testr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 OS_LOG_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./openstack_virtual_baremetal ./openstack_virtual_baremetal $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/bin/build-nodes-json b/bin/build-nodes-json deleted file mode 100755 index 8ba7d41..0000000 --- a/bin/build-nodes-json +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python -# Copyright 2015 Red Hat Inc. -# -# 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 os -import sys -import yaml - -from neutronclient.v2_0 import client as neutronclient -from novaclient import client as novaclient - -def main(): - parser = argparse.ArgumentParser( - prog='build-nodes-json.py', - description='Tool for collecting virtual IPMI details', - ) - parser.add_argument('--env', - dest='env', - default=None, - help='YAML file containing OVB environment details') - parser.add_argument('--bmc_prefix', - dest='bmc_prefix', - default='bmc', - help='BMC name prefix') - parser.add_argument('--baremetal_prefix', - dest='baremetal_prefix', - default='baremetal', - help='Baremetal name prefix') - parser.add_argument('--private_net', - dest='private_net', - default='private', - help='Private network name') - parser.add_argument('--provision_net', - dest='provision_net', - default='provision', - help='Provisioning network name') - parser.add_argument('--nodes_json', - dest='nodes_json', - default='nodes.json', - help='Destination to store the nodes json file to') - args = parser.parse_args() - - if args.env is None: - bmc_base = args.bmc_prefix - baremetal_base = args.baremetal_prefix - private_net = args.private_net - provision_net = args.provision_net - else: - with open(args.env) as f: - e = yaml.safe_load(f) - bmc_base = e['parameters']['bmc_prefix'] - baremetal_base = e['parameters']['baremetal_prefix'] - private_net = e['parameters']['private_net'] - provision_net = e['parameters']['provision_net'] - - cloud = os.environ.get('OS_CLOUD') - if cloud: - import os_client_config - nova = os_client_config.make_client('compute', cloud=cloud) - neutron = os_client_config.make_client('network', cloud=cloud) - - else: - username = os.environ.get('OS_USERNAME') - password = os.environ.get('OS_PASSWORD') - tenant = os.environ.get('OS_TENANT_NAME') - auth_url = os.environ.get('OS_AUTH_URL') - if not username or not password or not tenant or not auth_url: - print('Source an appropriate rc file first') - sys.exit(1) - - nova = novaclient.Client(2, username, password, tenant, auth_url) - neutron = neutronclient.Client( - username=username, - password=password, - tenant_name=tenant, - auth_url=auth_url - ) - node_template = { - 'pm_type': 'pxe_ipmitool', - 'mac': '', - 'cpu': '', - 'memory': '', - 'disk': '', - 'arch': 'x86_64', - 'pm_user': 'admin', - 'pm_password': 'password', - 'pm_addr': '', - 'capabilities': 'boot_option:local', - } - - all_ports = sorted(neutron.list_ports()['ports'], key=lambda x: x['name']) - bmc_ports = list([p for p in all_ports - if p['name'].startswith(bmc_base)]) - bm_ports = list([p for p in all_ports - if p['name'].startswith(baremetal_base)]) - if len(bmc_ports) != len(bm_ports): - raise RuntimeError('Found different numbers of baremetal and ' - 'bmc ports.') - nodes = [] - bmc_bm_pairs = [] - - for bmc_port, baremetal_port in zip(bmc_ports, bm_ports): - baremetal = nova.servers.get(baremetal_port['device_id']) - node = dict(node_template) - node['pm_addr'] = bmc_port['fixed_ips'][0]['ip_address'] - bmc_bm_pairs.append((node['pm_addr'], baremetal.name)) - node['mac'] = [baremetal.addresses[provision_net][0]['OS-EXT-IPS-MAC:mac_addr']] - flavor = nova.flavors.get(baremetal.flavor['id']) - node['cpu'] = flavor.vcpus - node['memory'] = flavor.ram - node['disk'] = flavor.disk - nodes.append(node) - - with open(args.nodes_json, 'w') as node_file: - contents = json.dumps({'nodes': nodes}, indent=2) - node_file.write(contents) - print(contents) - - with open('bmc_bm_pairs', 'w') as pairs_file: - pairs_file.write('# A list of BMC addresses and the name of the ' - 'instance that BMC manages.\n') - for i in bmc_bm_pairs: - pair = '%s %s' % i - pairs_file.write(pair + '\n') - print(pair) - -if __name__ == '__main__': - main() diff --git a/bin/build-nodes-json b/bin/build-nodes-json new file mode 120000 index 0000000..c679e77 --- /dev/null +++ b/bin/build-nodes-json @@ -0,0 +1 @@ +../openstack_virtual_baremetal/build_nodes_json.py \ No newline at end of file diff --git a/bin/deploy.py b/bin/deploy.py deleted file mode 100755 index bcf5960..0000000 --- a/bin/deploy.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python -# Copyright 2016 Red Hat Inc. -# -# 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 os -import random -import sys -import yaml - -from heatclient import client as heat_client -from heatclient.common import template_utils -from keystoneclient.v2_0 import client as keystone_client - -def _parse_args(): - parser = argparse.ArgumentParser(description='Deploy an OVB environment') - parser.add_argument('--env', - help='Path to Heat environment file describing the OVB ' - 'environment to be deployed. Default: %(default)s', - default='env.yaml') - parser.add_argument('--id', - help='Identifier to add to all resource names. The ' - 'resulting names will look like undercloud-ID or ' - 'baremetal-ID. By default no changes will be made to ' - 'the resource names. If an id is specified, a new ' - 'environment file will be written to env-ID.yaml. ') - parser.add_argument('--name', - help='Name for the Heat stack to be created. Defaults ' - 'to "baremetal" in a standard deployment. If ' - '--quintupleo is specified then the default is ' - '"quintupleo".') - parser.add_argument('--quintupleo', - help='Deploy a full environment suitable for TripleO ' - 'development.', - action='store_true', - default=False) - return parser.parse_args() - -def _process_args(args): - if args.id and not args.quintupleo: - raise RuntimeError('--id requires --quintupleo') - - env_path = args.env - if args.name: - stack_name = args.name - else: - stack_name = 'baremetal' - if args.quintupleo: - stack_name = 'quintupleo' - if not args.quintupleo: - stack_template = 'templates/virtual-baremetal.yaml' - else: - stack_template = 'templates/quintupleo.yaml' - return stack_name, stack_template - -def _add_identifier(env_data, name, identifier, default=None, parameter=True): - param_key = 'parameters' - if not parameter: - param_key = 'parameter_defaults' - if param_key not in env_data or not env_data[param_key]: - env_data[param_key] = {} - original = env_data[param_key].get(name) - if original is None: - original = default - if original is None: - raise RuntimeError('No base value found when adding id') - env_data[param_key][name] = '%s-%s' % (original, identifier) - -def _generate_id_env(args): - with open(args.env) as f: - env_data = yaml.safe_load(f) - _add_identifier(env_data, 'provision_net', args.id, default='provision') - _add_identifier(env_data, 'public_net', args.id, default='public') - _add_identifier(env_data, 'baremetal_prefix', args.id, default='baremetal') - _add_identifier(env_data, 'bmc_prefix', args.id, default='bmc') - _add_identifier(env_data, 'undercloud_name', args.id, default='undercloud') - _add_identifier(env_data, 'overcloud_internal_net', args.id, - default='internal', parameter=False) - _add_identifier(env_data, 'overcloud_storage_net', args.id, - default='storage', parameter=False) - _add_identifier(env_data, 'overcloud_storage_mgmt_net', args.id, - default='storage_mgmt', parameter=False) - _add_identifier(env_data, 'overcloud_tenant_net', args.id, - default='tenant', parameter=False) - env_path = 'env-%s.yaml' % args.id - with open(env_path, 'w') as f: - yaml.safe_dump(env_data, f, default_flow_style=False) - return env_path - -def _get_heat_client(): - cloud = os.environ.get('OS_CLOUD') - if cloud: - import os_client_config - return os_client_config.make_client('orchestration', cloud=cloud) - else: - username = os.environ.get('OS_USERNAME') - password = os.environ.get('OS_PASSWORD') - tenant = os.environ.get('OS_TENANT_NAME') - auth_url = os.environ.get('OS_AUTH_URL') - if not username or not password or not tenant or not auth_url: - print('Source an appropriate rc file first') - sys.exit(1) - - # Get token for Heat to use - kclient = keystone_client.Client(username=username, password=password, - tenant_name=tenant, auth_url=auth_url) - token_data = kclient.get_raw_token_from_identity_service( - username=username, - password=password, - tenant_name=tenant, - auth_url=auth_url) - token_id = token_data['token']['id'] - # Get Heat endpoint - for endpoint in token_data['serviceCatalog']: - if endpoint['name'] == 'heat': - # TODO: What if there's more than one endpoint? - heat_endpoint = endpoint['endpoints'][0]['publicURL'] - - return heat_client.Client('1', endpoint=heat_endpoint, token=token_id) - -def _deploy(stack_name, stack_template, env_path): - hclient = _get_heat_client() - - template_files, template = template_utils.get_template_contents( - stack_template) - env_files, env = template_utils.process_multiple_environments_and_files( - ['templates/resource-registry.yaml', env_path]) - all_files = {} - all_files.update(template_files) - all_files.update(env_files) - - hclient.stacks.create(stack_name=stack_name, - template=template, - environment=env, - files=all_files) - - print 'Deployment of stack "%s" started.' % stack_name - -if __name__ == '__main__': - args = _parse_args() - env_path = args.env - stack_name, stack_template = _process_args(args) - if args.id: - env_path = _generate_id_env(args) - _deploy(stack_name, stack_template, env_path) diff --git a/bin/deploy.py b/bin/deploy.py new file mode 120000 index 0000000..cc3b502 --- /dev/null +++ b/bin/deploy.py @@ -0,0 +1 @@ +../openstack_virtual_baremetal/deploy.py \ No newline at end of file diff --git a/bin/openstackbmc b/bin/openstackbmc deleted file mode 100755 index 6a23a40..0000000 --- a/bin/openstackbmc +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python -# Copyright 2015 Red Hat, Inc. -# Copyright 2015 Lenovo -# -# 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. - -# Virtual BMC for controlling OpenStack instances, based on fakebmc from -# python-pyghmi - -# Sample ipmitool commands: -# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on -# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status -# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootdev pxe|disk -# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 mc reset cold - -import argparse -import sys -import time - -from novaclient import client as novaclient -from novaclient import exceptions -import pyghmi.ipmi.bmc as bmc - - -class OpenStackBmc(bmc.Bmc): - def __init__(self, authdata, port, address, instance, user, password, tenant, - auth_url): - super(OpenStackBmc, self).__init__(authdata, port=port, address=address) - self.novaclient = novaclient.Client(2, user, password, - tenant, auth_url) - self.instance = None - # At times the bmc service is started before important things like - # networking have fully initialized. Keep trying to find the - # instance indefinitely, since there's no point in continuing if - # we don't have an instance. - while True: - try: - self._find_instance(instance) - if self.instance is not None: - name = self.novaclient.servers.get(self.instance).name - self.log('Managing instance: %s UUID: %s' % - (name, self.instance)) - break - except Exception as e: - self.log('Exception finding instance "%s": %s' % (instance, e)) - time.sleep(1) - - def _find_instance(self, instance): - try: - self.novaclient.servers.get(instance) - self.instance = instance - except exceptions.NotFound: - name_regex = '^%s$' % instance - i = self.novaclient.servers.list(search_opts={'name': name_regex}) - if len(i) > 1: - self.log('Ambiguous instance name %s' % instance) - sys.exit(1) - try: - self.instance = i[0].id - except IndexError: - self.log('Could not find specified instance %s' % instance) - sys.exit(1) - - def get_boot_device(self): - server = self.novaclient.servers.get(self.instance) - retval = 'network' if server.metadata.get('libvirt:pxe-first') else 'hd' - self.log('Reporting boot device', retval) - return retval - - def set_boot_device(self, bootdevice): - server = self.novaclient.servers.get(self.instance) - if bootdevice == 'network': - self.novaclient.servers.set_meta_item(server, 'libvirt:pxe-first', '1') - else: - self.novaclient.servers.set_meta_item(server, 'libvirt:pxe-first', '') - self.log('Set boot device to', bootdevice) - - def cold_reset(self): - # Reset of the BMC, not managed system, here we will exit the demo - self.log('Shutting down in response to BMC cold reset request') - sys.exit(0) - - def _instance_active(self): - return self.novaclient.servers.get(self.instance).status == 'ACTIVE' - - def get_power_state(self): - self.log('Getting power state for %s' % self.instance) - return self._instance_active() - - def power_off(self): - # this should be power down without waiting for clean shutdown - if self._instance_active(): - try: - self.novaclient.servers.stop(self.instance) - self.log('Powered off %s' % self.instance) - except exceptions.Conflict as e: - # This can happen if we get two requests to start a server in - # short succession. The instance may then be in a powering-on - # state, which means it is invalid to start it again. - self.log('Ignoring exception: "%s"' % e) - else: - self.log('%s is already off.' % self.instance) - return 0xd5 - - def power_on(self): - if not self._instance_active(): - try: - self.novaclient.servers.start(self.instance) - self.log('Powered on %s' % self.instance) - except exceptions.Conflict as e: - # This can happen if we get two requests to start a server in - # short succession. The instance may then be in a powering-on - # state, which means it is invalid to start it again. - self.log('Ignoring exception: "%s"' % e) - else: - self.log('%s is already on.' % self.instance) - return 0xd5 - - def power_reset(self): - pass - - def power_shutdown(self): - # should attempt a clean shutdown - self.novaclient.servers.stop(self.instance) - self.log('Politely shut down %s' % self.instance) - - def log(self, *msg): - print(' '.join(msg)) - sys.stdout.flush() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - prog='openstackbmc', - description='Virtual BMC for controlling OpenStack instance', - ) - parser.add_argument('--port', - dest='port', - type=int, - default=623, - help='Port to listen on; defaults to 623') - parser.add_argument('--address', - dest='address', - default='::', - help='Address to bind to; defaults to ::') - parser.add_argument('--instance', - dest='instance', - required=True, - help='The uuid or name of the OpenStack instance to manage') - parser.add_argument('--os-user', - dest='user', - required=True, - help='The user for connecting to OpenStack') - parser.add_argument('--os-password', - dest='password', - required=True, - help='The password for connecting to OpenStack') - parser.add_argument('--os-tenant', - dest='tenant', - required=True, - help='The tenant for connecting to OpenStack') - parser.add_argument('--os-auth-url', - dest='auth_url', - required=True, - help='The OpenStack Keystone auth url') - args = parser.parse_args() - mybmc = OpenStackBmc({'admin': 'password'}, port=args.port, - address='::ffff:%s' % args.address, - instance=args.instance, - user=args.user, - password=args.password, - tenant=args.tenant, - auth_url=args.auth_url) - mybmc.listen() diff --git a/bin/openstackbmc b/bin/openstackbmc new file mode 120000 index 0000000..d46f9a2 --- /dev/null +++ b/bin/openstackbmc @@ -0,0 +1 @@ +../openstack_virtual_baremetal/openstackbmc.py \ No newline at end of file diff --git a/openstack_virtual_baremetal/__init__.py b/openstack_virtual_baremetal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openstack_virtual_baremetal/build_nodes_json.py b/openstack_virtual_baremetal/build_nodes_json.py new file mode 100755 index 0000000..01fbe4b --- /dev/null +++ b/openstack_virtual_baremetal/build_nodes_json.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# Copyright 2015 Red Hat Inc. +# +# 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 os +import sys +import yaml + +from neutronclient.v2_0 import client as neutronclient +from novaclient import client as novaclient + + +def _parse_args(): + parser = argparse.ArgumentParser( + prog='build-nodes-json.py', + description='Tool for collecting virtual IPMI details', + ) + parser.add_argument('--env', + dest='env', + default=None, + help='YAML file containing OVB environment details') + parser.add_argument('--bmc_prefix', + dest='bmc_prefix', + default='bmc', + help='BMC name prefix') + parser.add_argument('--baremetal_prefix', + dest='baremetal_prefix', + default='baremetal', + help='Baremetal name prefix') + parser.add_argument('--private_net', + dest='private_net', + default='private', + help='DEPRECATED: This parameter is ignored.') + parser.add_argument('--provision_net', + dest='provision_net', + default='provision', + help='Provisioning network name') + parser.add_argument('--nodes_json', + dest='nodes_json', + default='nodes.json', + help='Destination to store the nodes json file to') + args = parser.parse_args() + return args + + +def _get_names(args): + if args.env is None: + bmc_base = args.bmc_prefix + baremetal_base = args.baremetal_prefix + provision_net = args.provision_net + else: + with open(args.env) as f: + e = yaml.safe_load(f) + bmc_base = e['parameters']['bmc_prefix'] + baremetal_base = e['parameters']['baremetal_prefix'] + provision_net = e['parameters']['provision_net'] + return bmc_base, baremetal_base, provision_net + + +def _get_clients(): + cloud = os.environ.get('OS_CLOUD') + if cloud: + import os_client_config + nova = os_client_config.make_client('compute', cloud=cloud) + neutron = os_client_config.make_client('network', cloud=cloud) + + else: + username = os.environ.get('OS_USERNAME') + password = os.environ.get('OS_PASSWORD') + tenant = os.environ.get('OS_TENANT_NAME') + auth_url = os.environ.get('OS_AUTH_URL') + if not username or not password or not tenant or not auth_url: + print('Source an appropriate rc file first') + sys.exit(1) + + nova = novaclient.Client(2, username, password, tenant, auth_url) + neutron = neutronclient.Client( + username=username, + password=password, + tenant_name=tenant, + auth_url=auth_url + ) + return nova, neutron + + +def _get_ports(neutron, bmc_base, baremetal_base): + all_ports = sorted(neutron.list_ports()['ports'], key=lambda x: x['name']) + bmc_ports = list([p for p in all_ports + if p['name'].startswith(bmc_base)]) + bm_ports = list([p for p in all_ports + if p['name'].startswith(baremetal_base)]) + if len(bmc_ports) != len(bm_ports): + raise RuntimeError('Found different numbers of baremetal and ' + 'bmc ports.') + return bmc_ports, bm_ports + + +def _build_nodes(nova, bmc_ports, bm_ports, provision_net): + node_template = { + 'pm_type': 'pxe_ipmitool', + 'mac': '', + 'cpu': '', + 'memory': '', + 'disk': '', + 'arch': 'x86_64', + 'pm_user': 'admin', + 'pm_password': 'password', + 'pm_addr': '', + 'capabilities': 'boot_option:local', + } + + nodes = [] + bmc_bm_pairs = [] + + for bmc_port, baremetal_port in zip(bmc_ports, bm_ports): + baremetal = nova.servers.get(baremetal_port['device_id']) + node = dict(node_template) + node['pm_addr'] = bmc_port['fixed_ips'][0]['ip_address'] + bmc_bm_pairs.append((node['pm_addr'], baremetal.name)) + node['mac'] = [baremetal.addresses[provision_net][0]['OS-EXT-IPS-MAC:mac_addr']] + flavor = nova.flavors.get(baremetal.flavor['id']) + node['cpu'] = flavor.vcpus + node['memory'] = flavor.ram + node['disk'] = flavor.disk + nodes.append(node) + return nodes, bmc_bm_pairs + + +def _write_nodes(nodes, args): + with open(args.nodes_json, 'w') as node_file: + contents = json.dumps({'nodes': nodes}, indent=2) + node_file.write(contents) + print(contents) + + +# TODO(bnemec): parameterize this based on args.nodes_json +def _write_pairs(bmc_bm_pairs): + with open('bmc_bm_pairs', 'w') as pairs_file: + pairs_file.write('# A list of BMC addresses and the name of the ' + 'instance that BMC manages.\n') + for i in bmc_bm_pairs: + pair = '%s %s' % i + pairs_file.write(pair + '\n') + print(pair) + + +def main(): + args = _parse_args() + bmc_base, baremetal_base, provision_net = _get_names(args) + nova, neutron = _get_clients() + bmc_ports, bm_ports = _get_ports(neutron, bmc_base, baremetal_base) + nodes, bmc_bm_pairs = _build_nodes(nova, bmc_ports, bm_ports, provision_net) + _write_nodes(nodes, args) + _write_pairs(bmc_bm_pairs) + + +if __name__ == '__main__': + main() diff --git a/openstack_virtual_baremetal/deploy.py b/openstack_virtual_baremetal/deploy.py new file mode 100755 index 0000000..bcf5960 --- /dev/null +++ b/openstack_virtual_baremetal/deploy.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# Copyright 2016 Red Hat Inc. +# +# 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 os +import random +import sys +import yaml + +from heatclient import client as heat_client +from heatclient.common import template_utils +from keystoneclient.v2_0 import client as keystone_client + +def _parse_args(): + parser = argparse.ArgumentParser(description='Deploy an OVB environment') + parser.add_argument('--env', + help='Path to Heat environment file describing the OVB ' + 'environment to be deployed. Default: %(default)s', + default='env.yaml') + parser.add_argument('--id', + help='Identifier to add to all resource names. The ' + 'resulting names will look like undercloud-ID or ' + 'baremetal-ID. By default no changes will be made to ' + 'the resource names. If an id is specified, a new ' + 'environment file will be written to env-ID.yaml. ') + parser.add_argument('--name', + help='Name for the Heat stack to be created. Defaults ' + 'to "baremetal" in a standard deployment. If ' + '--quintupleo is specified then the default is ' + '"quintupleo".') + parser.add_argument('--quintupleo', + help='Deploy a full environment suitable for TripleO ' + 'development.', + action='store_true', + default=False) + return parser.parse_args() + +def _process_args(args): + if args.id and not args.quintupleo: + raise RuntimeError('--id requires --quintupleo') + + env_path = args.env + if args.name: + stack_name = args.name + else: + stack_name = 'baremetal' + if args.quintupleo: + stack_name = 'quintupleo' + if not args.quintupleo: + stack_template = 'templates/virtual-baremetal.yaml' + else: + stack_template = 'templates/quintupleo.yaml' + return stack_name, stack_template + +def _add_identifier(env_data, name, identifier, default=None, parameter=True): + param_key = 'parameters' + if not parameter: + param_key = 'parameter_defaults' + if param_key not in env_data or not env_data[param_key]: + env_data[param_key] = {} + original = env_data[param_key].get(name) + if original is None: + original = default + if original is None: + raise RuntimeError('No base value found when adding id') + env_data[param_key][name] = '%s-%s' % (original, identifier) + +def _generate_id_env(args): + with open(args.env) as f: + env_data = yaml.safe_load(f) + _add_identifier(env_data, 'provision_net', args.id, default='provision') + _add_identifier(env_data, 'public_net', args.id, default='public') + _add_identifier(env_data, 'baremetal_prefix', args.id, default='baremetal') + _add_identifier(env_data, 'bmc_prefix', args.id, default='bmc') + _add_identifier(env_data, 'undercloud_name', args.id, default='undercloud') + _add_identifier(env_data, 'overcloud_internal_net', args.id, + default='internal', parameter=False) + _add_identifier(env_data, 'overcloud_storage_net', args.id, + default='storage', parameter=False) + _add_identifier(env_data, 'overcloud_storage_mgmt_net', args.id, + default='storage_mgmt', parameter=False) + _add_identifier(env_data, 'overcloud_tenant_net', args.id, + default='tenant', parameter=False) + env_path = 'env-%s.yaml' % args.id + with open(env_path, 'w') as f: + yaml.safe_dump(env_data, f, default_flow_style=False) + return env_path + +def _get_heat_client(): + cloud = os.environ.get('OS_CLOUD') + if cloud: + import os_client_config + return os_client_config.make_client('orchestration', cloud=cloud) + else: + username = os.environ.get('OS_USERNAME') + password = os.environ.get('OS_PASSWORD') + tenant = os.environ.get('OS_TENANT_NAME') + auth_url = os.environ.get('OS_AUTH_URL') + if not username or not password or not tenant or not auth_url: + print('Source an appropriate rc file first') + sys.exit(1) + + # Get token for Heat to use + kclient = keystone_client.Client(username=username, password=password, + tenant_name=tenant, auth_url=auth_url) + token_data = kclient.get_raw_token_from_identity_service( + username=username, + password=password, + tenant_name=tenant, + auth_url=auth_url) + token_id = token_data['token']['id'] + # Get Heat endpoint + for endpoint in token_data['serviceCatalog']: + if endpoint['name'] == 'heat': + # TODO: What if there's more than one endpoint? + heat_endpoint = endpoint['endpoints'][0]['publicURL'] + + return heat_client.Client('1', endpoint=heat_endpoint, token=token_id) + +def _deploy(stack_name, stack_template, env_path): + hclient = _get_heat_client() + + template_files, template = template_utils.get_template_contents( + stack_template) + env_files, env = template_utils.process_multiple_environments_and_files( + ['templates/resource-registry.yaml', env_path]) + all_files = {} + all_files.update(template_files) + all_files.update(env_files) + + hclient.stacks.create(stack_name=stack_name, + template=template, + environment=env, + files=all_files) + + print 'Deployment of stack "%s" started.' % stack_name + +if __name__ == '__main__': + args = _parse_args() + env_path = args.env + stack_name, stack_template = _process_args(args) + if args.id: + env_path = _generate_id_env(args) + _deploy(stack_name, stack_template, env_path) diff --git a/openstack_virtual_baremetal/openstackbmc.py b/openstack_virtual_baremetal/openstackbmc.py new file mode 100755 index 0000000..70378b3 --- /dev/null +++ b/openstack_virtual_baremetal/openstackbmc.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# Copyright 2015 Red Hat, Inc. +# Copyright 2015 Lenovo +# +# 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. + +# Virtual BMC for controlling OpenStack instances, based on fakebmc from +# python-pyghmi + +# Sample ipmitool commands: +# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power on +# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 power status +# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 chassis bootdev pxe|disk +# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 mc reset cold + +import argparse +import sys +import time + +from novaclient import client as novaclient +from novaclient import exceptions +import pyghmi.ipmi.bmc as bmc + + +class OpenStackBmc(bmc.Bmc): + def __init__(self, authdata, port, address, instance, user, password, tenant, + auth_url): + super(OpenStackBmc, self).__init__(authdata, port=port, address=address) + self.novaclient = novaclient.Client(2, user, password, + tenant, auth_url) + self.instance = None + # At times the bmc service is started before important things like + # networking have fully initialized. Keep trying to find the + # instance indefinitely, since there's no point in continuing if + # we don't have an instance. + while True: + try: + self.instance = self._find_instance(instance) + if self.instance is not None: + name = self.novaclient.servers.get(self.instance).name + self.log('Managing instance: %s UUID: %s' % + (name, self.instance)) + break + except Exception as e: + self.log('Exception finding instance "%s": %s' % (instance, e)) + time.sleep(1) + + def _find_instance(self, instance): + try: + self.novaclient.servers.get(instance) + return instance + except exceptions.NotFound: + name_regex = '^%s$' % instance + i = self.novaclient.servers.list(search_opts={'name': name_regex}) + if len(i) > 1: + self.log('Ambiguous instance name %s' % instance) + sys.exit(1) + try: + return i[0].id + except IndexError: + self.log('Could not find specified instance %s' % instance) + sys.exit(1) + + def get_boot_device(self): + server = self.novaclient.servers.get(self.instance) + retval = 'network' if server.metadata.get('libvirt:pxe-first') else 'hd' + self.log('Reporting boot device', retval) + return retval + + def set_boot_device(self, bootdevice): + server = self.novaclient.servers.get(self.instance) + if bootdevice == 'network': + self.novaclient.servers.set_meta_item(server, 'libvirt:pxe-first', '1') + else: + self.novaclient.servers.set_meta_item(server, 'libvirt:pxe-first', '') + self.log('Set boot device to', bootdevice) + + def cold_reset(self): + # Reset of the BMC, not managed system, here we will exit the demo + self.log('Shutting down in response to BMC cold reset request') + sys.exit(0) + + def _instance_active(self): + return self.novaclient.servers.get(self.instance).status == 'ACTIVE' + + def get_power_state(self): + self.log('Getting power state for %s' % self.instance) + return self._instance_active() + + def power_off(self): + # this should be power down without waiting for clean shutdown + if self._instance_active(): + try: + self.novaclient.servers.stop(self.instance) + self.log('Powered off %s' % self.instance) + except exceptions.Conflict as e: + # This can happen if we get two requests to start a server in + # short succession. The instance may then be in a powering-on + # state, which means it is invalid to start it again. + self.log('Ignoring exception: "%s"' % e) + else: + self.log('%s is already off.' % self.instance) + return 0xd5 + + def power_on(self): + if not self._instance_active(): + try: + self.novaclient.servers.start(self.instance) + self.log('Powered on %s' % self.instance) + except exceptions.Conflict as e: + # This can happen if we get two requests to start a server in + # short succession. The instance may then be in a powering-on + # state, which means it is invalid to start it again. + self.log('Ignoring exception: "%s"' % e) + else: + self.log('%s is already on.' % self.instance) + return 0xd5 + + def power_reset(self): + pass + + def power_shutdown(self): + # should attempt a clean shutdown + self.novaclient.servers.stop(self.instance) + self.log('Politely shut down %s' % self.instance) + + def log(self, *msg): + print(' '.join(msg)) + sys.stdout.flush() + + +def main(): + parser = argparse.ArgumentParser( + prog='openstackbmc', + description='Virtual BMC for controlling OpenStack instance', + ) + parser.add_argument('--port', + dest='port', + type=int, + default=623, + help='Port to listen on; defaults to 623') + parser.add_argument('--address', + dest='address', + default='::', + help='Address to bind to; defaults to ::') + parser.add_argument('--instance', + dest='instance', + required=True, + help='The uuid or name of the OpenStack instance to manage') + parser.add_argument('--os-user', + dest='user', + required=True, + help='The user for connecting to OpenStack') + parser.add_argument('--os-password', + dest='password', + required=True, + help='The password for connecting to OpenStack') + parser.add_argument('--os-tenant', + dest='tenant', + required=True, + help='The tenant for connecting to OpenStack') + parser.add_argument('--os-auth-url', + dest='auth_url', + required=True, + help='The OpenStack Keystone auth url') + args = parser.parse_args() + mybmc = OpenStackBmc({'admin': 'password'}, port=args.port, + address='::ffff:%s' % args.address, + instance=args.instance, + user=args.user, + password=args.password, + tenant=args.tenant, + auth_url=args.auth_url) + mybmc.listen() + + +if __name__ == '__main__': + main() diff --git a/openstack_virtual_baremetal/tests/__init__.py b/openstack_virtual_baremetal/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bin/test-deploy b/openstack_virtual_baremetal/tests/test_deploy.py similarity index 100% rename from bin/test-deploy rename to openstack_virtual_baremetal/tests/test_deploy.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5a14959 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pyghmi +PyYAML +python-heatclient +python-keystoneclient +python-novaclient diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2219c31 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +[metadata] +name = openstack-virtual-baremetal +summary = A collection of tools for using OpenStack instances to test baremetal deployment +description-file = + README.rst +author = Ben Nemec +author-email = bnemec@redhat.com +home-page = http://www.redhat.com/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + +[files] +packages = + openstack_virtual_baremetal + +#[entry_points] +#console_scripts = +# dlrn-repo = dlrn_repo.main:main + + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bdb0471 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# Copyright (c) 2013 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..f182620 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,7 @@ +coverage>=3.6 +discover +fixtures>=0.3.14 +python-subunit>=0.0.18 +testrepository>=0.0.18 +testtools>=0.9.36,!=1.2.0 +mock>=1.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ea45aee --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +minversion = 1.6 +skipsdist = True +envlist = py34,py27,pep8 + +[testenv] +usedevelop = True +setenv = VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt +commands = python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:venv] +commands = {posargs} + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:pep8] +deps = flake8 +commands = flake8 + +[testenv:cover] +commands = python setup.py test --coverage --coverage-package-name=openstack_virtual_baremetal --testr-args='{posargs}' + +[flake8] +ignore = H803 +show-source = True +exclude = .tox,dist,doc,*.egg,build