Support keystone v3

Make all the various bits of OVB that interact with the host cloud
able to use keystone v3 authentication.

Note that my experience has been that a newer novaclient is needed
on the bmc in order for this to work.  The pre-built BMC image in
the docs has been updated with a new enough novaclient.
This commit is contained in:
Ben Nemec 2017-03-03 10:26:54 -06:00
parent 43af75766c
commit 7e7ccea3dc
9 changed files with 391 additions and 49 deletions

View File

@ -36,7 +36,13 @@ export OS_USERNAME=$os_user
export OS_TENANT_NAME=$os_tenant
export OS_PASSWORD=$os_password
export OS_AUTH_URL=$os_auth_url
private_subnet=$(neutron net-show -f value -c subnets $private_net)
export OS_PROJECT_NAME=$os_project
export OS_USER_DOMAIN_ID=$os_user_domain
export OS_PROJECT_DOMAIN_ID=$os_project_domain
# At some point neutronclient started returning a python list repr from this
# command instead of just the value. This sed will strip off the bits we
# don't care about without messing up the output from older clients.
private_subnet=$(neutron net-show -f value -c subnets $private_net | sed "s/\[u'\(.*\)'\]/\1/")
default_gw=$(neutron subnet-show $private_subnet -f value -c gateway_ip)
prefix_len=$(neutron subnet-show -f value -c cidr $private_subnet | awk -F / '{print $2}')
@ -76,6 +82,11 @@ do
bm_instance=$(neutron port-show $bm_port -c device_id -f value)
bmc_port="$bmc_prefix_$(($i-1))"
bmc_ip=$(neutron port-show $bmc_port -c fixed_ips -f value | jq -r .ip_address)
# Newer neutronclient requires explicit json output and a slightly
# different jq query
if [ -z "$bmc_ip" ]; then
bmc_ip=$(neutron port-show $bmc_port -c fixed_ips -f json | jq -r .fixed_ips[0].ip_address)
fi
unit="openstack-bmc-$bm_port.service"
cat <<EOF >/usr/lib/systemd/system/$unit
@ -85,7 +96,7 @@ Requires=config-bmc-ips.service
After=config-bmc-ips.service
[Service]
ExecStart=/usr/local/bin/openstackbmc --os-user $os_user --os-password $os_password --os-tenant $os_tenant --os-auth-url $os_auth_url --instance $bm_instance --address $bmc_ip
ExecStart=/usr/local/bin/openstackbmc --os-user $os_user --os-password $os_password --os-tenant "$os_tenant" --os-auth-url $os_auth_url --os-project "$os_project" --os-user-domain "$os_user_domain" --os-project-domain "$os_project_domain" --instance $bm_instance --address $bmc_ip
Restart=always
User=root

View File

@ -94,24 +94,48 @@ def _get_clients():
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)
auth_url = os.environ.get('OS_AUTH_URL', '')
project = os.environ.get('OS_PROJECT_NAME')
user_domain = os.environ.get('OS_USER_DOMAIN_ID')
project_domain = os.environ.get('OS_PROJECT_DOMAIN_ID')
# novaclient 7+ is backwards-incompatible :-(
if int(nc.__version__[0]) <= 6:
nova = novaclient.Client(2, username, password, tenant, auth_url)
if '/v3' not in 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)
# novaclient 7+ is backwards-incompatible :-(
if int(nc.__version__[0]) <= 6:
nova = novaclient.Client(2, username, password, tenant, auth_url)
else:
nova = novaclient.Client(2, username, password,
auth_url=auth_url,
project_name=tenant)
neutron = neutronclient.Client(
username=username,
password=password,
tenant_name=tenant,
auth_url=auth_url
)
else:
if (not username or not password or not auth_url or not project or
not user_domain or not project_domain):
print('Source an appropriate rc file first')
sys.exit(1)
nova = novaclient.Client(2, username, password,
auth_url=auth_url,
project_name=tenant)
neutron = neutronclient.Client(
username=username,
password=password,
tenant_name=tenant,
auth_url=auth_url
)
project_name=project,
user_domain_name=user_domain,
project_domain_name=project_domain)
from keystoneauth1.identity import v3
from keystoneauth1 import session
auth = v3.Password(auth_url=auth_url,
username=username,
password=password,
project_name=project,
user_domain_name=user_domain,
project_domain_name=project_domain)
sess = session.Session(auth=auth)
neutron = neutronclient.Client(session=sess)
return nova, neutron

View File

@ -23,6 +23,7 @@ import yaml
from heatclient import client as heat_client
from heatclient.common import template_utils
from keystoneclient.v2_0 import client as keystone_client
from keystoneclient.v3 import client as keystone_v3_client
def _parse_args():
parser = argparse.ArgumentParser(description='Deploy an OVB environment')
@ -143,24 +144,59 @@ def _get_heat_client():
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)
project = os.environ.get('OS_PROJECT_NAME')
user_domain = os.environ.get('OS_USER_DOMAIN_ID')
project_domain = os.environ.get('OS_PROJECT_DOMAIN_ID')
# 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']
if '/v3' not in 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)
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']
catalog_key = 'serviceCatalog'
else:
if (not username or not password or not auth_url or not project or
not user_domain or not project_domain):
print('Source an appropriate rc file first')
sys.exit(1)
from keystoneauth1.identity import v3
from keystoneauth1 import session
auth = v3.Password(auth_url=auth_url,
username=username,
password=password,
project_name=project,
user_domain_name=user_domain,
project_domain_name=project_domain)
sess = session.Session(auth=auth)
kclient = keystone_v3_client.Client(session=sess)
token_data = kclient.get_raw_token_from_identity_service(
username=username,
password=password,
project_name=project,
auth_url=auth_url,
user_domain_name=user_domain,
project_domain_name=project_domain)
token_id = token_data['auth_token']
catalog_key = 'catalog'
# Get Heat endpoint
for endpoint in token_data['serviceCatalog']:
for endpoint in token_data[catalog_key]:
if endpoint['name'] == 'heat':
# TODO: What if there's more than one endpoint?
heat_endpoint = endpoint['endpoints'][0]['publicURL']
try:
# TODO: What if there's more than one endpoint?
heat_endpoint = endpoint['endpoints'][0]['publicURL']
except KeyError:
# Keystone v3 endpoint data looks different
heat_endpoint = [e for e in endpoint['endpoints']
if e['interface'] == 'public'][0]['url']
return heat_client.Client('1', endpoint=heat_endpoint, token=token_id)
@ -179,10 +215,16 @@ def _create_auth_parameters():
password = os.environ.get('OS_PASSWORD')
tenant = os.environ.get('OS_TENANT_NAME')
auth_url = os.environ.get('OS_AUTH_URL')
project = os.environ.get('OS_PROJECT_NAME')
user_domain = os.environ.get('OS_USER_DOMAIN_ID')
project_domain = os.environ.get('OS_PROJECT_DOMAIN_ID')
return {'os_user': username,
'os_password': password,
'os_tenant': tenant,
'os_auth_url': auth_url,
'os_project': project,
'os_user_domain': user_domain,
'os_project_domain': project_domain,
}
def _deploy(stack_name, stack_template, env_path, poll):

View File

@ -35,16 +35,23 @@ import pyghmi.ipmi.bmc as bmc
class OpenStackBmc(bmc.Bmc):
def __init__(self, authdata, port, address, instance, user, password, tenant,
auth_url):
auth_url, project, user_domain, project_domain):
super(OpenStackBmc, self).__init__(authdata, port=port, address=address)
# novaclient 7+ is backwards-incompatible :-(
if int(nc.__version__[0]) <= 6:
self.novaclient = novaclient.Client(2, user, password,
tenant, auth_url)
if not '/v3' in auth_url:
# novaclient 7+ is backwards-incompatible :-(
if int(nc.__version__[0]) <= 6:
self.novaclient = novaclient.Client(2, user, password,
tenant, auth_url)
else:
self.novaclient = novaclient.Client(2, user, password,
auth_url=auth_url,
project_name=tenant)
else:
self.novaclient = novaclient.Client(2, user, password,
auth_url=auth_url,
project_name=tenant)
project_name=project,
user_domain_name=user_domain,
project_domain_name=project_domain)
self.instance = None
self.cached_status = None
self.target_status = None
@ -191,12 +198,28 @@ def main():
help='The password for connecting to OpenStack')
parser.add_argument('--os-tenant',
dest='tenant',
required=True,
required=False,
default='',
help='The tenant for connecting to OpenStack')
parser.add_argument('--os-auth-url',
dest='auth_url',
required=True,
help='The OpenStack Keystone auth url')
parser.add_argument('--os-project',
dest='project',
required=False,
default='',
help='The project for connecting to OpenStack')
parser.add_argument('--os-user-domain',
dest='user_domain',
required=False,
default='',
help='The user domain for connecting to OpenStack')
parser.add_argument('--os-project-domain',
dest='project_domain',
required=False,
default='',
help='The project domain for connecting to OpenStack')
args = parser.parse_args()
# Default to ipv6 format, but if we get an ipv4 address passed in use the
# appropriate format for pyghmi to listen on it.
@ -209,7 +232,10 @@ def main():
user=args.user,
password=args.password,
tenant=args.tenant,
auth_url=args.auth_url)
auth_url=args.auth_url,
project=args.project,
user_domain=args.user_domain,
project_domain=args.project_domain)
mybmc.listen()

View File

@ -164,9 +164,7 @@ class TestBuildNodesJson(testtools.TestCase):
mock.call('network', cloud='foo')]
self.assertEqual(calls, mock_make_client.mock_calls)
@mock.patch('neutronclient.v2_0.client.Client')
@mock.patch('novaclient.client.Client')
def test_get_clients_env(self, mock_nova, mock_neutron):
def _test_get_clients_env(self, mock_nova, mock_neutron):
self.useFixture(fixtures.EnvironmentVariable('OS_USERNAME', 'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_PASSWORD', 'pw'))
self.useFixture(fixtures.EnvironmentVariable('OS_TENANT_NAME',
@ -180,11 +178,68 @@ class TestBuildNodesJson(testtools.TestCase):
self.assertEqual(mock_nova_client, nova)
self.assertEqual(mock_neutron_client, neutron)
@mock.patch('openstack_virtual_baremetal.build_nodes_json.nc.__version__',
('6', '0', '0'))
@mock.patch('neutronclient.v2_0.client.Client')
@mock.patch('novaclient.client.Client')
def test_get_clients_env_6(self, mock_nova, mock_neutron):
self._test_get_clients_env(mock_nova, mock_neutron)
@mock.patch('openstack_virtual_baremetal.build_nodes_json.nc.__version__',
('7', '0', '0'))
@mock.patch('neutronclient.v2_0.client.Client')
@mock.patch('novaclient.client.Client')
def test_get_clients_env_7(self, mock_nova, mock_neutron):
self._test_get_clients_env(mock_nova, mock_neutron)
@mock.patch('keystoneauth1.session.Session')
@mock.patch('keystoneauth1.identity.v3.Password')
@mock.patch('neutronclient.v2_0.client.Client')
@mock.patch('novaclient.client.Client')
def test_get_clients_env_v3(self, mock_nova, mock_neutron, mock_password,
mock_session):
self.useFixture(fixtures.EnvironmentVariable('OS_USERNAME', 'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_PASSWORD', 'pw'))
self.useFixture(fixtures.EnvironmentVariable('OS_PROJECT_NAME',
'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_AUTH_URL', 'auth/v3'))
self.useFixture(fixtures.EnvironmentVariable('OS_USER_DOMAIN_ID',
'default'))
self.useFixture(fixtures.EnvironmentVariable('OS_PROJECT_DOMAIN_ID',
'default'))
mock_nova_client = mock.Mock()
mock_nova.return_value = mock_nova_client
mock_neutron_client = mock.Mock()
mock_neutron.return_value = mock_neutron_client
mock_auth = mock.Mock()
mock_password.return_value = mock_auth
mock_session_inst = mock.Mock()
mock_session.return_value = mock_session_inst
nova, neutron = build_nodes_json._get_clients()
mock_password.assert_called_once_with(auth_url='auth/v3',
username='admin',
password='pw',
project_name='admin',
user_domain_name='default',
project_domain_name='default'
)
mock_session.assert_called_once_with(auth=mock_auth)
mock_neutron.assert_called_once_with(session=mock_session_inst)
self.assertEqual(mock_nova_client, nova)
self.assertEqual(mock_neutron_client, neutron)
@mock.patch('sys.exit')
def test_get_clients_missing(self, mock_exit):
build_nodes_json._get_clients()
mock_exit.assert_called_once_with(1)
@mock.patch('sys.exit')
def test_get_clients_missing_v3(self, mock_exit):
self.useFixture(fixtures.EnvironmentVariable('OS_AUTH_URL',
'http://host/v3'))
build_nodes_json._get_clients()
mock_exit.assert_called_once_with(1)
def test_get_ports(self):
neutron = mock.Mock()
fake_ports = {'ports':

View File

@ -17,7 +17,9 @@ import io
import unittest
import yaml
import fixtures
import mock
import testtools
import deploy
@ -58,7 +60,7 @@ class TestProcessArgs(unittest.TestCase):
self.assertEqual('foo', name)
self.assertEqual('templates/quintupleo.yaml', template)
def test_id_quintuple(self):
def test_id_quintupleo(self):
mock_args = mock.Mock()
mock_args.id = 'foo'
mock_args.quintupleo = False
@ -368,5 +370,93 @@ class TestDeploy(unittest.TestCase):
deploy._validate_env(args, 'foo.yaml')
V2_TOKEN_DATA = {'token': {'id': 'fake_token'},
'serviceCatalog': [{'name': 'nova'},
{'name': 'heat',
'endpoints': [
{'publicURL': 'heat_endpoint'}
]
}
]}
V3_TOKEN_DATA = {'auth_token': 'fake_v3_token',
'catalog': [{'name': 'nova'},
{'name': 'heat',
'endpoints': [
{'interface': 'private'},
{'interface': 'public',
'url': 'heat_endpoint'}
]
}
]}
class TestGetHeatClient(testtools.TestCase):
@mock.patch('os_client_config.make_client')
def test_os_cloud(self, mock_make_client):
self.useFixture(fixtures.EnvironmentVariable('OS_CLOUD', 'foo'))
deploy._get_heat_client()
mock_make_client.assert_called_once_with('orchestration', cloud='foo')
@mock.patch('heatclient.client.Client')
@mock.patch('keystoneclient.v2_0.client.Client')
def test_keystone_v2(self, mock_ksc, mock_hc):
self.useFixture(fixtures.EnvironmentVariable('OS_USERNAME', 'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_PASSWORD', 'pw'))
self.useFixture(fixtures.EnvironmentVariable('OS_TENANT_NAME',
'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_AUTH_URL', 'auth'))
mock_ks_client = mock.Mock()
mock_ksc.return_value = mock_ks_client
mock_ks_client.get_raw_token_from_identity_service.return_value = (
V2_TOKEN_DATA)
mock_token_value = 'fake_token'
deploy._get_heat_client()
mock_ksc.assert_called_once_with(username='admin', password='pw',
tenant_name='admin', auth_url='auth')
get_token = mock_ks_client.get_raw_token_from_identity_service
get_token.assert_called_once_with(username='admin', password='pw',
tenant_name='admin', auth_url='auth')
mock_hc.assert_called_once_with('1', endpoint='heat_endpoint',
token=mock_token_value)
@mock.patch('keystoneauth1.session.Session')
@mock.patch('keystoneauth1.identity.v3.Password')
@mock.patch('heatclient.client.Client')
@mock.patch('keystoneclient.v3.client.Client')
def test_keystone_v3(self, mock_ksc, mock_hc, mock_password, mock_session):
self.useFixture(fixtures.EnvironmentVariable('OS_USERNAME', 'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_PASSWORD', 'pw'))
self.useFixture(fixtures.EnvironmentVariable('OS_AUTH_URL', 'auth/v3'))
self.useFixture(fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'admin'))
self.useFixture(fixtures.EnvironmentVariable('OS_USER_DOMAIN_ID',
'default'))
self.useFixture(fixtures.EnvironmentVariable('OS_PROJECT_DOMAIN_ID',
'default'))
mock_auth = mock.Mock()
mock_password.return_value = mock_auth
mock_session_inst = mock.Mock()
mock_session.return_value = mock_session_inst
mock_ks_client = mock.Mock()
mock_ksc.return_value = mock_ks_client
mock_ks_client.get_raw_token_from_identity_service.return_value = (
V3_TOKEN_DATA)
mock_token_value = 'fake_v3_token'
deploy._get_heat_client()
mock_password.assert_called_once_with(auth_url='auth/v3',
username='admin',
password='pw',
project_name='admin',
user_domain_name='default',
project_domain_name='default'
)
mock_session.assert_called_once_with(auth=mock_auth)
get_token = mock_ks_client.get_raw_token_from_identity_service
get_token.assert_called_once_with(username='admin', password='pw',
project_name='admin',
auth_url='auth/v3',
user_domain_name='default',
project_domain_name='default')
mock_hc.assert_called_once_with('1', endpoint='heat_endpoint',
token=mock_token_value)
if __name__ == '__main__':
unittest.main()

View File

@ -42,7 +42,10 @@ class TestOpenStackBmcInit(unittest.TestCase):
user='admin',
password='password',
tenant='admin',
auth_url='http://keystone:5000'
auth_url='http://keystone:5000',
project='',
user_domain='',
project_domain=''
)
if old_nova:
mock_nova.assert_called_once_with(2, 'admin', 'password', 'admin',
@ -60,16 +63,47 @@ class TestOpenStackBmcInit(unittest.TestCase):
@mock.patch('openstack_virtual_baremetal.openstackbmc.nc.__version__',
('6', '0', '0'))
def test_init_6(self, mock_find_instance, mock_nova, mock_bmc_init,
mock_log):
mock_log):
self._test_init(mock_find_instance, mock_nova, mock_bmc_init, mock_log)
@mock.patch('openstack_virtual_baremetal.openstackbmc.nc.__version__',
('7', '0', '0'))
def test_init_7(self, mock_find_instance, mock_nova, mock_bmc_init,
mock_log):
mock_log):
self._test_init(mock_find_instance, mock_nova, mock_bmc_init, mock_log,
old_nova=False)
def test_init_v3(self, mock_find_instance, mock_nova, mock_bmc_init,
mock_log, old_nova=True):
mock_client = mock.Mock()
mock_server = mock.Mock()
mock_server.name = 'foo-instance'
mock_client.servers.get.return_value = mock_server
mock_nova.return_value = mock_client
mock_find_instance.return_value = 'abc-123'
bmc = openstackbmc.OpenStackBmc(authdata={'admin': 'password'},
port=623,
address='::ffff:127.0.0.1',
instance='foo',
user='admin',
password='password',
tenant='',
auth_url='http://keystone:5000/v3',
project='admin',
user_domain='default',
project_domain='default'
)
mock_nova.assert_called_once_with(2, 'admin', 'password',
auth_url='http://keystone:5000/v3',
project_name='admin',
user_domain_name='default',
project_domain_name='default')
mock_find_instance.assert_called_once_with('foo')
self.assertEqual('abc-123', bmc.instance)
mock_client.servers.get.assert_called_once_with('abc-123')
mock_log.assert_called_once_with('Managing instance: %s UUID: %s' %
('foo-instance', 'abc-123'))
@mock.patch('openstack_virtual_baremetal.openstackbmc.nc.__version__',
('6', '0', '0'))
@mock.patch('time.sleep')
@ -88,7 +122,10 @@ class TestOpenStackBmcInit(unittest.TestCase):
user='admin',
password='password',
tenant='admin',
auth_url='http://keystone:5000'
auth_url='http://keystone:5000',
project='',
user_domain='',
project_domain=''
)
mock_nova.assert_called_once_with(2, 'admin', 'password', 'admin',
'http://keystone:5000')
@ -119,7 +156,10 @@ class TestOpenStackBmc(unittest.TestCase):
user='admin',
password='password',
tenant='admin',
auth_url='http://keystone:5000'
auth_url='http://keystone:5000',
project='',
user_domain='',
project_domain=''
)
self.bmc.novaclient = self.mock_client
self.bmc.instance = 'abc-123'
@ -321,7 +361,10 @@ class TestMain(unittest.TestCase):
user='admin',
password='password',
tenant='admin',
auth_url='http://host:5000/v2.0'
auth_url='http://host:5000/v2.0',
project='',
user_domain='',
project_domain=''
)
mock_instance.listen.assert_called_once_with()
@ -342,6 +385,9 @@ class TestMain(unittest.TestCase):
user='admin',
password='password',
tenant='admin',
auth_url='http://host:5000/v2.0'
auth_url='http://host:5000/v2.0',
project='',
user_domain='',
project_domain=''
)
mock_instance.listen.assert_called_once_with()

View File

@ -137,6 +137,27 @@ parameters:
default: http://127.0.0.1:5000/v2.0
description: The Keystone auth_url of the host cloud
os_project:
type: string
default: ''
description: |
The project for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
os_user_domain:
type: string
default: ''
description: |
The user domain for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
os_project_domain:
type: string
default: ''
description: |
The project domain for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
resources:
provision_network:
type: OS::Neutron::Net
@ -209,6 +230,9 @@ resources:
os_password: {get_param: os_password}
os_tenant: {get_param: os_tenant}
os_auth_url: {get_param: os_auth_url}
os_project: {get_param: os_project}
os_user_domain: {get_param: os_user_domain}
os_project_domain: {get_param: os_project_domain}
outputs:
undercloud_host_floating_ip:

View File

@ -87,6 +87,27 @@ parameters:
default: http://127.0.0.1:5000/v2.0
description: The Keystone auth_url of the host cloud
os_project:
type: string
default: ''
description: |
The project for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
os_user_domain:
type: string
default: ''
description: |
The user domain for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
os_project_domain:
type: string
default: ''
description: |
The project domain for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
default_sg:
type: string
default: all_sg
@ -154,6 +175,9 @@ resources:
$os_password: {get_param: os_password}
$os_tenant: {get_param: os_tenant}
$os_auth_url: {get_param: os_auth_url}
$os_project: {get_param: os_project}
$os_user_domain: {get_param: os_user_domain}
$os_project_domain: {get_param: os_project_domain}
$bm_node_count: {get_param: node_count}
$bmc_prefix: {get_param: bmc_prefix}
$bmc_utility: {get_attr: [bmc_port, fixed_ips, 0, ip_address]}