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