Adding backend code for sources models

This commit is contained in:
kevgliss 2015-08-01 15:29:34 -07:00
parent 46c6b8f8a4
commit e247d635fc
26 changed files with 1096 additions and 588 deletions

View File

@ -22,6 +22,8 @@ from lemur.certificates.views import mod as certificates_bp
from lemur.status.views import mod as status_bp from lemur.status.views import mod as status_bp
from lemur.plugins.views import mod as plugins_bp from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp from lemur.notifications.views import mod as notifications_bp
from lemur.sources.views import mod as sources_bp
LEMUR_BLUEPRINTS = ( LEMUR_BLUEPRINTS = (
users_bp, users_bp,
@ -36,6 +38,7 @@ LEMUR_BLUEPRINTS = (
status_bp, status_bp,
plugins_bp, plugins_bp,
notifications_bp, notifications_bp,
sources_bp
) )

View File

@ -23,7 +23,9 @@ from lemur.plugins.base import plugins
from lemur.domains.models import Domain from lemur.domains.models import Domain
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
from lemur.models import certificate_associations, certificate_destination_associations, certificate_notification_associations
from lemur.models import certificate_associations, certificate_source_associations, \
certificate_destination_associations, certificate_notification_associations
def create_name(issuer, not_before, not_after, subject, san): def create_name(issuer, not_before, not_after, subject, san):
@ -222,6 +224,7 @@ class Certificate(db.Model):
authority_id = Column(Integer, ForeignKey('authorities.id')) authority_id = Column(Integer, ForeignKey('authorities.id'))
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate') notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate') destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
sources = relationship("Source", secondary=certificate_source_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate") domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate') elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate')

View File

@ -1,44 +0,0 @@
"""
.. module: lemur.elbs.models
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import Column, BigInteger, String, DateTime, PassiveDefault, func
from sqlalchemy.orm import relationship
from lemur.database import db
from lemur.listeners.models import Listener
class ELB(db.Model):
__tablename__ = 'elbs'
id = Column(BigInteger, primary_key=True)
# account_id = Column(BigInteger, ForeignKey("accounts.id"), index=True)
region = Column(String(32))
name = Column(String(128))
vpc_id = Column(String(128))
scheme = Column(String(128))
dns_name = Column(String(128))
listeners = relationship("Listener", backref='elb', cascade="all, delete, delete-orphan")
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
def __init__(self, elb_obj=None):
if elb_obj:
self.region = elb_obj.connection.region.name
self.name = elb_obj.name
self.vpc_id = elb_obj.vpc_id
self.scheme = elb_obj.scheme
self.dns_name = elb_obj.dns_name
for listener in elb_obj.listeners:
self.listeners.append(Listener(listener))
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
def serialize(self):
blob = self.as_dict()
del blob['date_created']
return blob

View File

@ -1,124 +0,0 @@
"""
.. module: lemur.elbs.service
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import func
from sqlalchemy.sql import and_
from lemur import database
from lemur.elbs.models import ELB
from lemur.listeners.models import Listener
def get_all(account_id, elb_name):
"""
Retrieves all ELBs in a given account
:param account_id:
:param elb_name:
:rtype : Elb
:return:
"""
query = database.session_query(ELB)
return query.filter(and_(ELB.name == elb_name, ELB.account_id == account_id)).all()
def get_by_region_and_account(region, account_id):
query = database.session_query(ELB)
return query.filter(and_(ELB.region == region, ELB.account_id == account_id)).all()
def get_all_elbs():
"""
Get all ELBs that Lemur knows about
:rtype : list
:return:
"""
return ELB.query.all()
def get(elb_id):
"""
Retrieve an ELB with a give ID
:rtype : Elb
:param elb_id:
:return:
"""
return database.get(ELB, elb_id)
def create(account, elb):
"""
Create a new ELB
:param account:
:param elb:
"""
elb = ELB(elb)
account.elbs.append(elb)
database.create(elb)
def delete(elb_id):
"""
Delete an ELB
:param elb_id:
"""
database.delete(get(elb_id))
def render(args):
query = database.session_query(ELB)
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
active = args.pop('active')
certificate_id = args.pop('certificate_id')
if certificate_id:
query.filter(ELB.listeners.any(Listener.certificate_id == certificate_id))
if active == 'true':
query = query.filter(ELB.listeners.any())
if filt:
terms = filt.split(';')
query = database.filter(query, ELB, terms)
query = database.find_all(query, ELB, args)
if sort_by and sort_dir:
query = database.sort(query, ELB, sort_by, sort_dir)
return database.paginate(query, page, count)
def stats(**kwargs):
attr = getattr(ELB, kwargs.get('metric'))
query = database.db.session.query(attr, func.count(attr))
if kwargs.get('account_id'):
query = query.filter(ELB.account_id == kwargs.get('account_id'))
if kwargs.get('active') == 'true':
query = query.join(ELB.listeners)
query = query.filter(Listener.certificate_id != None) # noqa
items = query.group_by(attr).all()
results = []
for key, count in items:
if key:
results.append({"key": key, "y": count})
return results

View File

@ -1,78 +0,0 @@
"""
.. module: lemur.elbs.service
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint
from flask.ext.restful import reqparse, Api, fields
from lemur.elbs import service
from lemur.auth.service import AuthenticatedResource
from lemur.common.utils import marshal_items, paginated_parser
mod = Blueprint('elbs', __name__)
api = Api(mod)
FIELDS = {
'name': fields.String,
'id': fields.Integer,
'region': fields.String,
'scheme': fields.String,
'accountId': fields.Integer(attribute='account_id'),
'vpcId': fields.String(attribute='vpc_id')
}
class ELBsList(AuthenticatedResource):
""" Defines the 'elbs' endpoint """
def __init__(self):
super(ELBsList, self).__init__()
@marshal_items(FIELDS)
def get(self):
parser = paginated_parser.copy()
parser.add_argument('owner', type=str, location='args')
parser.add_argument('id', type=str, location='args')
parser.add_argument('accountId', type=str, dest='account_id', location='args')
parser.add_argument('certificateId', type=str, dest='certificate_id', location='args')
parser.add_argument('active', type=str, default='true', location='args')
args = parser.parse_args()
return service.render(args)
class ELBsStats(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(ELBsStats, self).__init__()
def get(self):
self.reqparse.add_argument('metric', type=str, location='args')
self.reqparse.add_argument('accountId', dest='account_id', location='args')
self.reqparse.add_argument('active', type=str, default='true', location='args')
args = self.reqparse.parse_args()
items = service.stats(**args)
return {"items": items, "total": len(items)}
class ELBs(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(ELBs, self).__init__()
@marshal_items(FIELDS)
def get(self, elb_id):
return service.get(elb_id)
api.add_resource(ELBsList, '/elbs', endpoint='elbs')
api.add_resource(ELBs, '/elbs/<int:elb_id>', endpoint='elb')
api.add_resource(ELBsStats, '/elbs/stats', endpoint='elbsStats')

View File

@ -1,42 +0,0 @@
"""
.. module: lemur.elbs.models
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import Column, Integer, BigInteger, String, ForeignKey, DateTime, PassiveDefault, func
from lemur.database import db
from lemur.certificates import service as cert_service
from lemur.certificates.models import Certificate, get_name_from_arn
class Listener(db.Model):
__tablename__ = 'listeners'
id = Column(BigInteger, primary_key=True)
certificate_id = Column(Integer, ForeignKey(Certificate.id), index=True)
elb_id = Column(BigInteger, ForeignKey("elbs.id"), index=True)
instance_port = Column(Integer)
instance_protocol = Column(String(16))
load_balancer_port = Column(Integer)
load_balancer_protocol = Column(String(16))
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
def __init__(self, listener):
self.load_balancer_port = listener.load_balancer_port
self.load_balancer_protocol = listener.protocol
self.instance_port = listener.instance_port
self.instance_protocol = listener.instance_protocol
if listener.ssl_certificate_id not in ["Invalid-Certificate", None]:
self.certificate_id = cert_service.get_by_name(get_name_from_arn(listener.ssl_certificate_id)).id
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
def serialize(self):
blob = self.as_dict()
del blob['date_created']
return blob

View File

@ -1,159 +0,0 @@
"""
.. module: lemur.listeners.service
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy import func
from lemur import database
from lemur.exceptions import CertificateUnavailable
from lemur.elbs.models import ELB
from lemur.listeners.models import Listener
from lemur.elbs import service as elb_service
from lemur.certificates import service as certificate_service
# from lemur.common.services.aws.elb import update_listeners, create_new_listeners, delete_listeners
def verify_attachment(certificate_id, elb_account_number):
"""
Ensures that the certificate we want ot attach to our listener is
in the same account as our listener.
:rtype : Certificate
:param certificate_id:
:param elb_account_number:
:return: :raise CertificateUnavailable:
"""
cert = certificate_service.get(certificate_id)
# we need to ensure that the specified cert is in our account
for account in cert.accounts:
if account.account_number == elb_account_number:
break
else:
raise CertificateUnavailable
return cert
def get(listener_id):
return database.get(Listener, listener_id)
def create(elb_id, instance_protocol, instance_port, load_balancer_port, load_balancer_protocol, certificate_id=None):
listener = Listener(elb_id,
instance_port,
instance_protocol,
load_balancer_port,
load_balancer_protocol
)
elb = elb_service.get(elb_id)
elb.listeners.append(listener)
account_number = elb.account.account_number
cert = verify_attachment(certificate_id, account_number)
listener_tuple = (load_balancer_port, instance_port, load_balancer_protocol, cert.get_art(account_number),)
# create_new_listeners(account_number, elb.region, elb.name, [listener_tuple])
return {'message': 'Listener has been created'}
def update(listener_id, **kwargs):
listener = get(listener_id)
# if the lb_port has changed we need to make sure we are deleting
# the listener on the old port to avoid listener duplication
ports = []
if listener.load_balancer_port != kwargs.get('load_balancer_port'):
ports.append(listener.load_balancer_port)
else:
ports.append(kwargs.get('load_balancer_port'))
certificate_id = kwargs.get('certificate_id')
listener.instance_port = kwargs.get('instance_port')
listener.instance_protocol = kwargs.get('instance_protocol')
listener.load_balancer_port = kwargs.get('load_balancer_port')
listener.load_balancer_protocol = kwargs.get('load_balancer_protocol')
elb = listener.elb
account_number = listener.elb.account.account_number
arn = None
if certificate_id:
cert = verify_attachment(certificate_id, account_number)
cert.elb_listeners.append(listener)
arn = cert.get_arn(account_number)
# remove certificate that is no longer wanted
if listener.certificate and not certificate_id:
listener.certificate.remove()
database.update(listener)
listener_tuple = (listener.load_balancer_port, listener.instance_port, listener.load_balancer_protocol, arn,)
# update_listeners(account_number, elb.region, elb.name, [listener_tuple], ports)
return {'message': 'Listener has been updated'}
def delete(listener_id):
# first try to delete the listener in aws
listener = get(listener_id)
# delete_listeners(listener.elb.account.account_number, listener.elb.region, listener.elb.name, [listener.load_balancer_port])
# cleanup operation in lemur
database.delete(listener)
def render(args):
query = database.session_query(Listener)
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
elb_id = args.pop('elb_id', None)
if certificate_id:
query = database.get_all(Listener, certificate_id, field='certificate_id')
if elb_id:
query = query.filter(Listener.elb_id == elb_id)
if filt:
terms = filt.split(';')
query = database.filter(query, Listener, terms)
query = database.find_all(query, Listener, args)
if sort_by and sort_dir:
query = database.sort(query, Listener, sort_by, sort_dir)
return database.paginate(query, page, count)
def stats(**kwargs):
attr = getattr(Listener, kwargs.get('metric'))
query = database.db.session.query(attr, func.count(attr))
query = query.join(Listener.elb)
if kwargs.get('account_id'):
query = query.filter(ELB.account_id == kwargs.get('account_id'))
if kwargs.get('active') == 'true':
query = query.filter(Listener.certificate_id != None) # noqa
items = query.group_by(attr).all()
results = []
for key, count in items:
if key:
results.append({"key": key, "y": count})
return results

View File

@ -1,128 +0,0 @@
"""
.. module: lemur.listeners.service
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint
from flask.ext.restful import reqparse, Api, fields
from lemur.listeners import service
from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import admin_permission
from lemur.common.utils import marshal_items, paginated_parser
mod = Blueprint('listeners', __name__)
api = Api(mod)
FIELDS = {
'id': fields.Integer,
'elbId': fields.Integer(attribute="elb_id"),
'certificateId': fields.Integer(attribute="certificate_id"),
'instancePort': fields.Integer(attribute="instance_port"),
'instanceProtocol': fields.String(attribute="instance_protocol"),
'loadBalancerPort': fields.Integer(attribute="load_balancer_port"),
'loadBalancerProtocol': fields.String(attribute="load_balancer_protocol")
}
class ListenersList(AuthenticatedResource):
def __init__(self):
super(ListenersList, self).__init__()
@marshal_items(FIELDS)
def get(self):
parser = paginated_parser.copy()
parser.add_argument('certificateId', type=int, dest='certificate_id', location='args')
args = parser.parse_args()
return service.render(args)
class ListenersCertificateList(AuthenticatedResource):
def __init__(self):
super(ListenersCertificateList, self).__init__()
@marshal_items(FIELDS)
def get(self, certificate_id):
parser = paginated_parser.copy()
args = parser.parse_args()
args['certificate_id'] = certificate_id
return service.render(args)
class ListenersELBList(AuthenticatedResource):
def __init__(self):
super(ListenersELBList, self).__init__()
@marshal_items(FIELDS)
def get(self, elb_id):
parser = paginated_parser.copy()
args = parser.parse_args()
args['elb_id'] = elb_id
return service.render(args)
class ListenersStats(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(ListenersStats, self).__init__()
def get(self):
self.reqparse.add_argument('metric', type=str, location='args')
self.reqparse.add_argument('accountId', dest='account_id', location='args')
self.reqparse.add_argument('active', type=str, default='true', location='args')
args = self.reqparse.parse_args()
items = service.stats(**args)
return {"items": items, "total": len(items)}
class Listeners(AuthenticatedResource):
def __init__(self):
super(Listeners, self).__init__()
@marshal_items(FIELDS)
def get(self, listener_id):
return service.get(listener_id)
@admin_permission.require(http_exception=403)
@marshal_items(FIELDS)
def post(self):
self.reqparse.add_argument('elbId', type=str, dest='elb_id', required=True, location='json')
self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json')
self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json')
self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json')
self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json')
self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json')
args = self.reqparse.parse_args()
return service.create(**args)
@admin_permission.require(http_exception=403)
@marshal_items(FIELDS)
def put(self, listener_id):
self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json')
self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json')
self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json')
self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json')
self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json')
args = self.reqparse.parse_args()
return service.update(listener_id, **args)
@admin_permission.require(http_exception=403)
def delete(self, listener_id):
return service.delete(listener_id)
api.add_resource(ListenersList, '/listeners', endpoint='listeners')
api.add_resource(Listeners, '/listeners/<int:listener_id>', endpoint='listener')
api.add_resource(ListenersStats, '/listeners/stats', endpoint='listenersStats')
api.add_resource(ListenersCertificateList, '/certificates/<int:certificate_id>/listeners', endpoint='listenersCertificates')
api.add_resource(ListenersELBList, '/elbs/<int:elb_id>/listeners', endpoint='elbListeners')

View File

@ -19,7 +19,7 @@ from lemur.certificates import service as cert_service
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
from lemur.certificates.verify import verify_string from lemur.certificates.verify import verify_string
from lemur.certificates import sync from lemur.sources import sync
from lemur import create_app from lemur import create_app
@ -33,6 +33,7 @@ from lemur.domains.models import Domain # noqa
from lemur.elbs.models import ELB # noqa from lemur.elbs.models import ELB # noqa
from lemur.listeners.models import Listener # noqa from lemur.listeners.models import Listener # noqa
from lemur.notifications.models import Notification # noqa from lemur.notifications.models import Notification # noqa
from lemur.sources.models import Source # noqa
manager = Manager(create_app) manager = Manager(create_app)
@ -183,12 +184,11 @@ class Sync(Command):
run on a periodic basis and updates the Lemur datastore with the run on a periodic basis and updates the Lemur datastore with the
information it discovers. information it discovers.
""" """
# TODO create these commands dynamically
option_list = [ option_list = [
Group( Group(
Option('-a', '--all', action="store_true"), Option('-a', '--all', action="store_true"),
Option('-b', '--aws', action="store_true"),
Option('-d', '--cloudca', action="store_true"),
Option('-s', '--source', action="store_true"),
exclusive=True, required=True exclusive=True, required=True
) )
] ]

View File

@ -0,0 +1,42 @@
"""Adding in models for certificate sources
Revision ID: 1ff763f5b80b
Revises: 4dc5ddd111b8
Create Date: 2015-08-01 15:24:20.412725
"""
# revision identifiers, used by Alembic.
revision = '1ff763f5b80b'
down_revision = '4dc5ddd111b8'
from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('sources',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('label', sa.String(length=32), nullable=True),
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('plugin_name', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('certificate_source_associations',
sa.Column('source_id', sa.Integer(), nullable=True),
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['source_id'], ['destinations.id'], ondelete='cascade')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_source_associations')
op.drop_table('sources')
### end Alembic commands ###

View File

@ -0,0 +1,41 @@
"""Adding notifications
Revision ID: 4c8915e461b3
Revises: 3b718f59b8ce
Create Date: 2015-07-24 14:34:57.316273
"""
# revision identifiers, used by Alembic.
revision = '4c8915e461b3'
down_revision = '3b718f59b8ce'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import sqlalchemy_utils
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('notifications',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('label', sa.String(length=128), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('plugin_name', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.drop_column(u'certificates', 'challenge')
op.drop_column(u'certificates', 'csr_config')
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column(u'certificates', sa.Column('csr_config', sa.TEXT(), autoincrement=False, nullable=True))
op.add_column(u'certificates', sa.Column('challenge', postgresql.BYTEA(), autoincrement=False, nullable=True))
op.drop_table('notifications')
### end Alembic commands ###

View File

@ -0,0 +1,31 @@
"""Creating a one-to-many relationship for notifications
Revision ID: 4dc5ddd111b8
Revises: 4c8915e461b3
Create Date: 2015-07-24 15:02:04.398262
"""
# revision identifiers, used by Alembic.
revision = '4dc5ddd111b8'
down_revision = '4c8915e461b3'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('certificate_notification_associations',
sa.Column('notification_id', sa.Integer(), nullable=True),
sa.Column('certificate_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'),
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('certificate_notification_associations')
### end Alembic commands ###

View File

@ -8,9 +8,7 @@
:license: Apache, see LICENSE for more details. :license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
""" """
from sqlalchemy import Column, Integer, ForeignKey from sqlalchemy import Column, Integer, ForeignKey
from lemur.database import db from lemur.database import db
certificate_associations = db.Table('certificate_associations', certificate_associations = db.Table('certificate_associations',
@ -25,6 +23,13 @@ certificate_destination_associations = db.Table('certificate_destination_associa
ForeignKey('certificates.id', ondelete='cascade')) ForeignKey('certificates.id', ondelete='cascade'))
) )
certificate_source_associations = db.Table('certificate_source_associations',
Column('source_id', Integer,
ForeignKey('destinations.id', ondelete='cascade')),
Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade'))
)
certificate_notification_associations = db.Table('certificate_notification_associations', certificate_notification_associations = db.Table('certificate_notification_associations',
Column('notification_id', Integer, Column('notification_id', Integer,
ForeignKey('notifications.id', ondelete='cascade')), ForeignKey('notifications.id', ondelete='cascade')),

29
lemur/sources/models.py Normal file
View File

@ -0,0 +1,29 @@
"""
.. module: lemur.sources.models
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import copy
from sqlalchemy import Column, Integer, String, Text
from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
class Source(db.Model):
__tablename__ = 'sources'
id = Column(Integer, primary_key=True)
label = Column(String(32))
options = Column(JSONType)
description = Column(Text())
plugin_name = Column(String(32))
@property
def plugin(self):
p = plugins.get(self.plugin_name)
c = copy.deepcopy(p)
c.options = self.options
return c

107
lemur/sources/service.py Normal file
View File

@ -0,0 +1,107 @@
"""
.. module: lemur.sources.service
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from lemur import database
from lemur.sources.models import Source
from lemur.certificates.models import Certificate
def create(label, plugin_name, options, description=None):
"""
Creates a new source, that can then be used as a source for certificates.
:param label: Source common name
:param description:
:rtype : Source
:return: New source
"""
source = Source(label=label, options=options, plugin_name=plugin_name, description=description)
return database.create(source)
def update(source_id, label, options, description):
"""
Updates an existing source.
:param source_id: Lemur assigned ID
:param label: Source common name
:rtype : Source
:return:
"""
source = get(source_id)
source.label = label
source.options = options
source.description = description
return database.update(source)
def delete(source_id):
"""
Deletes an source.
:param source_id: Lemur assigned ID
"""
database.delete(get(source_id))
def get(source_id):
"""
Retrieves an source by it's lemur assigned ID.
:param source_id: Lemur assigned ID
:rtype : Source
:return:
"""
return database.get(Source, source_id)
def get_by_label(label):
"""
Retrieves a source by it's label
:param label:
:return:
"""
return database.get(Source, label, field='label')
def get_all():
"""
Retrieves all source currently known by Lemur.
:return:
"""
query = database.session_query(Source)
return database.find_all(query, Source, {}).all()
def render(args):
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
if certificate_id:
query = database.session_query(Source).join(Certificate, Source.certificate)
query = query.filter(Certificate.id == certificate_id)
else:
query = database.session_query(Source)
if filt:
terms = filt.split(';')
query = database.filter(query, Source, terms)
query = database.find_all(query, Source, args)
if sort_by and sort_dir:
query = database.sort(query, Source, sort_by, sort_dir)
return database.paginate(query, page, count)

View File

@ -1,5 +1,5 @@
""" """
.. module: sync .. module: lemur.sources.sync
:platform: Unix :platform: Unix
:synopsis: This module contains various certificate syncing operations. :synopsis: This module contains various certificate syncing operations.
Because of the nature of the SSL environment there are multiple ways Because of the nature of the SSL environment there are multiple ways

359
lemur/sources/views.py Normal file
View File

@ -0,0 +1,359 @@
"""
.. module: lemur.sources.views
:platform: Unix
:synopsis: This module contains all of the accounts view code.
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint
from flask.ext.restful import Api, reqparse, fields
from lemur.sources import service
from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import admin_permission
from lemur.common.utils import paginated_parser, marshal_items
mod = Blueprint('sources', __name__)
api = Api(mod)
FIELDS = {
'description': fields.String,
'sourceOptions': fields.Raw(attribute='options'),
'pluginName': fields.String(attribute='plugin_name'),
'label': fields.String,
'id': fields.Integer,
}
class SourcesList(AuthenticatedResource):
""" Defines the 'sources' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(SourcesList, self).__init__()
@marshal_items(FIELDS)
def get(self):
"""
.. http:get:: /sources
The current account list
**Example request**:
.. sourcecode:: http
GET /sources HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
parser = paginated_parser.copy()
args = parser.parse_args()
return service.render(args)
@admin_permission.require(http_exception=403)
@marshal_items(FIELDS)
def post(self):
"""
.. http:post:: /sources
Creates a new account
**Example request**:
.. sourcecode:: http
POST /sources HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
:arg label: human readable account label
:arg description: some description about the account
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args()
return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description'])
class Sources(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Sources, self).__init__()
@marshal_items(FIELDS)
def get(self, source_id):
"""
.. http:get:: /sources/1
Get a specific account
**Example request**:
.. sourcecode:: http
GET /sources/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
return service.get(source_id)
@admin_permission.require(http_exception=403)
@marshal_items(FIELDS)
def put(self, source_id):
"""
.. http:put:: /sources/1
Updates an account
**Example request**:
.. sourcecode:: http
POST /sources/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
:arg accountNumber: aws account number
:arg label: human readable account label
:arg description: some description about the account
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args()
return service.update(source_id, args['label'], args['plugin']['pluginOptions'], args['description'])
@admin_permission.require(http_exception=403)
def delete(self, source_id):
service.delete(source_id)
return {'result': True}
class CertificateSources(AuthenticatedResource):
""" Defines the 'certificate/<int:certificate_id/sources'' endpoint """
def __init__(self):
super(CertificateSources, self).__init__()
@marshal_items(FIELDS)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/sources
The current account list for a given certificates
**Example request**:
.. sourcecode:: http
GET /certificates/1/sources HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [
{
"sourceOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-source",
"id": 3,
"description": "test",
"label": "test"
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
parser = paginated_parser.copy()
args = parser.parse_args()
args['certificate_id'] = certificate_id
return service.render(args)
api.add_resource(SourcesList, '/sources', endpoint='sources')
api.add_resource(Sources, '/sources/<int:source_id>', endpoint='account')
api.add_resource(CertificateSources, '/certificates/<int:certificate_id>/sources',
endpoint='certificateSources')

View File

@ -0,0 +1,38 @@
<div class="modal-header">
<div class="modal-title">
<h3 class="modal-header">Edit <span class="text-muted"><small>{{ certificate.name }}</small></span></h3>
</div>
<div class="modal-body">
<form name="editForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
<label class="control-label col-sm-2">
Owner
</label>
<div class="col-sm-10">
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@netflix.com"
class="form-control" required/>
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
email.</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
</div>
</div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</form>
</div>
<div class="modal-footer">
<button type="submit" ng-click="save(certificate)" ng-disabled="editForm.$invalid" class="btn btn-success">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
</div>

View File

@ -0,0 +1,28 @@
<div class="form-group">
<label class="control-label col-sm-2">
Notifications
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedNotification" placeholder="Email"
typeahead="notification.label for notification in notificationService.findNotificationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachNotification($item)" typeahead-min-wait="50"
tooltip="By default Lemur will always notify you about this certificate through Email notifications."
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="notifications.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.notifications.length || 0 }}</span>
</button>
</span>
</div>
<table class="table">
<tr ng-repeat="notification in certificate.notifications track by $index">
<td><a class="btn btn-sm btn-info" href="#/notifications/{{ notification.id }}/certificates">{{ notification.label }}</a></td>
<td><span class="text-muted">{{ notification.description }}</span></td>
<td>
<button type="button" ng-click="certificate.removeNotification($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
</div>

View File

@ -0,0 +1,56 @@
'use strict';
angular.module('lemur')
.controller('NotificationsCreateController', function ($scope, $modalInstance, PluginService, NotificationService, CertificateService, LemurRestangular){
$scope.notification = LemurRestangular.restangularizeElement(null, {}, 'notifications');
PluginService.getByType('notification').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.save = function (notification) {
NotificationService.create(notification).then(
function () {
$modalInstance.close();
},
function () {
}
);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.certificateService = CertificateService;
})
.controller('NotificationsEditController', function ($scope, $modalInstance, NotificationService, NotificationApi, PluginService, CertificateService, editId) {
NotificationApi.get(editId).then(function (notification) {
$scope.notification = notification;
NotificationService.getCertificates(notification);
});
PluginService.getByType('notification').then(function (plugins) {
$scope.plugins = plugins;
_.each($scope.plugins, function (plugin) {
if (plugin.slug == $scope.notification.pluginName) {
plugin.pluginOptions = $scope.notification.notificationOptions;
$scope.notification.plugin = plugin;
};
});
});
$scope.save = function (notification) {
NotificationService.update(notification).then(function () {
$modalInstance.close();
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.certificateService = CertificateService;
});

View File

@ -0,0 +1,87 @@
<div class="modal-header">
<div class="modal-title">
<h3 class="modal-header"><span ng-show="!notification.fromServer">Create</span><span ng-show="notification.fromServer">Edit</span> Notification <span class="text-muted"><small>you gotta speak louder son!</small></span></h3>
</div>
<div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': createForm.label.$invalid, 'has-success': !createForm.label.$invalid&&createForm.label.$dirty}">
<label class="control-label col-sm-2">
Label
</label>
<div class="col-sm-10">
<input name="label" ng-model="notification.label" placeholder="Label" class="form-control" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an notification label</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="comments" ng-model="notification.description" placeholder="Something elegant" class="form-control" ></textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="notification.plugin" ng-options="plugin.title for plugin in plugins" required></select>
</div>
</div>
<div class="form-group" ng-repeat="item in notification.plugin.pluginOptions">
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
<label class="control-label col-sm-2">
{{ item.name | titleCase }}
</label>
<div class="col-sm-10">
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="item.validation" class="form-control" ng-model="item.value"/>
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i as (i | titleCase) for i in item.available" ng-model="item.value"></select>
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
<input name="sub" ng-if="item.type == 'str'" ng-pattern="item.validation" type="text" class="form-control" ng-model="item.value"/>
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine" class="help-block">{{ item.helpMessage }}</p>
</div>
</div>
</ng-form>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Certificates
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="notification.selectedCertificate" placeholder="Certificate Name"
typeahead="certificate.name for certificate in certificateService.findCertificatesByName($viewValue)" typeahead-loading="loadingCertificates"
class="form-control input-md" typeahead-on-select="notification.attachCertificate($item)" typeahead-min-wait="50">
<span class="input-group-btn">
<button ng-model="certificates.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ notification.certificates.total || 0 }}</span>
</button>
</span>
</div>
<table ng-show="notification.certificates" class="table">
<tr ng-repeat="certificate in notification.certificates track by $index">
<td><a class="btn btn-sm btn-info" href="#">{{ certificate.name }}</a></td>
<td><span class="text-muted">{{ certificate.description }}</span></td>
<td>
<button type="button" ng-click="notification.removeCertificate($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td><a class="pull-right" ng-click="loadMoreCertificates()"><strong>More</strong></a></td>
</tr>
</table>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button ng-click="save(notification)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
</div>

View File

@ -0,0 +1,106 @@
'use strict';
angular.module('lemur')
.service('NotificationApi', function (LemurRestangular) {
LemurRestangular.extendModel('notifications', function (obj) {
return angular.extend(obj, {
attachCertificate: function (certificate) {
this.selectedCertificate = null;
if (this.certificates === undefined) {
this.certificates = [];
}
this.certificates.push(certificate);
},
removeCertificate: function (index) {
this.certificate.splice(index, 1);
}
});
});
return LemurRestangular.all('notifications');
})
.service('NotificationService', function ($location, NotificationApi, PluginService, toaster) {
var NotificationService = this;
NotificationService.findNotificationsByName = function (filterValue) {
return NotificationApi.getList({'filter[label]': filterValue})
.then(function (notifications) {
return notifications;
});
};
NotificationService.getCertificates = function (notification) {
notification.getList('certificates').then(function (certificates) {
notification.certificates = certificates;
});
};
NotificationService.getPlugin = function (notification) {
return PluginService.getByName(notification.pluginName).then(function (plugin) {
notification.plugin = plugin;
});
};
NotificationService.loadMoreCertificates = function (notification, page) {
notification.getList('certificates', {page: page}).then(function (certificates) {
_.each(certificates, function (certificate) {
notification.roles.push(certificate);
});
});
};
NotificationService.create = function (notification) {
return NotificationApi.post(notification).then(
function () {
toaster.pop({
type: 'success',
title: notification.label,
body: 'Successfully created!'
});
$location.path('notifications');
},
function (response) {
toaster.pop({
type: 'error',
title: notification.label,
body: 'Was not created! ' + response.data.message
});
});
};
NotificationService.update = function (notification) {
return notification.put().then(
function () {
toaster.pop({
type: 'success',
title: notification.label,
body: 'Successfully updated!'
});
$location.path('notifications');
},
function (response) {
toaster.pop({
type: 'error',
title: notification.label,
body: 'Was not updated! ' + response.data.message
});
});
};
NotificationService.updateActive = function (notification) {
notification.put().then(
function () {
toaster.pop({
type: 'success',
title: notification.name,
body: 'Successfully updated!'
});
},
function (response) {
toaster.pop({
type: 'error',
title: notification.name,
body: 'Was not updated! ' + response.data.message
});
});
};
return NotificationService;
});

View File

@ -0,0 +1,96 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/notifications', {
templateUrl: '/angular/notifications/view/view.tpl.html',
controller: 'NotificationsViewController'
});
})
.controller('NotificationsViewController', function ($q, $scope, $modal, NotificationApi, NotificationService, ngTableParams, toaster) {
$scope.filter = {};
$scope.notificationsTable = new ngTableParams({
page: 1, // show first page
count: 10, // count per page
sorting: {
id: 'desc' // initial sorting
},
filter: $scope.filter
}, {
total: 0, // length of data
getData: function ($defer, params) {
NotificationApi.getList(params.url()).then(
function (data) {
_.each(data, function (notification) {
NotificationService.getPlugin(notification);
});
params.total(data.total);
$defer.resolve(data);
}
);
}
});
$scope.getNotificationStatus = function () {
var def = $q.defer();
def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]);
return def;
};
$scope.remove = function (notification) {
notification.remove().then(
function () {
$scope.notificationsTable.reload();
},
function (response) {
toaster.pop({
type: 'error',
title: 'Opps',
body: 'I see what you did there' + response.data.message
});
}
);
};
$scope.edit = function (notificationId) {
var modalInstance = $modal.open({
animation: true,
templateUrl: '/angular/notifications/notification/notification.tpl.html',
controller: 'NotificationsEditController',
size: 'lg',
resolve: {
editId: function () {
return notificationId;
}
}
});
modalInstance.result.then(function () {
$scope.notificationsTable.reload();
});
};
$scope.create = function () {
var modalInstance = $modal.open({
animation: true,
controller: 'NotificationsCreateController',
templateUrl: '/angular/notifications/notification/notification.tpl.html',
size: 'lg'
});
modalInstance.result.then(function () {
$scope.notificationsTable.reload();
});
};
$scope.toggleFilter = function (params) {
params.settings().$scope.show_filter = !params.settings().$scope.show_filter;
};
$scope.notificationService = NotificationService;
});

View File

@ -0,0 +1,52 @@
<div class="row">
<div class="col-md-12">
<h2 class="featurette-heading">Notifications
<span class="text-muted"><small>you have to speak up son!</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<button ng-click="create()" class="btn btn-primary">Create</button>
</div>
<div class="btn-group">
<button ng-click="toggleFilter(notificationsTable)" class="btn btn-default">Filter</button>
</div>
<div class="clearfix"></div>
</div>
<div class="table-responsive">
<table ng-table="notificationsTable" class="table table-striped" show-filter="false" template-pagination="angular/pager.html" >
<tbody>
<tr ng-repeat="notification in $data track by $index">
<td data-title="'Label'" sortable="'label'" filter="{ 'label': 'text' }">
<ul class="list-unstyled">
<li>{{ notification.label }}</li>
<li><span class="text-muted">{{ notification.description }}</span></li>
</ul>
</td>
<td data-title="'Plugin'">
<ul class="list-unstyled">
<li>{{ notification.plugin.title }}</li>
<li><span class="text-muted">{{ notification.plugin.description }}</span></li>
</ul>
</td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getNotificationStatus()">
<form>
<switch ng-change="notificationService.updateActive(notification)" id="status" name="status" ng-model="notification.active" class="green small"></switch>
</form>
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button tooltip="Edit Notification" ng-click="edit(notification.id)" class="btn btn-sm btn-info">
Edit
</button>
<button tooltip="Delete Notification" ng-click="remove(notification)" type="button" class="btn btn-sm btn-danger pull-left">
Remove
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>