Optionally create trust for alarm actions
When creating actions using TrustRestAlarmNotifier, allow the absence of trust ID and automatically creates a trust in this case for the ceilometer service user. This enables creation of trust alarms without knowing the ceilometer service user ID outside of ceilometer itself. blueprint trust-alarm-notifier Change-Id: I4b781cbdd46dd4574fea44b40adad869373ab344
This commit is contained in:
parent
273e9eaf37
commit
cfd9b746e1
@ -14,11 +14,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Rest alarm notifier with trusted authentication."""
|
"""Rest alarm notifier with trusted authentication."""
|
||||||
|
|
||||||
from keystoneclient.v3 import client as keystone_client
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from six.moves.urllib import parse
|
from six.moves.urllib import parse
|
||||||
|
|
||||||
from ceilometer.alarm.notifier import rest
|
from ceilometer.alarm.notifier import rest
|
||||||
|
from ceilometer import keystone_client
|
||||||
|
|
||||||
|
|
||||||
cfg.CONF.import_opt('http_timeout', 'ceilometer.service')
|
cfg.CONF.import_opt('http_timeout', 'ceilometer.service')
|
||||||
@ -40,17 +40,7 @@ class TrustRestAlarmNotifier(rest.RestAlarmNotifier):
|
|||||||
reason, reason_data):
|
reason, reason_data):
|
||||||
trust_id = action.username
|
trust_id = action.username
|
||||||
|
|
||||||
auth_url = cfg.CONF.service_credentials.os_auth_url.replace(
|
client = keystone_client.get_v3_client(trust_id)
|
||||||
"v2.0", "v3")
|
|
||||||
client = keystone_client.Client(
|
|
||||||
username=cfg.CONF.service_credentials.os_username,
|
|
||||||
password=cfg.CONF.service_credentials.os_password,
|
|
||||||
cacert=cfg.CONF.service_credentials.os_cacert,
|
|
||||||
auth_url=auth_url,
|
|
||||||
region_name=cfg.CONF.service_credentials.os_region_name,
|
|
||||||
insecure=cfg.CONF.service_credentials.insecure,
|
|
||||||
timeout=cfg.CONF.http_timeout,
|
|
||||||
trust_id=trust_id)
|
|
||||||
|
|
||||||
# Remove the fake user
|
# Remove the fake user
|
||||||
netloc = action.netloc.split("@")[1]
|
netloc = action.netloc.split("@")[1]
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ import pecan
|
|||||||
from pecan import rest
|
from pecan import rest
|
||||||
import pytz
|
import pytz
|
||||||
import six
|
import six
|
||||||
|
from six.moves.urllib import parse as urlparse
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
import wsme
|
import wsme
|
||||||
from wsme import types as wtypes
|
from wsme import types as wtypes
|
||||||
@ -44,6 +46,7 @@ from ceilometer.api.controllers.v2 import base
|
|||||||
from ceilometer.api.controllers.v2 import utils as v2_utils
|
from ceilometer.api.controllers.v2 import utils as v2_utils
|
||||||
from ceilometer.api import rbac
|
from ceilometer.api import rbac
|
||||||
from ceilometer.i18n import _
|
from ceilometer.i18n import _
|
||||||
|
from ceilometer import keystone_client
|
||||||
from ceilometer import messaging
|
from ceilometer import messaging
|
||||||
from ceilometer.openstack.common import log
|
from ceilometer.openstack.common import log
|
||||||
from ceilometer import utils
|
from ceilometer import utils
|
||||||
@ -378,6 +381,52 @@ class Alarm(base.Base):
|
|||||||
for tc in self.time_constraints]
|
for tc in self.time_constraints]
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_trust_url(url):
|
||||||
|
return url.scheme in ('trust+http', 'trust+https')
|
||||||
|
|
||||||
|
def update_actions(self, old_alarm=None):
|
||||||
|
trustor_user_id = pecan.request.headers.get('X-User-Id')
|
||||||
|
trustor_project_id = pecan.request.headers.get('X-Project-Id')
|
||||||
|
roles = pecan.request.headers.get('X-Roles', '')
|
||||||
|
if roles:
|
||||||
|
roles = roles.split(',')
|
||||||
|
else:
|
||||||
|
roles = []
|
||||||
|
auth_plugin = pecan.request.environ.get('keystone.token_auth')
|
||||||
|
for actions in (self.ok_actions, self.alarm_actions,
|
||||||
|
self.insufficient_data_actions):
|
||||||
|
for index, action in enumerate(actions[:]):
|
||||||
|
url = netutils.urlsplit(action)
|
||||||
|
if self._is_trust_url(url):
|
||||||
|
if '@' not in url.netloc:
|
||||||
|
# We have a trust action without a trust ID, create it
|
||||||
|
trust_id = keystone_client.create_trust_id(
|
||||||
|
trustor_user_id, trustor_project_id, roles,
|
||||||
|
auth_plugin)
|
||||||
|
netloc = '%s:delete@%s' % (trust_id, url.netloc)
|
||||||
|
url = list(url)
|
||||||
|
url[1] = netloc
|
||||||
|
actions[index] = urlparse.urlunsplit(url)
|
||||||
|
if old_alarm:
|
||||||
|
for key in ('ok_actions', 'alarm_actions',
|
||||||
|
'insufficient_data_actions'):
|
||||||
|
for action in getattr(old_alarm, key):
|
||||||
|
url = netutils.urlsplit(action)
|
||||||
|
if (self._is_trust_url(url) and url.password and
|
||||||
|
action not in getattr(self, key)):
|
||||||
|
keystone_client.delete_trust_id(
|
||||||
|
url.username, auth_plugin)
|
||||||
|
|
||||||
|
def delete_actions(self):
|
||||||
|
auth_plugin = pecan.request.environ.get('keystone.token_auth')
|
||||||
|
for action in itertools.chain(self.ok_actions, self.alarm_actions,
|
||||||
|
self.insufficient_data_actions):
|
||||||
|
url = netutils.urlsplit(action)
|
||||||
|
if self._is_trust_url(url) and url.password:
|
||||||
|
keystone_client.delete_trust_id(url.username, auth_plugin)
|
||||||
|
|
||||||
|
|
||||||
Alarm.add_attributes(**{"%s_rule" % ext.name: ext.plugin
|
Alarm.add_attributes(**{"%s_rule" % ext.name: ext.plugin
|
||||||
for ext in ALARMS_RULES})
|
for ext in ALARMS_RULES})
|
||||||
|
|
||||||
@ -533,7 +582,9 @@ class AlarmController(rest.RestController):
|
|||||||
|
|
||||||
ALARMS_RULES[data.type].plugin.update_hook(data)
|
ALARMS_RULES[data.type].plugin.update_hook(data)
|
||||||
|
|
||||||
old_alarm = Alarm.from_db_model(alarm_in).as_dict(alarm_models.Alarm)
|
old_data = Alarm.from_db_model(alarm_in)
|
||||||
|
old_alarm = old_data.as_dict(alarm_models.Alarm)
|
||||||
|
data.update_actions(old_data)
|
||||||
updated_alarm = data.as_dict(alarm_models.Alarm)
|
updated_alarm = data.as_dict(alarm_models.Alarm)
|
||||||
try:
|
try:
|
||||||
alarm_in = alarm_models.Alarm(**updated_alarm)
|
alarm_in = alarm_models.Alarm(**updated_alarm)
|
||||||
@ -558,7 +609,9 @@ class AlarmController(rest.RestController):
|
|||||||
# ensure alarm exists before deleting
|
# ensure alarm exists before deleting
|
||||||
alarm = self._alarm()
|
alarm = self._alarm()
|
||||||
self.conn.delete_alarm(alarm.alarm_id)
|
self.conn.delete_alarm(alarm.alarm_id)
|
||||||
change = Alarm.from_db_model(alarm).as_dict(alarm_models.Alarm)
|
alarm_object = Alarm.from_db_model(alarm)
|
||||||
|
alarm_object.delete_actions()
|
||||||
|
change = alarm_object.as_dict(alarm_models.Alarm)
|
||||||
self._record_change(change,
|
self._record_change(change,
|
||||||
timeutils.utcnow(),
|
timeutils.utcnow(),
|
||||||
type=alarm_models.AlarmChange.DELETION)
|
type=alarm_models.AlarmChange.DELETION)
|
||||||
@ -693,6 +746,7 @@ class AlarmsController(rest.RestController):
|
|||||||
|
|
||||||
ALARMS_RULES[data.type].plugin.create_hook(data)
|
ALARMS_RULES[data.type].plugin.create_hook(data)
|
||||||
|
|
||||||
|
data.update_actions()
|
||||||
change = data.as_dict(alarm_models.Alarm)
|
change = data.as_dict(alarm_models.Alarm)
|
||||||
|
|
||||||
# make sure alarms are unique by name per project.
|
# make sure alarms are unique by name per project.
|
||||||
|
@ -14,7 +14,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
from keystoneclient.v2_0 import client as ksclient
|
from keystoneclient import discover as ks_discover
|
||||||
|
from keystoneclient import exceptions as ks_exception
|
||||||
|
from keystoneclient import session as ks_session
|
||||||
|
from keystoneclient.v2_0 import client as ks_client
|
||||||
|
from keystoneclient.v3 import client as ks_client_v3
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
cfg.CONF.import_group('service_credentials', 'ceilometer.service')
|
cfg.CONF.import_group('service_credentials', 'ceilometer.service')
|
||||||
@ -22,7 +26,7 @@ cfg.CONF.import_opt('http_timeout', 'ceilometer.service')
|
|||||||
|
|
||||||
|
|
||||||
def get_client():
|
def get_client():
|
||||||
return ksclient.Client(
|
return ks_client.Client(
|
||||||
username=cfg.CONF.service_credentials.os_username,
|
username=cfg.CONF.service_credentials.os_username,
|
||||||
password=cfg.CONF.service_credentials.os_password,
|
password=cfg.CONF.service_credentials.os_password,
|
||||||
tenant_id=cfg.CONF.service_credentials.os_tenant_id,
|
tenant_id=cfg.CONF.service_credentials.os_tenant_id,
|
||||||
@ -32,3 +36,60 @@ def get_client():
|
|||||||
region_name=cfg.CONF.service_credentials.os_region_name,
|
region_name=cfg.CONF.service_credentials.os_region_name,
|
||||||
insecure=cfg.CONF.service_credentials.insecure,
|
insecure=cfg.CONF.service_credentials.insecure,
|
||||||
timeout=cfg.CONF.http_timeout,)
|
timeout=cfg.CONF.http_timeout,)
|
||||||
|
|
||||||
|
|
||||||
|
def get_v3_client(trust_id=None):
|
||||||
|
"""Return a client for keystone v3 endpoint, optionally using a trust."""
|
||||||
|
auth_url = cfg.CONF.service_credentials.os_auth_url
|
||||||
|
try:
|
||||||
|
auth_url_noneversion = auth_url.replace('/v2.0', '/')
|
||||||
|
discover = ks_discover.Discover(auth_url=auth_url_noneversion)
|
||||||
|
v3_auth_url = discover.url_for('3.0')
|
||||||
|
if v3_auth_url:
|
||||||
|
auth_url = v3_auth_url
|
||||||
|
else:
|
||||||
|
auth_url = auth_url
|
||||||
|
except Exception:
|
||||||
|
auth_url = auth_url.replace('/v2.0', '/v3')
|
||||||
|
return ks_client_v3.Client(
|
||||||
|
username=cfg.CONF.service_credentials.os_username,
|
||||||
|
password=cfg.CONF.service_credentials.os_password,
|
||||||
|
cacert=cfg.CONF.service_credentials.os_cacert,
|
||||||
|
auth_url=auth_url,
|
||||||
|
region_name=cfg.CONF.service_credentials.os_region_name,
|
||||||
|
insecure=cfg.CONF.service_credentials.insecure,
|
||||||
|
timeout=cfg.CONF.http_timeout,
|
||||||
|
trust_id=trust_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_trust_id(trustor_user_id, trustor_project_id, roles, auth_plugin):
|
||||||
|
"""Create a new trust using the ceilometer service user."""
|
||||||
|
admin_client = get_v3_client()
|
||||||
|
|
||||||
|
trustee_user_id = admin_client.auth_ref.user_id
|
||||||
|
|
||||||
|
session = ks_session.Session.construct({
|
||||||
|
'cacert': cfg.CONF.service_credentials.os_cacert,
|
||||||
|
'insecure': cfg.CONF.service_credentials.insecure})
|
||||||
|
|
||||||
|
client = ks_client_v3.Client(session=session, auth=auth_plugin)
|
||||||
|
|
||||||
|
trust = client.trusts.create(trustor_user=trustor_user_id,
|
||||||
|
trustee_user=trustee_user_id,
|
||||||
|
project=trustor_project_id,
|
||||||
|
impersonation=True,
|
||||||
|
role_names=roles)
|
||||||
|
return trust.id
|
||||||
|
|
||||||
|
|
||||||
|
def delete_trust_id(trust_id, auth_plugin):
|
||||||
|
"""Delete a trust previously setup for the ceilometer user."""
|
||||||
|
session = ks_session.Session.construct({
|
||||||
|
'cacert': cfg.CONF.service_credentials.os_cacert,
|
||||||
|
'insecure': cfg.CONF.service_credentials.insecure})
|
||||||
|
|
||||||
|
client = ks_client_v3.Client(session=session, auth=auth_plugin)
|
||||||
|
try:
|
||||||
|
client.trusts.delete(trust_id)
|
||||||
|
except ks_exception.NotFound:
|
||||||
|
pass
|
||||||
|
@ -1792,6 +1792,54 @@ class TestAlarms(v2.FunctionalTest,
|
|||||||
self.assertEqual(['test://', 'log://'],
|
self.assertEqual(['test://', 'log://'],
|
||||||
alarms[0].alarm_actions)
|
alarms[0].alarm_actions)
|
||||||
|
|
||||||
|
def test_post_alarm_trust(self):
|
||||||
|
json = {
|
||||||
|
'name': 'added_alarm_defaults',
|
||||||
|
'type': 'threshold',
|
||||||
|
'ok_actions': ['trust+http://my.server:1234/foo'],
|
||||||
|
'threshold_rule': {
|
||||||
|
'meter_name': 'ameter',
|
||||||
|
'threshold': 300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth = mock.Mock()
|
||||||
|
trust_client = mock.Mock()
|
||||||
|
with mock.patch('ceilometer.keystone_client.get_v3_client') as client:
|
||||||
|
client.return_value = mock.Mock(
|
||||||
|
auth_ref=mock.Mock(user_id='my_user'))
|
||||||
|
with mock.patch('keystoneclient.v3.client.Client') as sub_client:
|
||||||
|
sub_client.return_value = trust_client
|
||||||
|
trust_client.trusts.create.return_value = mock.Mock(id='5678')
|
||||||
|
self.post_json('/alarms', params=json, status=201,
|
||||||
|
headers=self.auth_headers,
|
||||||
|
extra_environ={'keystone.token_auth': auth})
|
||||||
|
trust_client.trusts.create.assert_called_once_with(
|
||||||
|
trustor_user=self.auth_headers['X-User-Id'],
|
||||||
|
trustee_user='my_user',
|
||||||
|
project=self.auth_headers['X-Project-Id'],
|
||||||
|
impersonation=True,
|
||||||
|
role_names=[])
|
||||||
|
alarms = list(self.alarm_conn.get_alarms())
|
||||||
|
for alarm in alarms:
|
||||||
|
if alarm.name == 'added_alarm_defaults':
|
||||||
|
self.assertEqual(
|
||||||
|
['trust+http://5678:delete@my.server:1234/foo'],
|
||||||
|
alarm.ok_actions)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.fail("Alarm not found")
|
||||||
|
|
||||||
|
with mock.patch('ceilometer.keystone_client.get_v3_client') as client:
|
||||||
|
client.return_value = mock.Mock(
|
||||||
|
auth_ref=mock.Mock(user_id='my_user'))
|
||||||
|
with mock.patch('keystoneclient.v3.client.Client') as sub_client:
|
||||||
|
sub_client.return_value = trust_client
|
||||||
|
self.delete('/alarms/%s' % alarm.alarm_id,
|
||||||
|
headers=self.auth_headers,
|
||||||
|
status=204,
|
||||||
|
extra_environ={'keystone.token_auth': auth})
|
||||||
|
trust_client.trusts.delete.assert_called_once_with('5678')
|
||||||
|
|
||||||
def test_put_alarm(self):
|
def test_put_alarm(self):
|
||||||
json = {
|
json = {
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
@ -2083,6 +2131,39 @@ class TestAlarms(v2.FunctionalTest,
|
|||||||
self.assertEqual(1, len(alarms))
|
self.assertEqual(1, len(alarms))
|
||||||
self.assertEqual(['c', 'a', 'b'], alarms[0].rule.get('alarm_ids'))
|
self.assertEqual(['c', 'a', 'b'], alarms[0].rule.get('alarm_ids'))
|
||||||
|
|
||||||
|
def test_put_alarm_trust(self):
|
||||||
|
data = self._get_alarm('a')
|
||||||
|
data.update({'ok_actions': ['trust+http://something/ok']})
|
||||||
|
trust_client = mock.Mock()
|
||||||
|
with mock.patch('ceilometer.keystone_client.get_v3_client') as client:
|
||||||
|
client.return_value = mock.Mock(
|
||||||
|
auth_ref=mock.Mock(user_id='my_user'))
|
||||||
|
with mock.patch('keystoneclient.v3.client.Client') as sub_client:
|
||||||
|
sub_client.return_value = trust_client
|
||||||
|
trust_client.trusts.create.return_value = mock.Mock(id='5678')
|
||||||
|
self.put_json('/alarms/%s' % data['alarm_id'],
|
||||||
|
params=data,
|
||||||
|
headers=self.auth_headers)
|
||||||
|
data = self._get_alarm('a')
|
||||||
|
self.assertEqual(
|
||||||
|
['trust+http://5678:delete@something/ok'], data['ok_actions'])
|
||||||
|
|
||||||
|
data.update({'ok_actions': ['http://no-trust-something/ok']})
|
||||||
|
|
||||||
|
with mock.patch('ceilometer.keystone_client.get_v3_client') as client:
|
||||||
|
client.return_value = mock.Mock(
|
||||||
|
auth_ref=mock.Mock(user_id='my_user'))
|
||||||
|
with mock.patch('keystoneclient.v3.client.Client') as sub_client:
|
||||||
|
sub_client.return_value = trust_client
|
||||||
|
self.put_json('/alarms/%s' % data['alarm_id'],
|
||||||
|
params=data,
|
||||||
|
headers=self.auth_headers)
|
||||||
|
trust_client.trusts.delete.assert_called_once_with('5678')
|
||||||
|
|
||||||
|
data = self._get_alarm('a')
|
||||||
|
self.assertEqual(
|
||||||
|
['http://no-trust-something/ok'], data['ok_actions'])
|
||||||
|
|
||||||
def test_delete_alarm(self):
|
def test_delete_alarm(self):
|
||||||
data = self.get_json('/alarms')
|
data = self.get_json('/alarms')
|
||||||
self.assertEqual(7, len(data))
|
self.assertEqual(7, len(data))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user