Upgrades, add keystone authentification for ostf checker

In case if there is problem with conectivity between
ostf and keystone container, ostf checker won't catch
and user can get broken ostf.

* created separate module clients
* extracted keystone client in separate class
* added ostf client which uses keystone authentification

Closes-bug: #1363054
Change-Id: If5bd2fb6a5736bd043171489ef9615f376a9ed75
This commit is contained in:
Evgeniy L 2014-08-29 13:07:51 +04:00
parent a762c6029b
commit 03cdcd640e
15 changed files with 335 additions and 120 deletions

View File

@ -22,7 +22,7 @@ import six
from fuel_upgrade import errors
from fuel_upgrade import utils
from fuel_upgrade.nailgun_client import NailgunClient
from fuel_upgrade.clients import NailgunClient
logger = logging.getLogger(__name__)

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Mirantis, 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.
from fuel_upgrade.clients.keystone_client import KeystoneClient
from fuel_upgrade.clients.nailgun_client import NailgunClient
from fuel_upgrade.clients.ostf_client import OSTFClient
from fuel_upgrade.clients.supervisor_client import SupervisorClient

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Mirantis, 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 json
import logging
import requests
logger = logging.getLogger(__name__)
class KeystoneClient(object):
"""Simple keystone authentification client
:param str username: is user name
:param str password: is user password
:param str auth_url: authentification url
:param str tenant_name: tenant name
"""
def __init__(self, username=None, password=None,
auth_url=None, tenant_name=None):
self.auth_url = auth_url
self.tenant_name = tenant_name
self.username = username
self.password = password
@property
def request(self):
"""Creates authentification session if required
:returns: :class:`requests.Session` object
"""
session = requests.Session()
token = self.get_token()
if token:
session.headers.update({'X-Auth-Token': token})
return session
def get_token(self):
"""Retrieves auth token from keystone
:returns: authentification token or None in case of error
NOTE(eli): for 5.0.x versions of fuel we don't
have keystone and fuel access control feature,
as result this client should work with and without
authentication, in order to do this, we are
trying to create Keystone client and in case if
it fails we don't use authentication
"""
try:
resp = requests.post(
self.auth_url,
headers={'content-type': 'application/json'},
data=json.dumps({
'auth': {
'tenantName': self.tenant_name,
'passwordCredentials': {
'username': self.username,
'password': self.password}}})).json()
return (isinstance(resp, dict) and
resp.get('access', {}).get('token', {}).get('id'))
except (ValueError, requests.exceptions.RequestException) as exc:
logger.debug('Cannot authenticate in keystone: {0}'.format(exc))
return None

View File

@ -16,7 +16,8 @@
import json
import logging
import requests
from fuel_upgrade.clients import KeystoneClient
logger = logging.getLogger(__name__)
@ -35,11 +36,11 @@ class NailgunClient(object):
api_url = 'http://{host}:{port}/api/v1'
def __init__(self, host=None, port=None, keystone_credentials=None):
def __init__(self, host=None, port=None, keystone_credentials={}):
#: an url to nailgun's restapi service
self.api_url = self.api_url.format(host=host, port=port)
#: keystone credentials for nailgun authentification
self.keystone_credentials = keystone_credentials
#: keystone credentials for authentification
self.keystone_client = KeystoneClient(**keystone_credentials)
def get_releases(self):
"""Returns a list with all releases.
@ -130,43 +131,4 @@ class NailgunClient(object):
:returns: :class:`requests.Session` object
"""
session = requests.Session()
token = self.get_token()
if token:
session.headers.update({'X-Auth-Token': token})
return session
def get_token(self):
"""Retrieves auth token from keystone
:returns: authentification token
NOTE(eli): for 5.0.x versions of fuel we don't
have keystone and fuel access control feature,
as result this client should work with and without
authentication, in order to do this, we are
trying to create Keystone client and in case if
it fails we don't use authentication
"""
if not self.keystone_credentials:
return None
try:
auth_data = self.keystone_credentials
resp = requests.post(
auth_data['auth_url'],
headers={'content-type': 'application/json'},
data=json.dumps({
'auth': {
'tenantName': auth_data['tenant_name'],
'passwordCredentials': {
'username': auth_data['username'],
'password': auth_data['password']}}})).json()
return (isinstance(resp, dict) and
resp.get('access', {}).get('token', {}).get('id'))
except (ValueError, requests.exceptions.RequestException) as exc:
logger.debug('Cannot authenticate in keystone: {0}'.format(exc))
return None
return self.keystone_client.request

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Mirantis, 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 logging
from fuel_upgrade.clients import KeystoneClient
logger = logging.getLogger(__name__)
class OSTFClient(object):
"""OSTFClient is a simple wrapper around OSTF API.
:param str host: ostf's host address
:param (str|int) port: ostf's port number
:param dict keystone_credentials: keystone credentials where
`username` is user name
`password` is user password
`auth_url` authentification url
`tenant_name` tenant name
"""
api_url = 'http://{host}:{port}'
def __init__(self, host=None, port=None, keystone_credentials={}):
#: an url to nailgun's restapi service
self.api_url = self.api_url.format(host=host, port=port)
#: keystone credentials for authentification
self.keystone_client = KeystoneClient(**keystone_credentials)
@property
def request(self):
"""Creates authentification session if required
:returns: :class:`requests.Session` object
"""
return self.keystone_client.request
def get(self, path):
"""Retrieve list of tasks from nailgun
:returns: list of tasks
"""
result = self.request.get('{api_url}{path}'.format(
api_url=self.api_url, path=path))
return result

View File

@ -58,7 +58,7 @@ class SupervisorClient(object):
"""RPC Client for supervisor
"""
templates_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), 'templates'))
os.path.join(os.path.dirname(__file__), '..', 'templates'))
def __init__(self, config, from_version):
"""Create supervisor client

View File

@ -121,15 +121,17 @@ def get_endpoints(astute_config):
rabbitmq_mcollective_access = astute_config.get(
'mcollective', {'user': 'mcollective', 'password': 'marionette'})
keystone_credentials = {
'username': fuel_access['user'],
'password': fuel_access['password'],
'auth_url': 'http://{0}:5000/v2.0/tokens'.format(master_ip),
'tenant_name': 'admin'}
return {
'nginx_nailgun': {
'port': 8000,
'host': '0.0.0.0',
'keystone_credentials': {
'username': fuel_access['user'],
'password': fuel_access['password'],
'auth_url': 'http://{0}:5000/v2.0/tokens'.format(master_ip),
'tenant_name': 'admin'}},
'keystone_credentials': keystone_credentials},
'nginx_repo': {
'port': 8080,
@ -137,7 +139,8 @@ def get_endpoints(astute_config):
'ostf': {
'port': 8777,
'host': '127.0.0.1'},
'host': '127.0.0.1',
'keystone_credentials': keystone_credentials},
'cobbler': {
'port': 80,

View File

@ -24,9 +24,9 @@ from copy import deepcopy
import docker
import requests
from fuel_upgrade.clients import SupervisorClient
from fuel_upgrade.engines.base import UpgradeEngine
from fuel_upgrade.health_checker import FuelUpgradeVerify
from fuel_upgrade.supervisor_client import SupervisorClient
from fuel_upgrade.version_file import VersionFile
from fuel_upgrade import errors

View File

@ -22,8 +22,8 @@ import os
import requests
import six
from fuel_upgrade.clients import NailgunClient
from fuel_upgrade.engines.base import UpgradeEngine
from fuel_upgrade.nailgun_client import NailgunClient
from fuel_upgrade import utils

View File

@ -25,7 +25,8 @@ import six
from fuel_upgrade import errors
from fuel_upgrade import utils
from fuel_upgrade.nailgun_client import NailgunClient
from fuel_upgrade.clients import NailgunClient
from fuel_upgrade.clients import OSTFClient
logger = logging.getLogger(__name__)
@ -135,9 +136,6 @@ class OSTFChecker(BaseChecker):
resp = self.safe_get('http://{host}:{port}/'.format(
**self.endpoints['ostf']))
# NOTE(eli): 401 response when authorization is enabled
# 200 when there is no authorization, remove 200 when
# authorization is enabled by default
return resp and (resp['code'] == 401 or resp['code'] == 200)
@ -273,6 +271,24 @@ class IntegrationCheckerNginxNailgunChecker(BaseChecker):
return resp and resp['code'] == 200
class IntegrationOSTFKeystoneChecker(BaseChecker):
@property
def checker_name(self):
return 'integration_ostf_keystone'
def check(self):
ostf_client = OSTFClient(**self.endpoints['ostf'])
def get_request():
resp = ostf_client.get('/')
return resp.status_code
code = self.make_safe_request(get_request)
return code == 200
class KeystoneChecker(BaseChecker):
@property
@ -360,6 +376,7 @@ class FuelUpgradeVerify(object):
MCollectiveChecker,
KeystoneChecker,
NginxChecker,
IntegrationOSTFKeystoneChecker,
IntegrationCheckerNginxNailgunChecker,
IntegrationCheckerPostgresqlNailgunNginx,
IntegrationCheckerRabbitMQAstuteNailgun]

View File

@ -392,3 +392,15 @@ class TestCheckers(BaseTestCase):
get_mock.return_value = result
self.assert_checker_false(
health_checker.IntegrationCheckerRabbitMQAstuteNailgun)
@mock.patch('fuel_upgrade.health_checker.BaseChecker.make_safe_request')
def test_nailgun_checker_returns_true(self, make_request_mock):
make_request_mock.return_value = 200
self.assert_checker_true(
health_checker.IntegrationOSTFKeystoneChecker)
@mock.patch('fuel_upgrade.health_checker.BaseChecker.make_safe_request')
def test_nailgun_checker_returns_false(self, make_request_mock):
make_request_mock.return_value = 401
self.assert_checker_false(
health_checker.IntegrationOSTFKeystoneChecker)

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Mirantis, 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 mock
import requests
from fuel_upgrade.clients import KeystoneClient
from fuel_upgrade.tests import base
class TestKeystoneClient(base.BaseTestCase):
token = {'access': {'token': {'id': 'auth_token'}}}
def setUp(self):
self.credentials = {
'username': 'some_user',
'password': 'some_password',
'auth_url': 'http://127.0.0.1:5000/v2',
'tenant_name': 'some_tenant'}
self.keystone = KeystoneClient(**self.credentials)
@mock.patch('fuel_upgrade.clients.keystone_client.requests.post')
@mock.patch('fuel_upgrade.clients.keystone_client.requests.Session')
def test_makes_authenticated_requests(self, session, post_mock):
post_mock.return_value.json.return_value = self.token
self.keystone.request
session.return_value.headers.update.assert_called_once_with(
{'X-Auth-Token': 'auth_token'})
@mock.patch('fuel_upgrade.clients.keystone_client.requests.Session')
@mock.patch('fuel_upgrade.clients.keystone_client.requests.post',
side_effect=requests.exceptions.HTTPError(''))
def test_does_not_fail_without_keystone(self, _, __):
self.keystone.request
self.assertEqual(self.keystone.get_token(), None)

View File

@ -17,18 +17,23 @@
import mock
import requests
from fuel_upgrade import nailgun_client
from fuel_upgrade.clients import NailgunClient
from fuel_upgrade.tests import base
class TestNailgunClient(base.BaseTestCase):
def setUp(self):
self.nailgun = nailgun_client.NailgunClient('http://127.0.0.1', 8000)
@mock.patch('fuel_upgrade.nailgun_client.requests.Session.post')
def test_create_release(self, post):
def setUp(self):
mock_keystone = mock.MagicMock()
self.mock_request = mock_keystone.request
with mock.patch(
'fuel_upgrade.clients.nailgun_client.KeystoneClient',
return_value=mock_keystone):
self.nailgun = NailgunClient('127.0.0.1', 8000)
def test_create_release(self):
# test normal bahavior
post.return_value = self.mock_requests_response(
self.mock_request.post.return_value = self.mock_requests_response(
201, '{ "id": "42" }')
response = self.nailgun.create_release({
@ -37,33 +42,32 @@ class TestNailgunClient(base.BaseTestCase):
self.assertEqual(response, {'id': '42'})
# test failed result
post.return_value.status_code = 409
self.mock_request.post.return_value.status_code = 409
self.assertRaises(
requests.exceptions.HTTPError,
self.nailgun.create_release,
{'name': 'Havana on Ubuntu 12.04'})
@mock.patch('fuel_upgrade.nailgun_client.requests.Session.delete')
def test_delete_release(self, delete):
def test_delete_release(self):
# test normal bahavior
for status in (200, 204):
delete.return_value = self.mock_requests_response(
status, 'No Content')
self.mock_request.delete.return_value = \
self.mock_requests_response(status, 'No Content')
response = self.nailgun.remove_release(42)
self.assertEqual(response, 'No Content')
# test failed result
delete.return_value = self.mock_requests_response(409, 'Conflict')
self.mock_request.delete.return_value = self.mock_requests_response(
409, 'Conflict')
self.assertRaises(
requests.exceptions.HTTPError,
self.nailgun.remove_release,
42)
@mock.patch('fuel_upgrade.nailgun_client.requests.Session.post')
def test_create_notification(self, post):
def test_create_notification(self):
# test normal bahavior
post.return_value = self.mock_requests_response(
self.mock_request.post.return_value = self.mock_requests_response(
201,
'{ "id": "42" }')
@ -74,71 +78,40 @@ class TestNailgunClient(base.BaseTestCase):
self.assertEqual(response, {'id': '42'})
# test failed result
post.return_value.status_code = 409
self.mock_request.post.return_value.status_code = 409
self.assertRaises(
requests.exceptions.HTTPError,
self.nailgun.create_notification,
{'topic': 'release',
'message': 'New release available!'})
@mock.patch('fuel_upgrade.nailgun_client.requests.Session.delete')
def test_delete_notification(self, delete):
def test_delete_notification(self):
# test normal bahavior
for status in (200, 204):
delete.return_value = self.mock_requests_response(
status, 'No Content')
self.mock_request.delete.return_value = \
self.mock_requests_response(status, 'No Content')
response = self.nailgun.remove_notification(42)
self.assertEqual(response, 'No Content')
# test failed result
delete.return_value = self.mock_requests_response(409, 'Conflict')
self.mock_request.delete.return_value = self.mock_requests_response(
409, 'Conflict')
self.assertRaises(
requests.exceptions.HTTPError,
self.nailgun.remove_notification,
42)
@mock.patch('fuel_upgrade.nailgun_client.requests.Session.get')
def test_get_tasks(self, get):
def test_get_tasks(self):
# test positive cases
get.return_value = self.mock_requests_response(200, '[1,2,3]')
self.mock_request.get.return_value = self.mock_requests_response(
200, '[1,2,3]')
response = self.nailgun.get_tasks()
self.assertEqual(response, [1, 2, 3])
# test negative cases
get.return_value = self.mock_requests_response(502, 'Bad gateway')
self.mock_request.get.return_value = self.mock_requests_response(
502, 'Bad gateway')
self.assertRaises(
requests.exceptions.HTTPError, self.nailgun.get_tasks)
class TestNailgunClientWithAuthentification(base.BaseTestCase):
token = {'access': {'token': {'id': 'auth_token'}}}
def setUp(self):
self.credentials = {
'username': 'some_user',
'password': 'some_password',
'auth_url': 'http://127.0.0.1:5000/v2',
'tenant_name': 'some_tenant'}
self.nailgun = nailgun_client.NailgunClient(
'http://127.0.0.1',
8000,
keystone_credentials=self.credentials)
@mock.patch('fuel_upgrade.nailgun_client.requests.post')
@mock.patch('fuel_upgrade.nailgun_client.requests.Session')
def test_makes_authenticated_requests(self, session, post_mock):
post_mock.return_value.json.return_value = self.token
self.nailgun.request.get('http://some.url/path')
session.return_value.headers.update.assert_called_once_with(
{'X-Auth-Token': 'auth_token'})
@mock.patch('fuel_upgrade.nailgun_client.requests.Session')
@mock.patch('fuel_upgrade.nailgun_client.requests.post',
side_effect=requests.exceptions.HTTPError(''))
def test_does_not_fail_without_keystone(self, _, __):
self.nailgun.request.get('http://some.url/path')
self.assertEqual(self.nailgun.get_token(), None)

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Mirantis, 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 mock
from fuel_upgrade.clients import OSTFClient
from fuel_upgrade.tests import base
class TestOSTFClient(base.BaseTestCase):
def setUp(self):
mock_keystone = mock.MagicMock()
self.mock_request = mock_keystone.request
with mock.patch(
'fuel_upgrade.clients.ostf_client.KeystoneClient',
return_value=mock_keystone):
self.ostf = OSTFClient('127.0.0.1', 8777)
def test_get(self):
self.ostf.get('/some_path')
self.mock_request.get.assert_called_once_with(
'http://127.0.0.1:8777/some_path')

View File

@ -17,15 +17,16 @@
import mock
import xmlrpclib
from fuel_upgrade.supervisor_client import SupervisorClient
from fuel_upgrade.clients import SupervisorClient
from fuel_upgrade.tests.base import BaseTestCase
@mock.patch('fuel_upgrade.supervisor_client.os')
@mock.patch('fuel_upgrade.clients.supervisor_client.os')
class TestSupervisorClient(BaseTestCase):
def setUp(self):
self.utils_patcher = mock.patch('fuel_upgrade.supervisor_client.utils')
self.utils_patcher = mock.patch(
'fuel_upgrade.clients.supervisor_client.utils')
self.utils_mock = self.utils_patcher.start()
self.supervisor = SupervisorClient(self.fake_config, '0')
@ -78,7 +79,7 @@ class TestSupervisorClient(BaseTestCase):
def test_generate_config(self, _):
config_path = '/config/path'
with mock.patch('fuel_upgrade.supervisor_client.os.path.join',
with mock.patch('fuel_upgrade.clients.supervisor_client.os.path.join',
return_value=config_path):
self.supervisor.generate_config(
{'service_name': 'service_name1', 'command': 'command1'})
@ -94,7 +95,7 @@ class TestSupervisorClient(BaseTestCase):
paths = ['script_path', '/path/cobbler_config', '']
self.supervisor.generate_config = mock.MagicMock()
with mock.patch(
'fuel_upgrade.supervisor_client.os.path.join',
'fuel_upgrade.clients.supervisor_client.os.path.join',
side_effect=paths):
self.supervisor.generate_cobbler_config(