Use more os-client-config to handle bmc auth

It turns out to be a giant PITA to do keystone v3 auth by hand, as
the bmc install script was trying to.  Different clouds require a
different combination of auth values set, and os-client-config
already exists to handle these variations.

To do this, the os-client-config data is passed directly into the
bmc install script as json, which is then written to clouds.yaml
so it can be used by the clients.  In theory this should mean that
the bmc can handle any cloud where we were able to deploy the heat
stack because it's using the exact same auth data.

In addition, this change makes os-client-config mandatory on the bmc
image.  This is not a meaningful change though because it was already
pulled in by the clients, so it should already exist on any bmc
images in the wild.

It also deprecates the old method of providing auth data to the bmc.
For the moment it should continue to work, but the os-client-config
method should be used going forward.  Note that by default the bmc
will now read auth parameters from the environment, which is
actually an improvement over how it worked before when running
interactively.
This commit is contained in:
Ben Nemec 2017-07-20 15:39:44 -05:00
parent 9cd9267c42
commit ae02e88b98
9 changed files with 193 additions and 93 deletions

View File

@ -5,9 +5,7 @@ set -x
# install python2-crypto from EPEL
# python-[nova|neutron]client are in a similar situation. They were renamed
# in RDO to python2-*
# os-client-config is not a hard requirement, but it will be installed if
# we're installing packages anyway.
required_packages="python-pip os-net-config git jq"
required_packages="python-pip os-net-config git jq python2-os-client-config"
function have_packages() {
for i in $required_packages; do
@ -33,7 +31,7 @@ function have_packages() {
if ! have_packages; then
yum -y update centos-release # required for rdo-release install to work
yum install -y https://rdo.fedorapeople.org/rdo-release.rpm
yum install -y $required_packages python-crypto python2-novaclient python2-neutronclient python2-os-client-config
yum install -y $required_packages python-crypto python2-novaclient python2-neutronclient
pip install pyghmi
fi
@ -42,22 +40,23 @@ $openstackbmc_script
EOF
chmod +x /usr/local/bin/openstackbmc
export OS_USERNAME="$os_user"
export OS_TENANT_NAME="$os_tenant"
set +x
echo "exporting OS_PASSWORD"
export OS_PASSWORD="$os_password"
set -x
export OS_AUTH_URL="$os_auth_url"
export OS_PROJECT_NAME="$os_project"
# NOTE(bnemec): The double _ in these names is intentional. It prevents
# collisions with the $os_user and $os_project values above.
export OS_USER_DOMAIN="$os__user_domain"
export OS_PROJECT_DOMAIN="$os__project_domain"
# v3 env vars mess up v2 auth
[ -z $OS_PROJECT_NAME ] && unset OS_PROJECT_NAME
[ -z $OS_USER_DOMAIN ] && unset OS_USER_DOMAIN
[ -z $OS_PROJECT_DOMAIN ] && unset OS_PROJECT_DOMAIN
# Configure clouds.yaml so we can authenticate to the host cloud
mkdir -p ~/.config/openstack
# Passing this as an argument is problematic because it has quotes inline that
# cause syntax errors. Reading from a file should be easier.
cat <<EOF >/tmp/bmc-cloud-data
$cloud_data
EOF
python -c 'import json
import sys
import yaml
with open("/tmp/bmc-cloud-data") as f:
data=json.loads(f.read())
clouds={"clouds": {"host_cloud": data}}
print(yaml.safe_dump(clouds, default_flow_style=False))' > ~/.config/openstack/clouds.yaml
rm -f /tmp/bmc-cloud-data
export OS_CLOUD=host_cloud
# 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.
@ -119,7 +118,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 --os-project "$os_project" --os-user-domain "$os__user_domain" --os-project-domain "$os__project_domain" --instance $bm_instance --address $bmc_ip $cache_status
ExecStart=/usr/local/bin/openstackbmc --os-cloud host_cloud --instance $bm_instance --address $bmc_ip $cache_status
Restart=always
User=root

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import sys
@ -59,3 +60,11 @@ def _create_auth_parameters():
'os_user_domain': user_domain,
'os_project_domain': project_domain,
}
def _cloud_json():
"""Return the current cloud's data in JSON
Retrieves the cloud from os-client-config and serializes it to JSON.
"""
config = os_client_config.OpenStackConfig().get_one_cloud(OS_CLOUD)
return json.dumps(config.config)

View File

@ -153,7 +153,7 @@ def _deploy(stack_name, stack_template, env_path, poll):
all_files = {}
all_files.update(template_files)
all_files.update(env_files)
parameters = auth._create_auth_parameters()
parameters = {'cloud_data': auth._cloud_json()}
hclient.stacks.create(stack_name=stack_name,
template=template,

View File

@ -24,6 +24,7 @@
# ipmitool -I lanplus -U admin -P password -H 127.0.0.1 mc reset cold
import argparse
import os
import sys
import time
@ -44,17 +45,24 @@ NO_OCC_DEPRECATION = ('WARNING: Creating novaclient without os-client-config '
class OpenStackBmc(bmc.Bmc):
def __init__(self, authdata, port, address, instance, user, password, tenant,
auth_url, project, user_domain, project_domain, cache_status):
auth_url, project, user_domain, project_domain, cache_status,
os_cloud):
super(OpenStackBmc, self).__init__(authdata, port=port, address=address)
if os_client_config:
kwargs = dict(os_username=user,
os_password=password,
os_project_name=tenant,
os_auth_url=auth_url,
os_user_domain=user_domain,
os_project_domain=project_domain)
self.novaclient = os_client_config.make_client('compute',
**kwargs)
if user:
# NOTE(bnemec): This is deprecated. clouds.yaml is a much
# more robust way to specify auth details.
kwargs = dict(os_username=user,
os_password=password,
os_project_name=tenant,
os_auth_url=auth_url,
os_user_domain=user_domain,
os_project_domain=project_domain)
self.novaclient = os_client_config.make_client('compute',
**kwargs)
else:
self.novaclient = os_client_config.make_client('compute',
cloud=os_cloud)
else:
# NOTE(bnemec): This path was deprecated 2017-7-17
self.log(NO_OCC_DEPRECATION)
@ -216,36 +224,53 @@ def main():
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')
required=False,
default='',
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'The user for connecting to OpenStack')
parser.add_argument('--os-password',
dest='password',
required=True,
help='The password for connecting to OpenStack')
required=False,
default='',
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'The password for connecting to OpenStack')
parser.add_argument('--os-tenant',
dest='tenant',
required=False,
default='',
help='The tenant for connecting to OpenStack')
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'The tenant for connecting to OpenStack')
parser.add_argument('--os-auth-url',
dest='auth_url',
required=True,
help='The OpenStack Keystone auth url')
required=False,
default='',
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'The OpenStack Keystone auth url')
parser.add_argument('--os-project',
dest='project',
required=False,
default='',
help='The project for connecting to OpenStack')
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'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')
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'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')
help='DEPRECATED: Use --os-cloud to specify auth '
'details. '
'The project domain for connecting to OpenStack')
parser.add_argument('--cache-status',
dest='cache_status',
default=False,
@ -254,6 +279,12 @@ def main():
'can reduce load on the host cloud, but if the '
'instance status is changed outside the BMC then '
'it may become out of sync.')
parser.add_argument('--os-cloud',
dest='os_cloud',
required=False,
default=os.environ.get('OS_CLOUD'),
help='Use the specified cloud from clouds.yaml. '
'Defaults to the OS_CLOUD environment variable.')
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.
@ -270,7 +301,8 @@ def main():
project=args.project,
user_domain=args.user_domain,
project_domain=args.project_domain,
cache_status=args.cache_status)
cache_status=args.cache_status,
os_cloud=args.os_cloud)
mybmc.listen()

View File

@ -13,6 +13,7 @@
# under the License.
import fixtures
import json
import mock
import testtools
@ -138,3 +139,22 @@ class TestCreateAuthParameters(testtools.TestCase):
'os_project_domain': 'default',
}
self.assertEqual(expected, result)
class TestCloudJSON(testtools.TestCase):
@mock.patch('openstack_virtual_baremetal.auth.OS_CLOUD', 'foo')
@mock.patch('os_client_config.OpenStackConfig')
def test_cloud_json(self, mock_osc):
mock_data = mock.Mock()
mock_data.config = {'auth': {'username': 'admin',
'password': 'password',
'project_name': 'admin',
'auth_url': 'http://host:5000',
'user_domain_name': 'default',
'project_domain_name': 'default',
}}
mock_instance = mock.Mock()
mock_instance.get_one_cloud.return_value = mock_data
mock_osc.return_value = mock_instance
result = auth._cloud_json()
expected = json.dumps(mock_data.config)
self.assertEqual(expected, result)

View File

@ -215,7 +215,7 @@ role_original_data = {
# end _process_role test data
class TestDeploy(testtools.TestCase):
def _test_deploy(self, mock_ghc, mock_tu, mock_poll, mock_cap, poll=False):
def _test_deploy(self, mock_ghc, mock_tu, mock_poll, mock_cj, poll=False):
mock_client = mock.Mock()
mock_ghc.return_value = mock_client
template_files = {'template.yaml': {'foo': 'bar'}}
@ -232,40 +232,43 @@ class TestDeploy(testtools.TestCase):
all_files = {}
all_files.update(template_files)
all_files.update(env_files)
params = {'os_user': 'admin',
'os_password': 'password',
'os_tenant': 'admin',
'os_auth_url': 'http://1.1.1.1:5000/v2.0',
}
mock_cap.return_value = params
auth = {'os_user': 'admin',
'os_password': 'password',
'os_tenant': 'admin',
'os_auth_url': 'http://1.1.1.1:5000/v2.0',
}
params = {'auth': auth}
expected_params = {'cloud_data': params}
mock_cj.return_value = params
deploy._deploy('test', 'template.yaml', 'env.yaml', poll)
mock_tu.get_template_contents.assert_called_once_with('template.yaml')
process = mock_tu.process_multiple_environments_and_files
process.assert_called_once_with(['templates/resource-registry.yaml',
'env.yaml'])
mock_client.stacks.create.assert_called_once_with(stack_name='test',
template=template,
environment=env,
files=all_files,
parameters=params)
mock_client.stacks.create.assert_called_once_with(
stack_name='test',
template=template,
environment=env,
files=all_files,
parameters=expected_params)
if not poll:
mock_poll.assert_not_called()
else:
mock_poll.assert_called_once_with('test', mock_client)
@mock.patch('openstack_virtual_baremetal.auth._create_auth_parameters')
@mock.patch('openstack_virtual_baremetal.auth._cloud_json')
@mock.patch('openstack_virtual_baremetal.deploy._poll_stack')
@mock.patch('openstack_virtual_baremetal.deploy.template_utils')
@mock.patch('openstack_virtual_baremetal.deploy._get_heat_client')
def test_deploy(self, mock_ghc, mock_tu, mock_poll, mock_cap):
self._test_deploy(mock_ghc, mock_tu, mock_poll, mock_cap)
def test_deploy(self, mock_ghc, mock_tu, mock_poll, mock_cj):
self._test_deploy(mock_ghc, mock_tu, mock_poll, mock_cj)
@mock.patch('openstack_virtual_baremetal.auth._create_auth_parameters')
@mock.patch('openstack_virtual_baremetal.auth._cloud_json')
@mock.patch('openstack_virtual_baremetal.deploy._poll_stack')
@mock.patch('openstack_virtual_baremetal.deploy.template_utils')
@mock.patch('openstack_virtual_baremetal.deploy._get_heat_client')
def test_deploy_poll(self, mock_ghc, mock_tu, mock_poll, mock_cap):
self._test_deploy(mock_ghc, mock_tu, mock_poll, mock_cap, True)
def test_deploy_poll(self, mock_ghc, mock_tu, mock_poll, mock_cj):
self._test_deploy(mock_ghc, mock_tu, mock_poll, mock_cj, True)
@mock.patch('time.sleep')
def test_poll(self, mock_sleep):

View File

@ -48,7 +48,8 @@ class TestOpenStackBmcInitDeprecated(unittest.TestCase):
project='',
user_domain='',
project_domain='',
cache_status=False
cache_status=False,
os_cloud=None
)
if old_nova:
mock_nova.assert_called_once_with(2, 'admin', 'password', 'admin',
@ -103,7 +104,8 @@ class TestOpenStackBmcInitDeprecated(unittest.TestCase):
project='admin',
user_domain='default',
project_domain='default',
cache_status=False
cache_status=False,
os_cloud=None
)
mock_nova.assert_called_once_with(2, 'admin', 'password',
auth_url='http://keystone:5000/v3',
@ -127,7 +129,6 @@ class TestOpenStackBmcInitDeprecated(unittest.TestCase):
class TestOpenStackBmcInit(testtools.TestCase):
def test_init_os_client_config(self, mock_make_client, mock_find_instance,
mock_bmc_init, mock_log):
self.useFixture(fixtures.EnvironmentVariable('OS_CLOUD', None))
mock_client = mock.Mock()
mock_server = mock.Mock()
mock_server.name = 'foo-instance'
@ -145,7 +146,8 @@ class TestOpenStackBmcInit(testtools.TestCase):
project='',
user_domain='',
project_domain='',
cache_status=False
cache_status=False,
os_cloud=None
)
mock_make_client.assert_called_once_with('compute',
@ -161,6 +163,36 @@ class TestOpenStackBmcInit(testtools.TestCase):
mock_log.assert_called_once_with('Managing instance: %s UUID: %s' %
('foo-instance', 'abc-123'))
def test_init_os_cloud(self, mock_make_client, mock_find_instance,
mock_bmc_init, mock_log):
mock_client = mock.Mock()
mock_server = mock.Mock()
mock_server.name = 'foo-instance'
mock_client.servers.get.return_value = mock_server
mock_make_client.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='',
password='',
tenant='',
auth_url='',
project='',
user_domain='',
project_domain='',
cache_status=False,
os_cloud='bar'
)
mock_make_client.assert_called_once_with('compute', cloud='bar')
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('time.sleep')
def test_init_retry(self, _, mock_make_client, mock_find_instance,
mock_bmc_init, mock_log):
@ -175,22 +207,17 @@ class TestOpenStackBmcInit(testtools.TestCase):
port=623,
address='::ffff:127.0.0.1',
instance='foo',
user='admin',
password='password',
tenant='admin',
auth_url='http://keystone:5000',
user='',
password='',
tenant='',
auth_url='',
project='',
user_domain='',
project_domain='',
cache_status=False
cache_status=False,
os_cloud='foo'
)
mock_make_client.assert_called_once_with('compute',
os_auth_url='http://keystone:5000',
os_password='password',
os_project_domain='',
os_project_name='admin',
os_user_domain='',
os_username='admin')
mock_make_client.assert_called_once_with('compute', cloud='foo')
find_calls = [mock.call('foo'), mock.call('foo')]
self.assertEqual(find_calls, mock_find_instance.mock_calls)
self.assertEqual('abc-123', bmc.instance)
@ -222,7 +249,8 @@ class TestOpenStackBmc(unittest.TestCase):
project='',
user_domain='',
project_domain='',
cache_status=False
cache_status=False,
os_cloud=None
)
self.bmc.novaclient = self.mock_client
self.bmc.instance = 'abc-123'
@ -424,23 +452,22 @@ class TestMain(unittest.TestCase):
mock_instance = mock.Mock()
mock_bmc.return_value = mock_instance
mock_argv = ['openstackbmc', '--port', '111', '--address', '1.2.3.4',
'--instance', 'foobar', '--os-user', 'admin',
'--os-password', 'password', '--os-tenant', 'admin',
'--os-auth-url', 'http://host:5000/v2.0']
'--instance', 'foobar', '--os-cloud', 'foo']
with mock.patch.object(sys, 'argv', mock_argv):
openstackbmc.main()
mock_bmc.assert_called_once_with({'admin': 'password'},
port=111,
address='::ffff:1.2.3.4',
instance='foobar',
user='admin',
password='password',
tenant='admin',
auth_url='http://host:5000/v2.0',
user='',
password='',
tenant='',
auth_url='',
project='',
user_domain='',
project_domain='',
cache_status=False
cache_status=False,
os_cloud='foo'
)
mock_instance.listen.assert_called_once_with()
@ -449,22 +476,21 @@ class TestMain(unittest.TestCase):
mock_instance = mock.Mock()
mock_bmc.return_value = mock_instance
mock_argv = ['openstackbmc', '--port', '111',
'--instance', 'foobar', '--os-user', 'admin',
'--os-password', 'password', '--os-tenant', 'admin',
'--os-auth-url', 'http://host:5000/v2.0']
'--instance', 'foobar']
with mock.patch.object(sys, 'argv', mock_argv):
openstackbmc.main()
mock_bmc.assert_called_once_with({'admin': 'password'},
port=111,
address='::',
instance='foobar',
user='admin',
password='password',
tenant='admin',
auth_url='http://host:5000/v2.0',
user='',
password='',
tenant='',
auth_url='',
project='',
user_domain='',
project_domain='',
cache_status=False
cache_status=False,
os_cloud=None
)
mock_instance.listen.assert_called_once_with()

View File

@ -158,6 +158,11 @@ parameters:
The project domain for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
cloud_data:
type: string
default: '{}'
resources:
provision_network:
type: OS::Neutron::Net
@ -233,6 +238,7 @@ resources:
os_project: {get_param: os_project}
os_user_domain: {get_param: os_user_domain}
os_project_domain: {get_param: os_project_domain}
cloud_data: {get_param: cloud_data}
outputs:
undercloud_host_floating_ip:

View File

@ -116,6 +116,10 @@ parameters:
The project domain for os_user. Required for Keystone v3, should be left
blank for Keystone v2.
cloud_data:
type: string
default: '{}'
# Ignored parameters for compatibility with QuintupleO env files
undercloud_image:
type: string
@ -184,6 +188,7 @@ resources:
$bm_prefix: {get_param: baremetal_prefix}
$private_net: {get_param: private_net}
$openstackbmc_script: {get_file: ../bin/openstackbmc}
$cloud_data: {get_param: cloud_data}
template: {get_file: ../bin/install_openstackbmc.sh}
baremetal_networks: