From: Tzu-Mainn Chen <tzumainn(a)redhat.com>
---
src/app/controllers/application_controller.rb | 1 +
src/app/controllers/notifications_controller.rb | 64 ++++++++++++
src/app/models/deployment.rb | 17 +++-
src/app/models/instance.rb | 10 ++
src/app/models/instance_observer.rb | 52 ++++++++---
src/app/models/notification.rb | 68 +++++++++++++
src/app/models/user_notification.rb | 32 ++++++
src/app/views/notifications/index.html.haml | 20 ++++
src/config/application.rb | 1 +
src/config/locales/en.yml | 48 +++++++++
src/config/routes.rb | 6 +
.../migrate/20120920130441_create_notifications.rb | 28 +++++
src/lib/notifications/notifier.rb | 105 ++++++++++++++++++++
src/lib/notify.rb | 13 +++
14 files changed, 451 insertions(+), 14 deletions(-)
create mode 100644 src/app/controllers/notifications_controller.rb
create mode 100644 src/app/models/notification.rb
create mode 100644 src/app/models/user_notification.rb
create mode 100644 src/app/views/notifications/index.html.haml
create mode 100644 src/db/migrate/20120920130441_create_notifications.rb
create mode 100644 src/lib/notifications/notifier.rb
create mode 100644 src/lib/notify.rb
diff --git a/src/app/controllers/application_controller.rb
b/src/app/controllers/application_controller.rb
index 0c02521..3b9c425 100644
--- a/src/app/controllers/application_controller.rb
+++ b/src/app/controllers/application_controller.rb
@@ -24,6 +24,7 @@ require 'will_paginate/array'
class ApplicationController < ActionController::Base
# FIXME: not sure what we're doing aobut service layer w/ deltacloud
include ApplicationService
+ include Notifications
helper_method :current_session, :current_user, :filter_view?
before_filter :read_breadcrumbs, :set_locale, :check_session_expiration
diff --git a/src/app/controllers/notifications_controller.rb
b/src/app/controllers/notifications_controller.rb
new file mode 100644
index 0000000..dc1187e
--- /dev/null
+++ b/src/app/controllers/notifications_controller.rb
@@ -0,0 +1,64 @@
+#
+# Copyright 2012 Red Hat, 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.
+#
+
+class NotificationsController < ApplicationController
+ before_filter :require_user
+
+ def index
+ clear_breadcrumbs
+ save_breadcrumb(notifications_path)
+
+ load_notifications
+ load_headers
+
+ respond_to do |format|
+ format.html
+ format.js
+ end
+ end
+
+ def filter
+ redirect_to_original({ "source_type" => params[:source_type] })
+ end
+
+ protected
+
+ def load_notifications
+ conditions = ["exists (select 1 from user_notifications
+ where user_id = ?
+ and notification_id = notifications.id)",
+ current_user.id
+ ]
+
+ @notifications = Notification.find(:all,
+ :include => :source,
+ :conditions => conditions,
+ :order => "created_at desc"
+ )
+
+ @paginated_notifications = paginate_collection(@notifications, params[:page],
PER_PAGE)
+ end
+
+ def load_headers
+ @header = [
+ { :name => t('notifications.index.notification_time'), :sortable =>
false },
+ { :name => t('notifications.index.level'), :sortable => false },
+ { :name => t('notifications.index.source'), :sortable => false },
+ { :name => t('notifications.index.message'), :sortable => false },
+ { :name => t('notifications.index.details'), :sortable => false },
+ ]
+ end
+end
diff --git a/src/app/models/deployment.rb b/src/app/models/deployment.rb
index dd833b6..05680ed 100644
--- a/src/app/models/deployment.rb
+++ b/src/app/models/deployment.rb
@@ -242,13 +242,18 @@ class Deployment < ActiveRecord::Base
all_inst_match, account, errs = pick_provider_selection_match
if all_inst_match
+ summary = I18n.t('deployments.events.launch_attempt',
+ :account => account.name)
self.events << Event.create(
:source => self,
:event_time => DateTime.now,
:status_code => 'deployment_launch_match',
- :summary => I18n.t('deployments.events.launch_attempt',
- :account => account.name)
+ :summary => summary
)
+ Notify.notice('notifications.deployments.launch_attempt',
+ :details => summary,
+ :source => self,
+ :user => user)
else
if errs.any?
raise I18n.t('deployments.errors.match_not_found_with_errors',
@@ -775,6 +780,9 @@ class Deployment < ActiveRecord::Base
:summary => I18n.t('deployments.events.state_changed',
:state => self.state)
)
+ Notify.notice("notifications.deployments.#{self.state}_state_change",
+ :source => self,
+ :user => self.owner)
end
end
@@ -794,6 +802,11 @@ class Deployment < ActiveRecord::Base
:summary => I18n.t('deployments.errors.launch_failed'),
:description => $!.message
)
+ Notify.error('notifications.deployments.launch_failed',
+ :source => self,
+ :details => $!.message,
+ :user => self.owner)
+
update_attribute(:state, STATE_FAILED)
cleanup_failed_launch
logger.error $!.message
diff --git a/src/app/models/instance.rb b/src/app/models/instance.rb
index 8e02002..a011016 100644
--- a/src/app/models/instance.rb
+++ b/src/app/models/instance.rb
@@ -225,6 +225,9 @@ class Instance < ActiveRecord::Base
event = Event.create!(:source => self, :event_time => Time.now,
:summary => "#{action} action queued",
:status_code => "#{action}_queued")
+ Notify.notice("notifications.instances.#{action}_action_queued",
+ :source => self,
+ :user => user)
return task
end
@@ -473,6 +476,10 @@ class Instance < ActiveRecord::Base
:summary => "Failed to stop instance #{self.name}",
:description => $!.message
)
+ Notify.error("notifications.instances.stop_failed",
+ :details => $!.message,
+ :source => self,
+ :user => user)
logger.error $!.message
logger.error $!.backtrace.join("\n ")
false
@@ -493,6 +500,9 @@ class Instance < ActiveRecord::Base
event = Event.create!(:source => self, :event_time => Time.now,
:summary => "Instance is not accessible, state changed
to stopped",
:status_code => "forced_stop")
+ Notify.warning("notifications.instances.forced_stop",
+ :source => self,
+ :user => user)
end
def deployed?
diff --git a/src/app/models/instance_observer.rb b/src/app/models/instance_observer.rb
index d42fbe6..f19dfed 100644
--- a/src/app/models/instance_observer.rb
+++ b/src/app/models/instance_observer.rb
@@ -89,6 +89,9 @@ class InstanceObserver < ActiveRecord::Observer
event = Event.new(:source => instance, :event_time => instance.created_at,
:summary => "created")
event.save!
+ Notify.success("notifications.instances.created",
+ :source => instance,
+ :user => instance.owner)
end
def after_update(instance)
@@ -99,31 +102,56 @@ class InstanceObserver < ActiveRecord::Observer
:summary => "state changed to #{instance.state}",
:change_hash => instance.changes, :status_code =>
instance.state)
event.save!
+ Notify.notice("notifications.instances.#{instance.state}_state_change",
+ :source => instance,
+ :user => instance.owner)
if instance.state == Instance::STATE_RUNNING && instance.deployment
- event2 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
- :status_code => "first_running", :summary =>
"1st instance is running",
- :change_hash => instance.changes) if instance.first_running?
- event3 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
- :status_code => "all_running", :summary =>
"All instances are running",
- :change_hash => instance.changes) if
instance.deployment.all_instances_running?
- event2.save! if event2
- event3.save! if event3
+ if instance.first_running?
+ event2 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
+ :status_code => "first_running", :summary =>
"1st instance is running",
+ :change_hash => instance.changes)
+ event2.save!
+ Notify.success("notifications.deployments.first_instance_running",
+ :source => instance.deployment,
+ :user => instance.deployment.owner)
+ end
+ if instance.deployment.all_instances_running?
+ event3 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
+ :status_code => "all_running", :summary =>
"All instances are running",
+ :change_hash => instance.changes)
+ event3.save!
+ Notify.success("notifications.deployments.all_instances_running",
+ :source => instance.deployment,
+ :user => instance.deployment.owner)
+ end
elsif (instance.state == Instance::STATE_STOPPED || instance.state ==
Instance::STATE_ERROR) && instance.deployment
if instance.deployment.any_instance_running? &&
!(instance.deployment.events.empty?)
- event4 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
- :status_code => "some_stopped", :summary =>
"Some instances are stopped",
- :change_hash => instance.changes) if
instance.deployment.events.lifetime.last{|e|e.status_code == :all_running}
+ if instance.deployment.events.lifetime.last{|e|e.status_code == :all_running}
+ event4 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
+ :status_code => "some_stopped", :summary
=> "Some instances are stopped",
+ :change_hash => instance.changes)
+ event4.save!
+ Notify.notice("notifications.deployments.some_instances_stopped",
+ :source => instance.deployment,
+ :user => instance.deployment.owner)
+ end
else
event4 = Event.new(:source => instance.deployment, :event_time =>
DateTime.now,
:status_code => "all_stopped", :summary =>
"All instances are stopped",
:change_hash => instance.changes)
+ event4.save!
+ Notify.notice("notifications.deployments.all_instances_stopped",
+ :source => instance.deployment,
+ :user => instance.deployment.owner)
end
- event4.save! if event4
elsif instance.state == Instance::STATE_VANISHED
Event.create!(
:source => instance, :event_time => Time.now,
:status_code => 'vanished', :summary => "Instance
disappeared from cloud provider"
)
+ Notify.error('notifications.instances.vanished',
+ :source => instance,
+ :user => instance.owner)
end
instance.deployment.update_state(instance) if instance.deployment
diff --git a/src/app/models/notification.rb b/src/app/models/notification.rb
new file mode 100644
index 0000000..c5e9b66
--- /dev/null
+++ b/src/app/models/notification.rb
@@ -0,0 +1,68 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+#
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+class Notification < ActiveRecord::Base
+
+ has_many :user_notifications
+ has_many :users, :through => :user_notifications
+ belongs_to :source, :polymorphic => true, :conditions => '1=1'
+
+ LEVELS = [:notice, :warning, :success, :error]
+
+ validates_inclusion_of :level, :in => LEVELS + LEVELS.collect{|type| type.to_s}
+ validates_presence_of :source_id
+ validates_presence_of :source_type
+ validates_presence_of :key
+ validates_length_of :user_notifications, :minimum => 1
+
+ before_validation :set_default_notice_level
+
+ scope :readable, lambda { |user| joins(:users).where('users.id' => user) }
+ scope :read, lambda { viewed true }
+ scope :unread, lambda { viewed false }
+
+ def source
+ case source_type
+ when "Deployment" then Deployment.unscoped.find(source_id)
+ when "Instance" then Instance.unscoped.find(source_id)
+ else super
+ end
+ end
+
+ def to_s
+ "#{level}: #{key} #{source.name}"
+ end
+
+ def check_permissions operation
+ logger.debug "CHECKING #{operation}"
+ # anybody can create notices
+ return true if operation == :create
+ if operation == :update or operation == :destroy
+ # TODO: who is a real owner of a notice?
+ end
+ false
+ end
+
+ def message
+ if source.nil?
+ I18n.t("notifications.errors.missing_source")
+ else
+ I18n.t(key, :name => source.name)
+ end
+ end
+
+ private
+
+ def set_default_notice_level
+ self.level ||= TYPES.first
+ end
+end
diff --git a/src/app/models/user_notification.rb b/src/app/models/user_notification.rb
new file mode 100644
index 0000000..80f9e74
--- /dev/null
+++ b/src/app/models/user_notification.rb
@@ -0,0 +1,32 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+#
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+class UserNotification < ActiveRecord::Base
+
+ belongs_to :user
+ belongs_to :notification
+
+ def check_permissions operation
+ # anybody can create user_notice relationships
+ return true if operation == :create
+ # only notice owner can update or destroy
+ if operation == :update or operation == :destroy
+ return true if user.id and User.current and user.id == User.current.id
+ end
+ false
+ end
+
+ def read!
+ update_attributes! :viewed => true
+ end
+
+end
diff --git a/src/app/views/notifications/index.html.haml
b/src/app/views/notifications/index.html.haml
new file mode 100644
index 0000000..5133e9c
--- /dev/null
+++ b/src/app/views/notifications/index.html.haml
@@ -0,0 +1,20 @@
+= render :partial => 'layouts/nav_history'
+
+%header.page-header
+ %h1.section-index= t('notifications.index.title')
+
+%section.content-section.toggle-view.notifications
+ - content_for :filter_controls do
+ %li
+ = label_tag "source_type", t('filter_table.viewing')
+ = hidden_field_tag :current_path, request.fullpath
+ = restful_submit_tag t("filter_table.apply_filters"), "index",
filter_notifications_path, 'POST', :class => 'button', :id =>
'apply_notifications_filter'
+ %span.label.badge.dark= @notifications.count
+ = filter_table(@header, @paginated_notifications) do |notification|
+ - source = notification.source
+ %tr{:class => cycle('nostripe','stripe')}
+ %td= notification.created_at.strftime("%d-%b-%Y %H:%M:%S")
+ %td= notification.level
+ %td= source.nil? ? t('notifications.index.not_available') :
link_to(source.class.to_s + " (" + source.name + ")",
polymorphic_path(source))
+ %td= notification.message
+ %td= notification.details
diff --git a/src/config/application.rb b/src/config/application.rb
index 88d6d1d..88606a5 100644
--- a/src/config/application.rb
+++ b/src/config/application.rb
@@ -38,6 +38,7 @@ module Conductor
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
+ config.autoload_paths += %W(#{Rails.root}/lib)
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
diff --git a/src/config/locales/en.yml b/src/config/locales/en.yml
index 1955d2d..60e75b6 100644
--- a/src/config/locales/en.yml
+++ b/src/config/locales/en.yml
@@ -12,6 +12,53 @@ en:
no_results: "No matching results."
activity:
activity: Activity
+ notifications:
+ index:
+ title: Notifications
+ notification_time: Time
+ source: Source
+ message: Message
+ details: Details
+ not_available: N/A
+ level: Level
+ deployments:
+ launch_attempt: Attempting to launch deployment %{name}
+ launch_failed: Failed to launch deployment %{name}
+ first_instance_running: First instance running for deployment %{name}
+ all_instances_running: All instances running for deployment %{name}
+ some_instances_stopped: Some instances stopped for deployment %{name}
+ all_instances_stopped: All instances stopped for deployment %{name}
+ new_state_change: Deployment %{name} created
+ pending_state_change: Deployment %{name} pending
+ running_state_change: Deployment %{name} running
+ shutting_down_state_change: Deployment %{name} shutting down
+ stopped_state_change: Deployment %{name} stopped
+ failed_state_change: Deployment %{name} failed
+ incomplete_state_change: Deployment %{name} incomplete
+ rollback_in_progress: Deployment %{name} rollback in progress
+ rollback_complete: Deployment %{name} rollback complete
+ rollback_failed: Deployment %{name} rollback failed
+ instances:
+ created: Instance %{name} created
+ new_state_change: Instance %{name} state changed to new
+ pending_state_change: Instance %{name} state changed to pending
+ running_state_change: Instance %{name} state changed to running
+ shutting_down_state_change: Instance %{name} state changed to shutting down
+ stopped_state_change: Instance %{name} state changed to stopped
+ stopping_state_change: Instance %{name} state changed to stopping
+ create_failed_state_change: Instance %{name} state changed to create failed
+ error_state_change: Instance %{name} state changed to error
+ vanished_state_change: Instance %{name} state changed to new
+ create_action_queued: Create action queued for instance %{name}
+ start_action_queued: Start action queued for instance %{name}
+ stop_action_queued: Stop action queued for instance %{name}
+ reboot_action_queued: Reboot action queued for instance %{name}
+ destroy_action_queued: Destroy action queued for instance %{name}
+ stop_failed: Failed to stop instance %{name}
+ forced_stop: Instance %{name} is not accessible, state changed to stopped
+ vanished: Instance %{name} vanished
+ errors:
+ missing_source: Notification source is missing
logs:
options:
default_users: All Users
@@ -1576,3 +1623,4 @@ en:
deployables: Deployables
deployments: Deployments
logs: Logs
+ notifications: Notifications
diff --git a/src/config/routes.rb b/src/config/routes.rb
index 4533796..35b7b6f 100644
--- a/src/config/routes.rb
+++ b/src/config/routes.rb
@@ -189,6 +189,12 @@ Conductor::Application.routes.draw do
end
end
+ resources :notifications do
+ collection do
+ post 'filter'
+ end
+ end
+
resources :config_servers do
member do
get 'test'
diff --git a/src/db/migrate/20120920130441_create_notifications.rb
b/src/db/migrate/20120920130441_create_notifications.rb
new file mode 100644
index 0000000..ea5fd91
--- /dev/null
+++ b/src/db/migrate/20120920130441_create_notifications.rb
@@ -0,0 +1,28 @@
+class CreateNotifications < ActiveRecord::Migration
+ def self.up
+ # These are notifications
+ create_table :notifications do |t|
+ t.integer :source_id, :null => false
+ t.string :source_type, :null => false
+ t.string :key, :null => false
+ t.text :details
+ t.string :level, :null => false
+ t.timestamps
+ end
+ create_table :user_notifications do |t|
+ t.references :user
+ t.references :notification
+ t.boolean :viewed, :null => false, :default => false
+ end
+ add_index :user_notifications, :user_id
+ add_index :user_notifications, :notification_id
+ end
+
+ def self.down
+ remove_index :user_notifications, :column=>:user_id
+ remove_index :user_notifications, :column=>:notification_id
+ drop_table :user_notifications
+ drop_table :notifications
+ end
+
+end
diff --git a/src/lib/notifications/notifier.rb b/src/lib/notifications/notifier.rb
new file mode 100644
index 0000000..baab263
--- /dev/null
+++ b/src/lib/notifications/notifier.rb
@@ -0,0 +1,105 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+#
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+module Notifications
+
+ class Notifier
+ include Rails.application.routes.url_helpers
+
+ attr_reader :controller, :default_options
+
+ def initialize(controller = nil, options = { })
+ @controller = controller
+ @default_options = { :level => :success,
+ :details => nil,
+ :user => nil,
+ :source => nil}.merge(options)
+ end
+
+ # generates success-notice
+ def success(key, options = { })
+ notification key, options
+ end
+
+ # generates message-notice
+ def notice(key, options = { })
+ options = { :level => :notice }.merge options
+ notification key, options
+ end
+
+ # generates warning-notice, not persisted by default
+ def warning(key, options = { })
+ options = { :level => :warning }.merge options
+ notification key, options
+ end
+
+ # generates error-notice
+ def error(key, options = { })
+ options = { :level => :error }.merge options
+ notification key, options
+ end
+
+ # generates error-notice form exception, optionally adding a note
+ # @example
+ # exception(an_exception)
+ # exception("some note", an_exception)
+ def exception(key, *args)
+ options = args.extract_options!
+ exception = args.pop
+
+ options = { :level => :error,
+ :details => exception.backtrace.join("\n") }.merge options
+
+ notification key, options
+ end
+
+ protected
+
+ # Generate a notification:
+ #
+ # key: The notification dictionary key
+ # options: Optional hash containing various optional parameters. This
includes:
+ #
+ # level: The type of notice to be generated. Supported values
include:
+ # :notice, :success (Default), :warning, :error
+ #
+ # details: String containing additional details. This would typically
be to store
+ # information such as a stack trace that is in addition to the
notice text.
+ #
+ # user: the user to send the notifcation to. If not set,
User.current is used
+ #
+ def notification(key, options = { })
+ options = process_options options
+
+ Notification.create! :key => key,
+ :details => options[:details],
+ :level => options[:level],
+ :source => options[:source],
+ :user_notifications =>
+ [UserNotification.new(:user => options[:user], :viewed => false)]
+ end
+
+ LEVELS = [:notice, :success, :warning, :error]
+
+ def process_options(options)
+ options = default_options.merge options
+ options[:user] ||= User.current
+
+ options.assert_valid_keys(*default_options.keys)
+
+ raise ArgumentError, "unknown notification level
#{options[:level].inspect}" unless LEVELS.include? options[:level]
+
+ return options
+ end
+ end
+end
+
diff --git a/src/lib/notify.rb b/src/lib/notify.rb
new file mode 100644
index 0000000..504de9e
--- /dev/null
+++ b/src/lib/notify.rb
@@ -0,0 +1,13 @@
+#
+# Copyright 2011 Red Hat, Inc.
+#
+# This software is licensed to you under the GNU General Public
+# License as published by the Free Software Foundation; either version
+# 2 of the License (GPLv2) or (at your option) any later version.
+# There is NO WARRANTY for this software, express or implied,
+# including the implied warranties of MERCHANTABILITY,
+# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
+# have received a copy of GPLv2 along with this software; if not, see
+#
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
+
+Notify = Notifications::Notifier.new
--
1.7.6.5