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.
This commit is contained in:
Ben Nemec 2016-07-14 12:23:41 -05:00
parent 701a42c804
commit 9fe31995c5
16 changed files with 625 additions and 481 deletions

7
.gitignore vendored
View File

@ -1,3 +1,10 @@
nodes.json
env.yaml
bmc_bm_pairs
*.pyc
*.pyo
.coverage
cover
.testrepository
.tox
*.egg-info

4
.testr.conf Normal file
View File

@ -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

View File

@ -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()

1
bin/build-nodes-json Symbolic link
View File

@ -0,0 +1 @@
../openstack_virtual_baremetal/build_nodes_json.py

View File

@ -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)

1
bin/deploy.py Symbolic link
View File

@ -0,0 +1 @@
../openstack_virtual_baremetal/deploy.py

View File

@ -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()

1
bin/openstackbmc Symbolic link
View File

@ -0,0 +1 @@
../openstack_virtual_baremetal/openstackbmc.py

View File

View File

@ -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()

View File

@ -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)

View File

@ -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()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
pyghmi
PyYAML
python-heatclient
python-keystoneclient
python-novaclient

33
setup.cfg Normal file
View File

@ -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

22
setup.py Normal file
View File

@ -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)

7
test-requirements.txt Normal file
View File

@ -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

29
tox.ini Normal file
View File

@ -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