Add 'notifications' argument

This can be used for displaying and sending notifications to Fuel.

Send an error message:
fuel notify -m This is wrong --topic error
fuel notifications --send Hello world

List all unread notifications:
fuel notifications

List all notifications:
fuel notifications -a

Mark messages as read:
fuel notifications -r 1 2

Mark all messages as read:
fuel notifications -r '*'

DocImpact
Related-Bug: #1371757
Change-Id: I6a5f05febf8f5a01a7b9415546ef56c11aedefce
This commit is contained in:
Przemyslaw Kaminski 2015-01-28 11:21:49 +01:00
parent 2ea7b3e91c
commit 3355e188ee
6 changed files with 468 additions and 7 deletions

View File

@ -26,6 +26,8 @@ from fuelclient.cli.actions.interrupt import StopAction
from fuelclient.cli.actions.network import NetworkAction
from fuelclient.cli.actions.node import NodeAction
from fuelclient.cli.actions.nodegroup import NodeGroupAction
from fuelclient.cli.actions.notifications import NotificationsAction
from fuelclient.cli.actions.notifications import NotifyAction
from fuelclient.cli.actions.release import ReleaseAction
from fuelclient.cli.actions.role import RoleAction
from fuelclient.cli.actions.settings import SettingsAction
@ -51,7 +53,9 @@ actions_tuple = (
HealthCheckAction,
UserAction,
PluginAction,
NodeGroupAction
NodeGroupAction,
NotificationsAction,
NotifyAction
)
actions = dict(

View File

@ -0,0 +1,115 @@
# Copyright 2015 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 fuelclient.cli.actions.base import Action
import fuelclient.cli.arguments as Args
from fuelclient.cli.formatting import format_table
from fuelclient.objects.notifications import Notifications
class NotificationsAction(Action):
"""List and create notifications
"""
action_name = "notifications"
acceptable_keys = (
"id",
"message",
"status",
"topic",
)
def __init__(self):
super(NotificationsAction, self).__init__()
self.args = [
Args.group(
Args.get_list_arg("List all available notifications."),
Args.get_notify_send_arg("Send notification"),
Args.get_notify_mark_as_read_arg(
"Mark notification(s) as read ('*' to mark all as read)."),
),
Args.group(
Args.get_notify_topic_arg("Notification topic (severity)"),
),
Args.get_notify_all_messages_arg(
"Select all messages (only unread by default)."),
]
self.flag_func_map = (
("send", self.send),
("mark-as-read", self.mark_as_read),
(None, self.list),
)
def list(self, params):
"""Print all available notifications:
fuel notifications
fuel notifications --list
"""
notifications = Notifications.get_all_data()
if not params.all:
notifications = filter(
lambda notification: notification['status'] == 'unread',
notifications
)
self.serializer.print_to_output(
notifications,
format_table(
notifications,
acceptable_keys=self.acceptable_keys
)
)
def mark_as_read(self, params):
"""Mark given notifications as read.
fuel notifications --mark-as-read 1 2
fuel notifications -r 1 2
"""
result = Notifications.mark_as_read(
ids=getattr(params, 'mark-as-read'))
self.serializer.print_to_output(
result,
'Notification(s) marked as read'
)
def send(self, params):
"""Send notification:
fuel notifications --send "message" --topic done
"""
message = params.send
if isinstance(message, list):
message = ' '.join(message)
result = Notifications.send(message, topic=params.topic)
self.serializer.print_to_output(
result,
"Notification sent")
class NotifyAction(NotificationsAction):
"""Shortcut for quickly sending a notification.
"""
action_name = "notify"
def __init__(self):
super(NotifyAction, self).__init__()
self.args = [
Args.get_notify_message_arg("Notification message"),
Args.get_notify_topic_arg("Notification topic (severity)"),
]
self.flag_func_map = (
(None, self.send),
)

View File

@ -43,7 +43,7 @@ substitutions = {
def group(*args, **kwargs):
required = kwargs.get("required", False)
return (required, ) + args
return (required,) + args
class TaskAction(argparse.Action):
@ -146,8 +146,7 @@ def get_fuel_version_arg():
def get_arg(name, flags=None, aliases=None, help_=None, **kwargs):
if "_" in name:
name = name.replace("_", "-")
name = name.replace("_", "-")
args = ["--" + name, ]
if flags is not None:
args.extend(flags)
@ -305,7 +304,7 @@ def get_skip_tasks():
return get_arg(
'skip',
flags=('--skip',),
nargs = '+',
nargs='+',
default=[],
help="Get list of tasks to be skipped.")
@ -314,7 +313,7 @@ def get_tasks():
return get_arg(
'tasks',
flags=('--tasks',),
nargs = '+',
nargs='+',
default=[],
help="Get list of tasks to be executed.")
@ -430,3 +429,52 @@ def get_plugin_remove_arg(help_msg):
flags=("--remove",),
help=help_msg
)
def get_notify_all_messages_arg(help_msg):
return get_boolean_arg(
'all',
flags=('-a',),
help=help_msg
)
def get_notify_mark_as_read_arg(help_msg):
return get_str_arg(
"mark-as-read",
flags=('-r',),
nargs='+',
help=help_msg,
)
def get_notify_message_arg(help_msg):
return get_str_arg(
"send",
nargs='+',
flags=('-m',),
help=help_msg,
)
def get_notify_send_arg(help_msg):
return get_str_arg(
"send",
flags=("--send",),
help=help_msg
)
def get_notify_topic_arg(help_msg):
return get_str_arg(
"topic",
flags=("--topic",),
choices=(
'discover',
'done',
'error',
'warning',
'release'
),
help=help_msg
)

View File

@ -163,7 +163,7 @@ class Parser:
for flag in self.universal_flags:
self.move_argument_before_action(flag, has_value=False)
self.move_argument_after_action("--env", )
self.move_argument_after_action("--env",)
def move_argument_before_action(self, flag, has_value=True):
"""We need to move general argument before action, we use them

View File

@ -0,0 +1,67 @@
# Copyright 2015 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 fuelclient.cli import error
from fuelclient.objects import base
class Notifications(base.BaseObject):
class_api_path = "notifications/"
instance_api_path = "notifications/{0}"
default_topic = 'done'
@classmethod
def mark_as_read(cls, ids=None):
if not ids:
raise error.BadDataException('Message id not specified.')
if '*' in ids:
data = Notifications.get_all_data()
else:
try:
ids = map(int, ids)
except ValueError:
raise error.BadDataException(
"Numerical ids expected or the '*' symbol.")
notifications = Notifications.get_by_ids(ids)
data = [notification.get_fresh_data()
for notification in notifications]
for notification in data:
notification['status'] = 'read'
resp = cls.connection.put_request(
cls.class_api_path, data)
return resp
@classmethod
def send(cls, message, topic=default_topic):
if not topic:
topic = cls.default_topic
if not message:
raise error.BadDataException('Message not specified.')
resp = cls.connection.post_request(
cls.class_api_path, {
'message': message,
'topic': topic,
})
return resp

View File

@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 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
from mock import Mock
from mock import patch
from fuelclient.tests import base
@patch('fuelclient.client.requests')
class TestNotificationsActions(base.UnitTestCase):
def test_notification_send(self, mrequests):
response_mock = Mock(status_code=201)
mrequests.post.return_value = response_mock
self.execute_wo_auth(
['fuel', 'notifications', '--send', 'test message'])
self.assertEqual(mrequests.post.call_count, 1)
request = json.loads(mrequests.post.call_args[1]['data'])
self.assertEqual('test message', request['message'])
self.assertEqual('done', request['topic'])
self.execute_wo_auth(
['fuel', 'notify', '-m', 'test message 2'])
self.assertEqual(mrequests.post.call_count, 2)
request = json.loads(mrequests.post.call_args[1]['data'])
self.assertEqual('test message 2', request['message'])
self.assertEqual('done', request['topic'])
def test_notification_send_with_topic(self, mrequests):
response_mock = Mock(status_code=201)
mrequests.post.return_value = response_mock
self.execute_wo_auth(
['fuel', 'notifications', '--send', 'test error',
'--topic', 'error'])
self.assertEqual(mrequests.post.call_count, 1)
request = json.loads(mrequests.post.call_args[1]['data'])
self.assertEqual('test error', request['message'])
self.assertEqual('error', request['topic'])
self.execute_wo_auth(
['fuel', 'notify', '-m', 'test error 2', '--topic', 'error'])
self.assertEqual(mrequests.post.call_count, 2)
request = json.loads(mrequests.post.call_args[1]['data'])
self.assertEqual('test error 2', request['message'])
self.assertEqual('error', request['topic'])
def test_notification_send_no_message(self, mrequests):
response_mock = Mock(status_code=201)
mrequests.post.return_value = response_mock
self.assertRaises(
SystemExit,
self.execute_wo_auth,
['fuel', 'notifications', '--send']
)
self.assertEqual(mrequests.post.call_count, 0)
self.assertRaises(
SystemExit,
self.execute_wo_auth,
['fuel', 'notify', '-m']
)
self.assertEqual(mrequests.post.call_count, 0)
def test_notification_send_invalid_topic(self, mrequests):
response_mock = Mock(status_code=201)
mrequests.post.return_value = response_mock
self.assertRaises(
SystemExit,
self.execute_wo_auth,
['fuel', 'notifications', '--send', 'test message',
'--topic', 'x']
)
self.assertEqual(mrequests.post.call_count, 0)
self.assertRaises(
SystemExit,
self.execute_wo_auth,
['fuel', 'notify', '-m', 'test message', '--topic', 'x']
)
self.assertEqual(mrequests.post.call_count, 0)
def test_mark_as_read(self, mrequests):
m1 = Mock(status=200)
m1.json.return_value = {
'id': 1,
'message': 'test message',
'status': 'unread',
'topic': 'done',
}
m2 = Mock(status=200)
m2.json.return_value = {
'id': 2,
'message': 'test message 2',
'status': 'unread',
'topic': 'done',
}
mrequests.get.side_effect = [m1, m2]
mrequests.put.return_value = Mock(status_code=200)
self.execute_wo_auth(
['fuel', 'notifications', '-r', '1'])
self.assertEqual(mrequests.get.call_count, 1)
self.assertEqual(mrequests.put.call_count, 1)
request = m1.json.return_value
self.assertEqual('test message', request['message'])
self.assertEqual('read', request['status'])
request = m2.json.return_value
self.assertEqual('test message 2', request['message'])
self.assertEqual('unread', request['status'])
mrequests.get.side_effect = [m1, m2]
self.execute_wo_auth(
['fuel', 'notifications', '-r', '1', '2'])
self.assertEqual(mrequests.get.call_count, 3)
self.assertEqual(mrequests.put.call_count, 2)
request = m1.json.return_value
self.assertEqual('test message', request['message'])
self.assertEqual('read', request['status'])
request = m2.json.return_value
self.assertEqual('test message 2', request['message'])
self.assertEqual('read', request['status'])
def test_mark_all_as_read(self, mrequests):
m = Mock(status=200)
m.json.return_value = [
{
'id': 1,
'message': 'test message',
'status': 'unread',
'topic': 'done',
},
{
'id': 2,
'message': 'test message 2',
'status': 'unread',
'topic': 'done',
}
]
mrequests.get.return_value = m
mrequests.put.return_value = Mock(status_code=200)
self.execute_wo_auth(
['fuel', 'notifications', '-r', '*'])
self.assertEqual(mrequests.get.call_count, 1)
self.assertEqual(mrequests.put.call_count, 1)
request = m.json.return_value
self.assertEqual('test message', request[0]['message'])
self.assertEqual('read', request[0]['status'])
self.assertEqual('test message 2', request[1]['message'])
self.assertEqual('read', request[1]['status'])
@patch('fuelclient.cli.actions.notifications.format_table')
def test_list_notifications(self, mformat_table, mrequests):
m = Mock(status=200)
m.json.return_value = [
{
'id': 1,
'message': 'test message',
'status': 'unread',
'topic': 'done',
},
{
'id': 2,
'message': 'test message 2',
'status': 'read',
'topic': 'done',
}
]
mrequests.get.return_value = m
mrequests.put.return_value = Mock(status_code=200)
self.execute_wo_auth(['fuel', 'notifications'])
self.assertEqual(mrequests.get.call_count, 1)
notifications = mformat_table.call_args[0][0]
self.assertEqual(len(notifications), 1)
self.assertDictEqual(notifications[0], m.json.return_value[0])
@patch('fuelclient.cli.actions.notifications.format_table')
def test_list_all_notifications(self, mformat_table, mrequests):
m = Mock(status=200)
m.json.return_value = [
{
'id': 1,
'message': 'test message',
'status': 'unread',
'topic': 'done',
},
{
'id': 2,
'message': 'test message 2',
'status': 'read',
'topic': 'done',
}
]
mrequests.get.return_value = m
mrequests.put.return_value = Mock(status_code=200)
self.execute_wo_auth(['fuel', 'notifications', '-a'])
self.assertEqual(mrequests.get.call_count, 1)
notifications = mformat_table.call_args[0][0]
self.assertEqual(len(notifications), 2)
self.assertListEqual(notifications, m.json.return_value)