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:
Thomas Herve 2015-05-20 10:39:49 -07:00
parent 273e9eaf37
commit cfd9b746e1
4 changed files with 202 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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