├── test
├── __init__.py
├── cache
│ ├── __init__.py
│ ├── global_cache_test.py
│ └── data_store_test.py
├── config
│ ├── __init__.py
│ └── test_configuration.py
├── entities
│ ├── __init__.py
│ ├── custom_filter_test.py
│ └── test_application_state.py
├── messages
│ ├── __init__.py
│ └── app_dep_test.py
├── predicate
│ ├── __init__.py
│ ├── zkgut_test.py
│ ├── health_test.py
│ ├── pred_simple_test.py
│ ├── factory_test.py
│ ├── process_test.py
│ └── pred_not_test.py
├── noop_test.py
├── test.sh
└── test_utils.py
├── scripts
├── __init__.py
├── local_zoom.sh
├── local_sentinel.sh
├── bootstrap.sh
└── win_install.py
├── server
├── zoom
│ ├── __init__.py
│ ├── common
│ │ ├── __init__.py
│ │ ├── constants.py
│ │ ├── types.py
│ │ ├── handlers.py
│ │ └── pagerduty.py
│ ├── www
│ │ ├── cache
│ │ │ ├── __init__.py
│ │ │ └── global_cache.py
│ │ ├── config
│ │ │ └── __init__.py
│ │ ├── handlers
│ │ │ ├── __init__.py
│ │ │ ├── list_pillar_servers_handler.py
│ │ │ ├── salt_master_handler.py
│ │ │ ├── zoom_ws_handler.py
│ │ │ ├── environment_handler.py
│ │ │ ├── pagerduty_services_handler.py
│ │ │ ├── list_servers_handler.py
│ │ │ ├── reload_cache_handler.py
│ │ │ ├── delete_path_handler.py
│ │ │ ├── time_estimate_handler.py
│ │ │ ├── disable_app_handler.py
│ │ │ ├── service_info_handler.py
│ │ │ ├── application_dependencies_handler.py
│ │ │ ├── global_mode_handler.py
│ │ │ ├── filters_handler.py
│ │ │ └── application_opdep_handler.py
│ │ ├── messages
│ │ │ ├── __init__.py
│ │ │ ├── global_mode_message.py
│ │ │ ├── timing_estimate.py
│ │ │ ├── application_dependencies.py
│ │ │ ├── message_throttler.py
│ │ │ └── application_states.py
│ │ ├── __init__.py
│ │ ├── .coveragerc
│ │ └── entities
│ │ │ ├── __init__.py
│ │ │ ├── custom_filter.py
│ │ │ └── application_state.py
│ └── agent
│ │ ├── action
│ │ └── __init__.py
│ │ ├── client
│ │ ├── __init__.py
│ │ └── wmi_client.py
│ │ ├── config
│ │ ├── __init__.py
│ │ ├── sentinel_windows_config.xml_SAMPLE
│ │ └── config_schema.xsd
│ │ ├── task
│ │ ├── __init__.py
│ │ ├── zk_task_client.py
│ │ ├── base_task_client.py
│ │ └── task.py
│ │ ├── util
│ │ └── __init__.py
│ │ ├── web
│ │ ├── handlers
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ ├── templates
│ │ │ └── log.html
│ │ └── rest.py
│ │ ├── __init__.py
│ │ ├── check
│ │ ├── __init__.py
│ │ ├── always_fail.bat
│ │ ├── always_fail
│ │ ├── findstring
│ │ └── logtick
│ │ ├── entities
│ │ ├── __init__.py
│ │ ├── thread_safe_object.py
│ │ ├── unique_queue.py
│ │ ├── restart.py
│ │ ├── work_manager.py
│ │ └── job.py
│ │ └── predicate
│ │ ├── __init__.py
│ │ ├── zknode_exists.py
│ │ ├── pred_not.py
│ │ ├── pred_or.py
│ │ ├── pred_and.py
│ │ ├── weekend.py
│ │ ├── zkglob.py
│ │ └── process.py
├── .coveragerc
├── sentinel.py
└── zoom.py
├── client
├── styles
│ ├── app
│ │ ├── production.css
│ │ ├── staging.css
│ │ ├── applicationState.css
│ │ ├── dependencyMaps.css
│ │ ├── pillarConfig.css
│ │ └── spot.css
│ └── fonts
│ │ ├── FontAwesome.otf
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.ttf
│ │ ├── fontawesome-webfont.woff
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.ttf
│ │ └── glyphicons-halflings-regular.woff
├── images
│ ├── vadar.jpg
│ ├── stg
│ │ └── favicon.ico
│ └── prod
│ │ └── favicon.ico
├── classes
│ ├── applicationStateArray.js
│ ├── predicateFactory.js
│ ├── dependency-maps
│ │ ├── ClusterTree.js
│ │ └── TreeMap.js
│ └── LogicPredicate.js
├── viewmodels
│ ├── faq
│ │ ├── serverConfig.js
│ │ └── applicationState.js
│ ├── pillarConfig.js
│ ├── sentinelConfig
│ │ └── alertsViewModel.js
│ ├── navbar.js
│ └── applicationState.js
├── views
│ ├── faq
│ │ └── serverConfig.html
│ └── index.html
├── .jshintrc
├── bindings
│ ├── uppercase.js
│ ├── radio.js
│ └── tooltip.js
├── libs
│ ├── jquery.ba-throttle-debounce.min.js
│ └── jquery.mousewheel.min.js
├── model
│ ├── externalLinkModel.js
│ ├── adminModel.js
│ ├── appInfoModel.js
│ ├── environmentModel.js
│ ├── loginModel.js
│ ├── toolsModel.js
│ └── GlobalMode.js
├── main.js
└── service.js
├── docker
├── zoom
│ ├── build.sh
│ └── Dockerfile
└── sentinel
│ ├── build.sh
│ └── Dockerfile
├── apidoc.json
├── Makefile
├── .jshintrc
├── .gitignore
├── .jscsrc
└── README.md
/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/cache/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/entities/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/messages/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/predicate/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/www/cache/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/agent/action/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/agent/client/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/agent/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/agent/task/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/agent/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/www/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/www/messages/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/zoom/agent/web/handlers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | include = zoom/*
3 |
--------------------------------------------------------------------------------
/client/styles/app/production.css:
--------------------------------------------------------------------------------
1 | tr{background-color: '';}
2 |
--------------------------------------------------------------------------------
/server/zoom/agent/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'davidshrader'
2 |
--------------------------------------------------------------------------------
/server/zoom/www/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'davidshrader'
2 |
--------------------------------------------------------------------------------
/client/styles/app/staging.css:
--------------------------------------------------------------------------------
1 | tr{background-color: lightblue;}
2 |
--------------------------------------------------------------------------------
/server/zoom/agent/check/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'davidshrader'
2 |
--------------------------------------------------------------------------------
/server/zoom/agent/web/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'nick.moskwinski'
2 |
--------------------------------------------------------------------------------
/server/zoom/www/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | include = zoom/*
3 |
4 |
--------------------------------------------------------------------------------
/server/zoom/www/entities/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'davidshrader'
2 |
--------------------------------------------------------------------------------
/server/zoom/agent/entities/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'nick.moskwinski'
2 |
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = 'nick.moskwinski'
2 |
--------------------------------------------------------------------------------
/client/images/vadar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/images/vadar.jpg
--------------------------------------------------------------------------------
/client/images/stg/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/images/stg/favicon.ico
--------------------------------------------------------------------------------
/docker/zoom/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -x
3 | docker build --rm -t local/zoom -f docker/zoom/Dockerfile .
4 |
--------------------------------------------------------------------------------
/client/images/prod/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/images/prod/favicon.ico
--------------------------------------------------------------------------------
/docker/sentinel/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -x
3 | docker build --rm -t local/sentinel -f docker/sentinel/Dockerfile .
4 |
--------------------------------------------------------------------------------
/client/styles/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/client/classes/applicationStateArray.js:
--------------------------------------------------------------------------------
1 | define(['knockout'], function(ko) {
2 | return new ko.observableArray([]);
3 | });
4 |
--------------------------------------------------------------------------------
/client/styles/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/client/styles/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/client/styles/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/client/viewmodels/faq/serverConfig.js:
--------------------------------------------------------------------------------
1 | define([ 'model/loginModel' ], function(login) {
2 | return {
3 | login: login
4 | };
5 | });
6 |
--------------------------------------------------------------------------------
/client/styles/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/client/styles/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/client/styles/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spottradingllc/zoom/HEAD/client/styles/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/client/views/faq/serverConfig.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apidoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Zoom API Documentation",
3 | "version": "1.0.0",
4 | "description": "Documentation for the Zoom API.",
5 | "title": "Zoom API Documentation"
6 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | sh docker/zoom/build.sh
3 | sh docker/sentinel/build.sh
4 |
5 | zoom:
6 | sh docker/zoom/build.sh
7 |
8 | sentinel:
9 | sh docker/sentinel/build.sh
10 |
--------------------------------------------------------------------------------
/client/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.jshintrc",
3 | "node": false,
4 | "browser": true,
5 | "devel": true,
6 | "globals": {
7 | "define": true,
8 | "require": true
9 | }
10 | }
--------------------------------------------------------------------------------
/server/zoom/agent/check/always_fail.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | if "%1" == "--succeed" goto succeed
4 | if NOT "%1" == "--succeed" goto fail
5 |
6 | :succeed
7 | EXIT /B 0
8 |
9 | :fail
10 | EXIT /B 1
--------------------------------------------------------------------------------
/client/viewmodels/faq/applicationState.js:
--------------------------------------------------------------------------------
1 | define([ 'model/loginModel', 'model/constants' ], function(login, constants) {
2 | return {
3 | login: login,
4 | constants: constants
5 | };
6 | });
7 |
--------------------------------------------------------------------------------
/docker/zoom/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM local/zoom-onbuild
2 | MAINTAINER Jeremy Alons
3 | EXPOSE 8889
4 | ENTRYPOINT cd /opt/spot/zoom/server; EnvironmentToUse='Staging' sh /opt/spot/zoom/scripts/local_zoom.sh
5 |
--------------------------------------------------------------------------------
/docker/sentinel/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM local/zoom-onbuild
2 | MAINTAINER Jeremy Alons
3 | EXPOSE 8889
4 | ENTRYPOINT cd /opt/spot/zoom/server; EnvironmentToUse='Staging' sh /opt/spot/zoom/scripts/local_sentinel.sh
5 |
--------------------------------------------------------------------------------
/test/noop_test.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 |
4 | class NoOpTest(TestCase):
5 | def setUp(self):
6 | print "Setup"
7 |
8 | def tearDown(self):
9 | print "TearDown"
10 |
11 | def test_noop(self):
12 | print "Noop"
13 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": "nofunc",
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "strict": true,
10 | "undef": true,
11 | "unused": "vars",
12 | "boss": true,
13 | "eqnull": true,
14 | "node": true
15 | }
16 |
--------------------------------------------------------------------------------
/test/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | VENV_DIR=$PWD/venv
4 | /bin/echo 'Running bootstrap';
5 | ./scripts/bootstrap.sh $VENV_DIR > /dev/null 2>&1 || exit 1
6 |
7 | cd server/ || exit 1
8 |
9 | /bin/echo 'Starting tests';
10 | $VENV_DIR/bin/nosetests -v ../test/ --with-cov --cover-html --xunit-file=test.xml --with-xunit || exit 1
11 |
--------------------------------------------------------------------------------
/scripts/local_zoom.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PROJ_PATH=/opt/spot/zoom/
3 | APPPATH="${PROJ_PATH}/server"
4 | VENV_PATH="${PROJ_PATH}/venv/"
5 | STARTCMD="python $APPPATH/zoom.py -p 8889 -e Staging"
6 | TIMEOUT=30
7 | RUNLOG=$APPPATH/logs/web_stdout
8 |
9 | echo "cd $APPPATH; . ${VENV_PATH}/bin/activate; $STARTCMD "
10 | cd $APPPATH; . ${VENV_PATH}/bin/activate; $STARTCMD
11 |
--------------------------------------------------------------------------------
/client/bindings/uppercase.js:
--------------------------------------------------------------------------------
1 | define(['knockout'],
2 | function(ko) {
3 |
4 | /******* CAPITALIZATION EXTENDER *******/
5 | ko.extenders.uppercase = function(target, option) {
6 | target.subscribe(function(newValue) {
7 | target(newValue.toUpperCase());
8 | });
9 | return target;
10 | };
11 | /****** END CAPITALIZATION EXTENDER *******/
12 |
13 | });
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /doc
3 | .project
4 | .cproject
5 | .pydevproject
6 | *.pyc
7 | *.sw*
8 | *.autosave
9 | venv/
10 | core.*
11 | cover/
12 | *.coverage
13 | test_client.py
14 | .idea
15 | bootstrap_log.txt
16 | lib/
17 | *.dot
18 | *.png
19 |
20 | # Logs and databases #
21 | ######################
22 | logs/
23 | *.log
24 |
25 | # OS generated files #
26 | ######################
27 | .DS_Store
28 | .DS_Store?
29 | ._*
30 | .Spotlight-V100
31 | .Trashes
32 | ehthumbs.db
33 | Thumbs.db
34 |
--------------------------------------------------------------------------------
/test/entities/custom_filter_test.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from zoom.www.entities.custom_filter import CustomFilter
3 |
4 |
5 | class TestCustomFilter(TestCase):
6 | def setUp(self):
7 | self.filter = CustomFilter(name="1", login_name="2", parameter="3",
8 | search_term="4", inversed="5")
9 |
10 | def test_failed(self):
11 | self.assertEquals({'searchTerm': '4', 'inversed': '5', 'loginName': '2',
12 | 'parameter': '3', 'name': '1'},
13 | self.filter.to_dictionary())
--------------------------------------------------------------------------------
/server/zoom/www/entities/custom_filter.py:
--------------------------------------------------------------------------------
1 | class CustomFilter(object):
2 | def __init__(self, name, login_name, parameter,
3 | search_term, inversed):
4 | self.name = name
5 | self.login_name = login_name
6 | self.parameter = parameter
7 | self.search_term = search_term
8 | self.inversed = inversed
9 |
10 | def to_dictionary(self):
11 | return {
12 | 'name': self.name,
13 | 'loginName': self.login_name,
14 | 'parameter': self.parameter,
15 | 'searchTerm': self.search_term,
16 | 'inversed': self.inversed
17 | }
18 |
--------------------------------------------------------------------------------
/client/bindings/radio.js:
--------------------------------------------------------------------------------
1 | define(['knockout', 'jquery'],
2 | function(ko, $) {
3 |
4 | /******* RADIO BUTTON BINDING *******/
5 | ko.bindingHandlers.radio = {
6 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
7 | $(element).click(function() {
8 | valueAccessor()(viewModel);
9 | });
10 | },
11 | update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
12 | var value = ko.unwrap(valueAccessor());
13 | if (viewModel == value) {
14 | $(element).addClass("active");
15 | }
16 | else {
17 | $(element).removeClass("active");
18 | }
19 | }
20 | };
21 | /****** END RADIO BUTTON BINDING *******/
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/client/libs/jquery.ba-throttle-debounce.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery throttle / debounce - v1.1 - 3/7/2010
3 | * http://benalman.com/projects/jquery-throttle-debounce-plugin/
4 | *
5 | * Copyright (c) 2010 "Cowboy" Ben Alman
6 | * Dual licensed under the MIT and GPL licenses.
7 | * http://benalman.com/about/license/
8 | */
9 | (function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this);
--------------------------------------------------------------------------------
/server/zoom/agent/check/always_fail:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | This check is designed purely for testing.
5 | It simulates a check failure/success.
6 | """
7 |
8 | import sys
9 | from argparse import ArgumentParser
10 |
11 |
12 | if __name__ == "__main__":
13 | parser = ArgumentParser(description='Designed for testing. Will exit with 1'
14 | ' unless you pass the -s option.')
15 | parser.add_argument('-s', '--succeed', action='store_true', required=False,
16 | help='Return success.')
17 | args = parser.parse_args()
18 |
19 | if args.succeed:
20 | print ('Exiting with 0 (success).')
21 | sys.exit(0)
22 | else:
23 | print ('Exiting with 1 (failure).')
24 | sys.exit(1)
25 |
--------------------------------------------------------------------------------
/scripts/local_sentinel.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | APP="ZKagent"
3 | LOGDATE=`date +%C%y%m%d`
4 | LOGTIME=`date +%H%M%S`
5 |
6 | PROJ_PATH="/opt/spot/zoom/"
7 |
8 | echo $(git rev-parse --short --verify HEAD) > $PROJ_PATH/version.txt
9 |
10 | APPPATH="${PROJ_PATH}/server"
11 | VENV_PATH="/opt/spot/zoom/venv"
12 | STARTCMD="python sentinel.py -v"
13 | RUNLOG=$APPPATH/logs/stdout
14 |
15 | export PATH=$PATH:/bin
16 | if [ -f /etc/profile.d/spotdev.sh ]; then source /etc/profile.d/spotdev.sh; fi
17 |
18 |
19 | if [ -f $RUNLOG ]; then
20 | mv $RUNLOG $RUNLOG.$LOGDATE.$LOGTIME
21 | fi;
22 |
23 | # check for log dir
24 | if [ ! -d $APPPATH/logs ]; then
25 | /bin/mkdir $APPPATH/logs;
26 | /bin/touch $RUNLOG;
27 | fi;
28 |
29 | cd $APPPATH; source ${VENV_PATH}/bin/activate; $STARTCMD # > $RUNLOG 2>&1
30 |
--------------------------------------------------------------------------------
/server/sentinel.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import logging
4 | import platform
5 |
6 | import tornado.ioloop
7 | from zoom.agent.util.helpers import setup_logging, parse_args
8 | from zoom.agent.entities.daemon import SentinelDaemon
9 |
10 |
11 | if __name__ == '__main__':
12 | args = parse_args()
13 | setup_logging(verbose=args.verbose)
14 | if 'Linux' in platform.platform():
15 | from setproctitle import setproctitle
16 | logging.info('Changing the process name to ZKagent')
17 | setproctitle('ZKagent') # Changes process name
18 |
19 | with SentinelDaemon(args.port) as sentinel:
20 | logging.info('Starting web server loop...')
21 | print 'Ready to go!'
22 | tornado.ioloop.IOLoop.instance().start()
23 | logging.info('Exiting Application')
--------------------------------------------------------------------------------
/server/zoom/www/messages/global_mode_message.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from zoom.common.types import UpdateType
4 |
5 |
6 | class GlobalModeMessage(object):
7 | def __init__(self, mode):
8 | self._message_type = UpdateType.GLOBAL_MODE_UPDATE
9 | self._operation_type = None
10 | self._mode = mode
11 |
12 | @property
13 | def message_type(self):
14 | return self._message_type
15 |
16 | @property
17 | def operation_type(self):
18 | return self._operation_type
19 |
20 | @property
21 | def mode(self):
22 | return self._mode
23 |
24 | def to_json(self):
25 | return json.dumps({
26 | "update_type": self._message_type,
27 | "operation_type": self._operation_type,
28 | "global_mode": self._mode
29 | })
30 |
--------------------------------------------------------------------------------
/client/bindings/tooltip.js:
--------------------------------------------------------------------------------
1 | define(['knockout', 'jquery'],
2 | function(ko, $) {
3 |
4 | /******* TOOLTIP BUTTON BINDING *******/
5 | ko.bindingHandlers.tooltip = {
6 | init: function(element, valueAccessor) {
7 | var local = ko.utils.unwrapObservable(valueAccessor()),
8 | options = {};
9 |
10 | ko.utils.extend(options, ko.bindingHandlers.tooltip.options);
11 | ko.utils.extend(options, local);
12 |
13 | $(element).tooltip(options);
14 |
15 | ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
16 | $(element).tooltip("destroy");
17 | });
18 | },
19 | options: {
20 | placement: "top",
21 | trigger: "hover",
22 | html: "true"
23 | }
24 | };
25 | /****** END TOOLTIP BUTTON BINDING *******/
26 |
27 | });
28 |
29 |
--------------------------------------------------------------------------------
/server/zoom/agent/web/templates/log.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sentinel Log
6 |
19 |
20 |
21 |
22 | {% for d in data %}
23 |
26 | {% end %}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/client/styles/app/applicationState.css:
--------------------------------------------------------------------------------
1 | #application_state_page {
2 | margin-top: 40px;
3 | }
4 |
5 | .navbar-drop {
6 | padding: 15px;
7 | }
8 |
9 | .view-tabs {
10 | margin-bottom: 5px;
11 | }
12 |
13 | .app-row {
14 | margin-top: 5px;
15 | }
16 |
17 | .cursor-pointer{
18 | cursor: pointer;
19 | }
20 |
21 | .dep-list{
22 | list-style-type: none;
23 | }
24 |
25 | .server-opt{
26 | margin-right: 10px;
27 | }
28 |
29 | .filter-name{
30 | width: 10% !important;
31 | }
32 |
33 | .scroll-footer{
34 | display: inline-block;
35 | text-align: center;
36 | bottom: 0;
37 | opacity: 0.7;
38 | height: 20px;
39 | width: 100%;
40 | position: fixed;
41 | background-color: #163F67;
42 | color: white;
43 | Z-index: 99;
44 | }
45 |
46 | .dropdown-menu {
47 | max-height: 400px;
48 | overflow: hidden;
49 | overflow-y: auto;
50 | }
51 |
52 | a.dropdown-toggle {
53 | position: relative;
54 | }
55 |
--------------------------------------------------------------------------------
/client/styles/app/dependencyMaps.css:
--------------------------------------------------------------------------------
1 | .node rect {
2 | cursor: pointer;
3 | fill: #fff;
4 | fill-opacity: .5;
5 | stroke: #3182bd;
6 | stroke-width: 1.5px;
7 | }
8 |
9 | .node text {
10 | font: 20px sans-serif;
11 | pointer-events: all;
12 | cursor: pointer;
13 | }
14 |
15 | path.link {
16 | fill: none;
17 | stroke: #9ecae1;
18 | stroke-width: 1.5px;
19 | }
20 |
21 | .node circle {
22 | fill: #fff;
23 | stroke: steelblue;
24 | stroke-width: 1.5px;
25 | }
26 |
27 | .chart {
28 | display: block;
29 | margin: auto;
30 | font-size: 11px;
31 | }
32 |
33 | rect {
34 | stroke: #eee;
35 | fill-opacity: .8;
36 | }
37 |
38 | rect.parent {
39 | cursor: pointer;
40 | }
41 |
42 | circle {
43 | stroke: #999;
44 | pointer-events: all;
45 | }
46 |
47 | circle.parent {
48 | fill-opacity: .1;
49 | stroke: steelblue;
50 | }
51 |
52 | circle.parent:hover {
53 | stroke: #ff7f0e;
54 | stroke-width: .5px;
55 | }
56 |
57 | circle.child {
58 | pointer-events: none;
59 | }
60 |
--------------------------------------------------------------------------------
/server/zoom/www/messages/timing_estimate.py:
--------------------------------------------------------------------------------
1 | import json
2 | from zoom.common.types import UpdateType
3 |
4 |
5 | class TimeEstimateMessage(object):
6 | def __init__(self):
7 | self._message_type = UpdateType.TIMING_UPDATE
8 | self._contents = dict()
9 |
10 | @property
11 | def message_type(self):
12 | return self._message_type
13 |
14 | @property
15 | def contents(self):
16 | return self._contents
17 |
18 | def update(self, item):
19 | """
20 | :type item: dict
21 | """
22 | self._contents.update(item)
23 |
24 | def combine(self, message):
25 | """
26 | :type message: TimeEstimateMessage
27 | """
28 | self._contents.update(message.contents)
29 |
30 | def clear(self):
31 | self._contents.clear()
32 |
33 | def to_json(self):
34 | _dict = {}
35 | _dict.update({
36 | "update_type": self._message_type,
37 | })
38 | _dict.update(self.contents)
39 |
40 | return json.dumps(_dict)
41 |
--------------------------------------------------------------------------------
/server/zoom.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import platform
3 | import traceback
4 |
5 | from argparse import ArgumentParser
6 | from zoom.www.entities.session import Session
7 |
8 |
9 | def parse_args():
10 | ap = ArgumentParser()
11 | ap.add_argument('-p', '--port', type=int,
12 | help='Which port the server should listen on. ')
13 | ap.add_argument('-e', '--environment',
14 | help='The environment to connect to. '
15 | 'This is an over-ride for the setting in the config')
16 | return ap.parse_args()
17 |
18 |
19 | if __name__ == "__main__":
20 | try:
21 | if 'Linux' in platform.platform():
22 | from setproctitle import setproctitle
23 | logging.info('Changing the process name to Zoom')
24 | setproctitle('Zoom') # Changes process name
25 |
26 | settings = vars(parse_args())
27 | session = Session(**settings)
28 | session.start()
29 |
30 | session.stop()
31 |
32 | except Exception as e:
33 | print traceback.format_exc()
34 | print str(e)
35 |
--------------------------------------------------------------------------------
/server/zoom/agent/entities/thread_safe_object.py:
--------------------------------------------------------------------------------
1 | class ThreadSafeObject(object):
2 | def __init__(self, value, callback=None):
3 | self.value = value
4 | self._callback = callback
5 |
6 | def set_value(self, value, run_callback=True):
7 | self.value = value
8 | if self._callback is not None and run_callback:
9 | self._callback()
10 |
11 | def get(self, key, default=None):
12 | if isinstance(self.value, dict):
13 | return self.value.get(key, default)
14 | else:
15 | return None
16 |
17 | def __eq__(self, other):
18 | return self.value == other
19 |
20 | def __ne__(self, other):
21 | return self.value != other
22 |
23 | def __str__(self):
24 | return str(self.value)
25 |
26 | def __repr__(self):
27 | return '{0}(value={1})'.format(self.__class__.__name__, self.value)
28 |
29 |
30 | class ApplicationMode(ThreadSafeObject):
31 | AUTO = "auto"
32 | MANUAL = "manual"
33 |
34 | def __init__(self, val, callback=None):
35 | ThreadSafeObject.__init__(self, val, callback=callback)
36 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/list_pillar_servers_handler.py:
--------------------------------------------------------------------------------
1 | import tornado.ioloop
2 | import tornado.web
3 | import logging
4 | import json
5 |
6 | from zoom.common.decorators import TimeThis
7 |
8 |
9 | class ListPillarServersHandler(tornado.web.RequestHandler):
10 | @property
11 | def zk(self):
12 | """
13 | :rtype: kazoo.client.KazooClient
14 | """
15 | return self.application.zk
16 |
17 | @TimeThis(__file__)
18 | def get(self):
19 | """
20 | @api {get} /api/v1/pillar/list_servers/ List pillar servers
21 | @apiVersion 1.0.0
22 | @apiName GetPilServers
23 | @apiGroup Pillar
24 | @apiSuccessExample {json} Success-Response:
25 | HTTP/1.1 200 OK
26 | [
27 | "foo.example.com",
28 | "bar.example.com"
29 | ]
30 | """
31 | logging.info("Generating list of servers")
32 | # get all nodes at the root config path
33 | path = self.application.configuration.pillar_path
34 | logging.info(path)
35 | nodes = self.zk.get_children(path)
36 | self.write(json.dumps(nodes))
37 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/salt_master_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import httplib
4 | import tornado.web
5 | import tornado.httpclient
6 |
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class SaltMasterHandler(tornado.web.RequestHandler):
11 | @property
12 | def environment(self):
13 | """
14 | :rtype: str
15 | """
16 | return self.application.configuration.environment
17 |
18 | def salt(self):
19 | """
20 | :rtype: str
21 | """
22 | return self.application.configuration.salt_settings
23 |
24 | @TimeThis(__file__)
25 | def get(self):
26 | """
27 | @api {get} /api/v1/saltmaster/ Get salt settings
28 | @apiVersion 1.0.0
29 | @apiName GetSaltSettings
30 | @apiGroup Salt
31 | """
32 | try:
33 | self.write({'salt': self.salt()})
34 |
35 | except Exception as e:
36 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
37 | self.write(json.dumps({'errorText': str(e)}))
38 | logging.exception(e)
39 |
40 | self.set_header('Content-Type', 'application/json')
41 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/zoom_ws_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import tornado.websocket
3 |
4 | from zoom.common.decorators import TimeThis
5 |
6 |
7 | class ZoomWSHandler(tornado.websocket.WebSocketHandler):
8 |
9 | @property
10 | def socket_clients(self):
11 | """
12 | :rtype: list
13 | """
14 | return self.application.data_store.web_socket_clients
15 |
16 | @TimeThis(__file__)
17 | def open(self):
18 | logging.debug("[WEBSOCKET] Opening")
19 | self.socket_clients.append(self)
20 | logging.debug('Added websocket client {0}. Total clients: {1}'
21 | .format(self.request.remote_ip, len(self.socket_clients)))
22 |
23 | @TimeThis(__file__)
24 | def on_message(self, message):
25 | logging.debug("[WEBSOCKET] Message: '{0}' for client {1}"
26 | .format(message, self.request.remote_ip))
27 |
28 | @TimeThis(__file__)
29 | def on_close(self):
30 | self.socket_clients.remove(self)
31 | logging.debug("[WEBSOCKET] Closed for client {0}. Total clients: {1}"
32 | .format(self.request.remote_ip, len(self.socket_clients)))
33 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/environment_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import httplib
4 | import tornado.web
5 | import tornado.httpclient
6 |
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class EnvironmentHandler(tornado.web.RequestHandler):
11 | @property
12 | def environment(self):
13 | """
14 | :rtype: str
15 | """
16 | return self.application.configuration.environment
17 |
18 | @TimeThis(__file__)
19 | def get(self):
20 | """
21 | @api {get} /api/v1/environment/ Get Environment
22 | @apiVersion 1.0.0
23 | @apiName GetEnv
24 | @apiGroup Env
25 | @apiSuccessExample {json} Success-Response:
26 | HTTP/1.1 200 OK
27 | {
28 | "environment": "Staging"
29 | }
30 | """
31 | try:
32 |
33 | self.write({'environment': self.environment})
34 |
35 | except Exception as e:
36 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
37 | self.write(json.dumps({'errorText': str(e)}))
38 | logging.exception(e)
39 |
40 | self.set_header('Content-Type', 'application/json')
41 |
--------------------------------------------------------------------------------
/test/config/test_configuration.py:
--------------------------------------------------------------------------------
1 | import mox
2 | from unittest import TestCase
3 |
4 | from kazoo.client import KazooClient
5 | from zoom.www.config.configuration import Configuration
6 | from zoom.common.constants import ZOOM_CONFIG
7 |
8 |
9 | class TestConfiguration(TestCase):
10 |
11 | def setUp(self):
12 | self.mox = mox.Mox()
13 | self.zoom_config = (
14 | '{"web_server": { }, "active_directory": {}, "staging": {}, '
15 | '"production": {}, "zookeeper": {}, "pagerduty": {}, "database": '
16 | '{}, "message_throttle": {}, "logging": {"version": 1}}')
17 |
18 | self.zoo_keeper = self.mox.CreateMock(KazooClient)
19 | self.zoo_keeper.get(ZOOM_CONFIG).AndReturn((self.zoom_config, None))
20 |
21 | self.comp_name = "Test Predicate Or"
22 |
23 | def test_actual(self):
24 | self.mox.ReplayAll()
25 | # TODO: Need to mock out kazoo client here
26 | Configuration(self.zoo_keeper)
27 | self.mox.VerifyAll()
28 |
29 | def test_failed(self):
30 | caught = False
31 | try:
32 | Configuration()
33 | except Exception:
34 | caught = True
35 | self.assertTrue(caught)
36 |
--------------------------------------------------------------------------------
/client/viewmodels/pillarConfig.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'durandal/app',
4 | 'knockout',
5 | 'service',
6 | 'jquery',
7 | 'd3',
8 | 'model/loginModel',
9 | 'model/pillarModel',
10 | 'model/adminModel',
11 | 'bindings/radio',
12 | 'bindings/tooltip'
13 | ],
14 | function(app, ko, service, $, d3, login, pillarModel, admin) {
15 | self.login = login;
16 | self.adminModel = admin;
17 | self.pillarModel = new pillarModel(self.login, self.adminModel);
18 | self.attached = function() {
19 | self.pillarModel.pillarApiModel.loadServers(true);
20 | };
21 | self.activate = function(host) {
22 | if (host !== null) {
23 | self.pillarModel.searchVal(host)
24 | }
25 | };
26 | self.detached = function() {
27 | self.pillarModel.searchVal("");
28 | self.pillarModel.newNodeName("");
29 | };
30 |
31 | return {
32 | pillarModel: self.pillarModel,
33 | detached: self.detached,
34 | attached: self.attached,
35 | activate: self.activate
36 | };
37 | });
38 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/pagerduty_services_handler.py:
--------------------------------------------------------------------------------
1 | import httplib
2 | import json
3 | import logging
4 |
5 | import tornado.web
6 |
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class PagerDutyServicesHandler(tornado.web.RequestHandler):
11 | @property
12 | def data_store(self):
13 | """
14 | :rtype: zoom.www.cache.data_store.DataStore
15 | """
16 | return self.application.data_store
17 |
18 | @TimeThis(__file__)
19 | def get(self):
20 | """
21 | @api {get} /api/v1/pagerduty/services/ Get PagerDuty Services
22 | @apiVersion 1.0.0
23 | @apiName GetPDSvc
24 | @apiGroup PagerDuty
25 | @apiSuccessExample {json} Success-Response:
26 | HTTP/1.1 200 OK
27 | {
28 | "foo": "00000000000000000000000000000000"
29 | "bar": "11111111111111111111111111111111",
30 | }
31 | """
32 | try:
33 | self.write(json.dumps(self.data_store.pagerduty_services))
34 |
35 | except Exception as e:
36 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
37 | self.write(json.dumps({'errorText': str(e)}))
38 | logging.exception(e)
39 |
--------------------------------------------------------------------------------
/test/test_utils.py:
--------------------------------------------------------------------------------
1 | class StatMock:
2 | def __init__(self):
3 | self.ephemeralOwner = None
4 | self.started = None
5 |
6 |
7 | class EventMock:
8 | def __init__(self):
9 | self.path = None
10 |
11 |
12 | class ConfigurationMock:
13 | def __init__(self):
14 | self.application_state_path = None
15 | self.global_mode_path = None
16 | self.agent_state_path = None
17 | self.environment = None
18 | self.throttle_interval = 1
19 | self.pagerduty_subdomain = "sub"
20 | self.pagerduty_api_token = "token"
21 | self.pagerduty_default_svc_key = "key"
22 | self.pagerduty_alert_footer = "footer"
23 | self.alert_path = "/path"
24 | self.override_node = "/override_foo"
25 | self.graphite_host = 'graphite_host'
26 | self.graphite_recheck = '5m'
27 |
28 |
29 | class ApplicationStateMock:
30 | def __init__(self):
31 | self.mock_dict = None
32 | self.configuration_path = "test/path"
33 |
34 | def to_dictionary(self):
35 | return self.mock_dict
36 |
37 | class FakeMessage:
38 | def __init__(self, data):
39 | self.data = data
40 |
41 | def to_json(self):
42 | return self.data
43 |
--------------------------------------------------------------------------------
/client/classes/predicateFactory.js:
--------------------------------------------------------------------------------
1 | define(['knockout', 'classes/LogicPredicate', 'classes/Predicate'],
2 | function(ko, LogicPredicate, Predicate) {
3 |
4 | var Factory = {};
5 | Factory.newPredicate = function(parent, type) {
6 | if (type === 'and' || type === 'or' || type === 'not') {
7 | return new LogicPredicate(Factory, type, parent);
8 | }
9 | else {
10 | return new Predicate(parent);
11 | }
12 | };
13 |
14 | Factory.firstChild = function(node) {
15 | if (typeof node === 'undefined') {return null;}
16 |
17 | var child = node.firstChild;
18 | // nodeType 1 == ELEMENT_NODE
19 | while (child !== null && child.nodeType !== 1) {
20 | child = child.nextSibling;
21 | }
22 | return child;
23 | };
24 |
25 | Factory.nextChild = function(child) {
26 | child = child.nextSibling;
27 | // nodeType 1 == ELEMENT_NODE
28 | while (child !== null && child.nodeType !== 1) {
29 | child = child.nextSibling;
30 | }
31 | return child;
32 | };
33 | return Factory;
34 |
35 | });
36 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "validateIndentation": 4,
3 | "requireSpaceAfterLineComment": true,
4 | "requireCamelCaseOrUpperCaseIdentifiers": true,
5 | "requireCapitalizedConstructors": true,
6 | "disallowTrailingComma": true,
7 | "disallowTrailingWhitespace": true,
8 | "disallowMixedSpacesAndTabs": true,
9 | "requireSpaceAfterKeywords": [
10 | "if",
11 | "else",
12 | "for",
13 | "while",
14 | "do",
15 | "switch",
16 | "return",
17 | "try",
18 | "catch"
19 | ],
20 | "maximumLineLength": 120,
21 | "validateQuoteMarks": "'",
22 | "disallowMultipleVarDecl": true,
23 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"],
24 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"],
25 | "requireSpaceBeforeBinaryOperators": [
26 | "=",
27 | "+",
28 | "-",
29 | "/",
30 | "*",
31 | "==",
32 | "===",
33 | "!=",
34 | "!=="
35 | ],
36 | "disallowSpacesInFunction": {
37 | "beforeOpeningRoundBrace": true
38 | },
39 | "requireSpacesInFunction": {
40 | "beforeOpeningCurlyBrace": true
41 | },
42 | "requireSpacesInConditionalExpression": {
43 | "afterTest": true,
44 | "beforeConsequent": true,
45 | "afterConsequent": true,
46 | "beforeAlternate": true
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/list_servers_handler.py:
--------------------------------------------------------------------------------
1 | import tornado.ioloop
2 | import tornado.web
3 | import logging
4 | import json
5 |
6 | from zoom.common.decorators import TimeThis
7 |
8 |
9 | class ListServersHandler(tornado.web.RequestHandler):
10 | @property
11 | def agent_configuration_path(self):
12 | """
13 | :rtype: str
14 | """
15 | return self.application.configuration.agent_configuration_path
16 |
17 | @property
18 | def zk(self):
19 | """
20 | :rtype: kazoo.client.KazooClient
21 | """
22 | return self.application.zk
23 |
24 | @TimeThis(__file__)
25 | def get(self):
26 | """
27 | @api {get} /api/v1/config/list_servers/ List sentinel servers
28 | @apiVersion 1.0.0
29 | @apiName ListSentServers
30 | @apiGroup Sentinel Config
31 | @apiSuccessExample {json} Success-Response:
32 | HTTP/1.1 200 OK
33 | [
34 | "foo.example.com",
35 | "bar.example.com"
36 | ]
37 | """
38 | logging.info('Generating list of nodes')
39 |
40 | # get all nodes at the root config path
41 | nodes = self.zk.get_children(self.agent_configuration_path)
42 | self.write(json.dumps(nodes))
43 |
--------------------------------------------------------------------------------
/server/zoom/agent/entities/unique_queue.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from collections import deque
3 |
4 | from zoom.agent.task.task import Task
5 |
6 |
7 | class UniqueQueue(deque):
8 | def __init__(self):
9 | deque.__init__(self)
10 | self._log = logging.getLogger('sent.q')
11 |
12 | def append_unique(self, task, sender='', first=False):
13 | """
14 | :type task: zoom.agent.task.task.Task
15 | :type sender: str
16 | :type first: bool
17 | :rtype: bool
18 | """
19 | if not isinstance(task, Task):
20 | self._log.error('Queue items must be of type Task.')
21 | return False
22 |
23 | if task in self:
24 | self._log.info('Object {0} already in queue. Not adding again.'
25 | .format(task))
26 | return False
27 | else:
28 | if first:
29 | self._log.info('{0} Adding "{1}" to the head of the queue.'
30 | .format(sender, task.name))
31 | self.appendleft(task)
32 | else:
33 | self._log.info('{0} Adding "{1}" to the tail of the queue.'
34 | .format(sender, task.name))
35 | self.append(task)
36 | return True
37 |
--------------------------------------------------------------------------------
/client/model/externalLinkModel.js:
--------------------------------------------------------------------------------
1 | define([],
2 | function() {
3 | var externalLink = {};
4 |
5 | var urls = {
6 | prodErrors: 'http://kibanaproduction/index.html#/dashboard/elasticsearch/Errors',
7 | prodStats: 'http://kibanaproduction/index.html#/dashboard/elasticsearch/Zoom_Production_stats',
8 | stagingStats: 'http://kibanastaging:9292/index.html#/dashboard/elasticsearch/Zoom_Staging_stats',
9 | apiDoc: "http://" + document.location.host + "/doc"
10 | };
11 |
12 | var createLink = function(url) {
13 | var form = document.createElement("form");
14 | form.method = "GET";
15 | form.action = url;
16 | form.target = "_blank";
17 | form.submit();
18 | };
19 |
20 | externalLink.ProdErrorsURL = function() {
21 | createLink(urls.prodErrors)
22 | };
23 |
24 | externalLink.ProdStatsURL = function() {
25 | createLink(urls.prodStats)
26 | };
27 |
28 | externalLink.StagingStatsURL = function() {
29 | createLink(urls.stagingStats)
30 | };
31 | externalLink.apiDoc = function() {
32 | createLink(urls.apiDoc)
33 | };
34 |
35 | return externalLink;
36 | });
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/predicate/zkgut_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 | import unittest
3 |
4 | from zoom.agent.predicate.zkgut import ZookeeperGoodUntilTime
5 |
6 |
7 | class ZookeeperGoodUntilTimeTest(unittest.TestCase):
8 | def setUp(self):
9 | self.mox = mox.Mox()
10 | self.interval = 0.1
11 |
12 | def tearDown(self):
13 | self.mox.UnsetStubs()
14 |
15 | def test_start(self):
16 |
17 | pred = ZookeeperGoodUntilTime("test", {}, None, "/path", None,
18 | interval=self.interval)
19 | self.mox.StubOutWithMock(pred, "_watch_node")
20 | pred._watch_node()
21 |
22 | self.mox.ReplayAll()
23 |
24 | print "This test should complete quickly"
25 | pred.start()
26 | pred.start() # should noop
27 | pred.start() # should noop
28 | pred.stop()
29 |
30 | self.mox.VerifyAll()
31 |
32 | def test_stop(self):
33 |
34 | pred = ZookeeperGoodUntilTime("test", {}, None, "/path", None,
35 | interval=self.interval)
36 | self.mox.StubOutWithMock(pred, "_watch_node")
37 | pred._watch_node()
38 |
39 | self.mox.ReplayAll()
40 |
41 | pred.start()
42 | pred.stop()
43 | pred.stop()
44 | pred.stop()
45 |
46 | self.mox.VerifyAll()
47 |
--------------------------------------------------------------------------------
/client/styles/app/pillarConfig.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .table-striped.edit>tbody>tr:nth-child(even)>td,
4 | .table-striped.edit>tbody>tr:nth-child(even)>th {
5 | background-color: #F5F3A2;
6 | }
7 |
8 |
9 | .table-striped.edit>tbody>tr:nth-child(odd)>td,
10 | .table-striped.edit>tbody>tr:nth-child(odd)>th {
11 | background-color: #F5FFA2;
12 | }
13 |
14 | .table-striped>tbody>tr:nth-child(odd)>td,
15 | .table-striped>tbody>tr:nth-child(odd)>th {
16 | background-color: #FFF;
17 | }
18 |
19 | /* margin-top correction */
20 | .mt {
21 | margin-top: 10px;
22 | }
23 |
24 | /* margin right correction */
25 | .mr {
26 | margin-right: 10px;
27 | }
28 |
29 | .mb {
30 | margin-bottom: 5px;
31 | }
32 |
33 | /* Delete minus sign next to each key */
34 | .del {
35 | float: right;
36 | color: #d9534f;
37 | cursor: pointer;
38 | }
39 |
40 | .contrast {
41 | color: white;
42 | }
43 |
44 | .overlay {
45 | z-index: 2000;
46 | position: fixed;
47 | width: 10%;
48 | min-width: 200px;
49 | margin-left: 41%;
50 | margin-top: 1%;
51 | }
52 |
53 | /* Allows display of second modal over the first when necessary
54 | // Credit: http://bit.ly/1FAsS9t
55 | */
56 | .modal-backdrop.fade.in:nth-child(2){
57 | z-index: 1060 !important;
58 | }
59 |
60 | .second {
61 | z-index: 1061 !important
62 | }
63 |
--------------------------------------------------------------------------------
/test/predicate/health_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 | import time
3 | import unittest
4 |
5 | from zoom.agent.predicate.health import PredicateHealth
6 | from zoom.common.types import PlatformType
7 |
8 |
9 | class PredicateHealthTest(unittest.TestCase):
10 | def setUp(self):
11 | self.mox = mox.Mox()
12 | self.interval = 0.1
13 |
14 | def tearDown(self):
15 | self.mox.UnsetStubs()
16 |
17 | def test_start(self):
18 |
19 | pred = PredicateHealth("test", "echo", self.interval, PlatformType.LINUX)
20 | self.mox.StubOutWithMock(pred, "_run")
21 | pred._run().MultipleTimes()
22 |
23 | self.mox.ReplayAll()
24 |
25 | print "This test should complete quickly"
26 | pred.start()
27 | pred.start() # should noop
28 | pred.start() # should noop
29 | time.sleep(0.25) # give other thread time to check
30 | pred.stop()
31 |
32 | self.mox.VerifyAll()
33 |
34 | def test_stop(self):
35 |
36 | pred = PredicateHealth("test", "echo", self.interval, PlatformType.LINUX)
37 | self.mox.StubOutWithMock(pred, "_run")
38 | pred._run().MultipleTimes()
39 |
40 | self.mox.ReplayAll()
41 |
42 | pred.start()
43 | time.sleep(0.25) # give other thread time to check
44 | pred.stop()
45 | pred.stop()
46 | pred.stop()
47 |
48 | self.mox.VerifyAll()
49 |
--------------------------------------------------------------------------------
/client/model/adminModel.js:
--------------------------------------------------------------------------------
1 | define(['knockout', 'jquery', './loginModel'],
2 | function(ko, $, login) {
3 | var admin = {};
4 |
5 | admin._login = login;
6 | admin._enabled = ko.observable(false);
7 | admin.showProgress = ko.observable(true);
8 | admin.disable = function() {
9 | admin._enabled(false);
10 | };
11 | admin.enable = function() {
12 | if (login.elements.authenticated()) {
13 | admin._enabled(true);
14 | }
15 | else {
16 | swal('You must be logged in to use admin');
17 | }
18 | };
19 | admin.enabled = ko.computed(function() {
20 | if (login.elements.authenticated()) {
21 | return admin._enabled();
22 | }
23 | admin._enabled(false);
24 | return false;
25 | });
26 |
27 | admin.clearTasks = function() {
28 | $.ajax({
29 | url: '/api/v1/agent/',
30 | type: 'DELETE',
31 | success: function(data) { swal('Tasks cleared') },
32 | error: function(data) { swal('Failure Clearing Tasks ', '', 'error'); }
33 | });
34 | };
35 |
36 | admin.toggleProgress = function() {
37 | admin.showProgress(!admin.showProgress())
38 | };
39 |
40 | return admin;
41 | });
42 |
--------------------------------------------------------------------------------
/client/viewmodels/sentinelConfig/alertsViewModel.js:
--------------------------------------------------------------------------------
1 | define(['knockout'],
2 | function (ko) {
3 | var AlertsViewModel = {
4 | successMode: ko.observable(false),
5 | successText: ko.observable(''),
6 | errorMode: ko.observable(false),
7 | errorText: ko.observable('')
8 | };
9 |
10 | AlertsViewModel.closeAlerts = function() {
11 | AlertsViewModel.closeError();
12 | AlertsViewModel.closeSuccess();
13 | };
14 |
15 | AlertsViewModel.displaySuccess = function(successMessage) {
16 | AlertsViewModel.closeAlerts();
17 | AlertsViewModel.successMode(true);
18 | AlertsViewModel.successText(successMessage);
19 | };
20 |
21 | AlertsViewModel.displayError = function(errorMessage) {
22 | // TODO: float alerts for visibility
23 | AlertsViewModel.closeAlerts();
24 | AlertsViewModel.errorMode(true);
25 | AlertsViewModel.errorText(errorMessage);
26 | };
27 |
28 | AlertsViewModel.closeSuccess = function() {
29 | AlertsViewModel.successMode(false);
30 | AlertsViewModel.successText('');
31 | };
32 |
33 | AlertsViewModel.closeError = function() {
34 | AlertsViewModel.errorMode(false);
35 | AlertsViewModel.errorText('');
36 | };
37 | return AlertsViewModel;
38 | });
39 |
--------------------------------------------------------------------------------
/server/zoom/agent/web/rest.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import tornado.web
4 |
5 | from zoom.agent.task.base_task_client import BaseTaskClient
6 | from zoom.agent.web.handlers.v1 import (
7 | LogHandler,
8 | TaskHandler,
9 | StatusHandler
10 | )
11 | from zoom.common.handlers import (
12 | LogVerbosityHandler,
13 | RUOKHandler,
14 | VersionHandler
15 | )
16 |
17 |
18 | class RestServer(tornado.web.Application):
19 | def __init__(self, children, version, temp_dir, hostname, zk_object):
20 | """
21 | :type children: dict
22 | """
23 | self.log = logging.getLogger('sent.rest')
24 | self.children = children
25 | self.version = version
26 | self.temp_dir = temp_dir
27 | self.hostname = hostname
28 | self.zk = zk_object
29 | self.task_client = BaseTaskClient(children)
30 | handlers = [
31 | # Versioned
32 | (r"/api/v1/log/?(?P\d+)?", LogHandler),
33 | (r"/api/v1/status/?(?P[\w|\/]+)?", StatusHandler),
34 | (r"/api/v1/task/(?P\w+)/?(?P[\w|\/]+)?", TaskHandler),
35 | # Unversioned
36 | (r'/loglevel/(?P\w+)', LogVerbosityHandler),
37 | (r"/ruok", RUOKHandler),
38 | (r"/version", VersionHandler)
39 | ]
40 | tornado.web.Application.__init__(self, handlers)
41 | self.log.info('Created Rest Server...')
42 |
--------------------------------------------------------------------------------
/test/predicate/pred_simple_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 | import unittest
3 |
4 | from zoom.agent.predicate.simple import SimplePredicate
5 |
6 |
7 | class PredicateSimpleTest(unittest.TestCase):
8 | def setUp(self):
9 | self.mox = mox.Mox()
10 | self.comp_name = "Test Predicate Simple"
11 |
12 | def tearDown(self):
13 | pass
14 |
15 | def testmet_true(self):
16 | pred = self._create_simple_pred(met=True)
17 | self.assertTrue(pred.met)
18 |
19 | def testmet_false(self):
20 | pred = self._create_simple_pred()
21 | self.assertFalse(pred.met)
22 |
23 | def testmet_return_to_false(self):
24 | pred = self._create_simple_pred()
25 | pred.set_met(True)
26 | pred.set_met(False)
27 | self.assertFalse(pred.met)
28 |
29 | def test_equal(self):
30 | pred1 = self._create_simple_pred()
31 | pred2 = self._create_simple_pred()
32 |
33 | self.assertTrue(pred1 == pred2)
34 |
35 | def test_not_equal(self):
36 | pred1 = self._create_simple_pred(cname=self.comp_name)
37 | pred2 = self._create_simple_pred(cname=self.comp_name + "Foo")
38 |
39 | self.assertNotEqual(pred1, pred2)
40 |
41 | def _create_simple_pred(self, cname=None, met=None):
42 | if cname is None:
43 | cname = self.comp_name
44 | s = SimplePredicate(cname, {})
45 | if met is not None:
46 | s.set_met(met)
47 |
48 | return s
--------------------------------------------------------------------------------
/test/predicate/factory_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 | from unittest import TestCase
3 | from zoom.agent.predicate.simple import SimplePredicate
4 | from zoom.agent.predicate.factory import PredicateFactory
5 | from zoom.agent.entities.thread_safe_object import ThreadSafeObject
6 |
7 |
8 | class PredicateFactoryTest(TestCase):
9 | def setUp(self):
10 | self.mox = mox.Mox()
11 | self.comp_name = "Test Predicate Or"
12 |
13 | self.predat = SimplePredicate("a", ThreadSafeObject({}))
14 | self.predat.set_met(True)
15 | self.predbt = SimplePredicate("b", ThreadSafeObject({}))
16 | self.predbt.set_met(True)
17 |
18 | self.predaf = SimplePredicate("a", ThreadSafeObject({}))
19 | self.predbf = SimplePredicate("b", ThreadSafeObject({}))
20 |
21 | self.list = [self.predaf, self.predbf, self.predat, self.predbt]
22 |
23 | self.factory = PredicateFactory(component_name="factory", zkclient=None,
24 | proc_client=None, system=None,
25 | pred_list=self.list, settings={})
26 |
27 | def tearDown(self):
28 | pass
29 |
30 | def test_match(self):
31 | new = SimplePredicate("a", ThreadSafeObject({}))
32 | ret = self.factory._ensure_new(new)
33 | self.assertTrue(new is not ret)
34 |
35 | def test_no_match(self):
36 | new = SimplePredicate("c", ThreadSafeObject({}))
37 | ret = self.factory._ensure_new(new)
38 | self.assertTrue(new is ret)
39 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/reload_cache_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import tornado.web
3 | from httplib import INTERNAL_SERVER_ERROR
4 |
5 | from zoom.common.decorators import TimeThis
6 |
7 |
8 | class ReloadCacheHandler(tornado.web.RequestHandler):
9 | @property
10 | def data_store(self):
11 | """
12 | :rtype: zoom.www.cache.data_store.DataStore
13 | """
14 | return self.application.data_store
15 |
16 | @TimeThis(__file__)
17 | def post(self):
18 | """
19 | @api {post} /api/v1/cache/reload/ Reload data from Zookeeper
20 | @apiParam {String} user The user that submitted the task
21 | @apiParam {String} command Can be anything...currently only used for logging
22 | @apiVersion 1.0.0
23 | @apiName Reload
24 | @apiGroup Cache
25 | """
26 | try:
27 | user = self.get_argument("user")
28 | command = self.get_argument("command")
29 |
30 | logging.info("Received reload cache command for target '{0}' from "
31 | "user {1}:{2}"
32 | .format(command, user, self.request.remote_ip))
33 | logging.info("Clearing and reloading all server side caches")
34 | self.data_store.reload()
35 | self.write('Cache Reloaded')
36 | self.set_header('Content-Type', 'text/html')
37 | except Exception as e:
38 | self.set_status(INTERNAL_SERVER_ERROR)
39 | self.write({'errorText': str(e)})
40 | logging.exception(e)
41 |
--------------------------------------------------------------------------------
/server/zoom/agent/config/sentinel_windows_config.xml_SAMPLE:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/predicate/process_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 | import time
3 |
4 | from unittest import TestCase
5 | from multiprocessing import Lock
6 | from zoom.agent.predicate.process import PredicateProcess
7 | from zoom.agent.client.process_client import ProcessClient
8 |
9 |
10 | class PredicateProcessTest(TestCase):
11 | def setUp(self):
12 | self.mox = mox.Mox()
13 | self.interval = 0.1
14 | self.proc_client = self.mox.CreateMock(ProcessClient)
15 | self.proc_client.process_client_lock = Lock()
16 | self.proc_client.cancel_flag = False
17 |
18 | def tearDown(self):
19 | self.mox.UnsetStubs()
20 |
21 | def test_start(self):
22 |
23 | pred = PredicateProcess("/path", self.proc_client, interval=self.interval)
24 | self.mox.StubOutWithMock(pred, "running")
25 | pred.running().MultipleTimes().AndReturn(True)
26 |
27 | self.mox.ReplayAll()
28 |
29 | pred.start()
30 | pred.start() # should noop
31 | pred.start() # should noop
32 | time.sleep(0.25) # give other thread time to check
33 | pred.stop()
34 |
35 | self.mox.VerifyAll()
36 |
37 | def test_stop(self):
38 |
39 | pred = PredicateProcess("/path", self.proc_client, interval=self.interval)
40 | self.mox.StubOutWithMock(pred, "running")
41 | pred.running().MultipleTimes().AndReturn(True)
42 |
43 | self.mox.ReplayAll()
44 |
45 | pred.start()
46 | time.sleep(0.25) # give other thread time to check
47 | pred.stop()
48 | pred.stop()
49 | pred.stop()
50 |
51 | self.mox.VerifyAll()
52 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/delete_path_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import tornado.web
3 |
4 | from zoom.common.decorators import TimeThis
5 |
6 |
7 | class DeletePathHandler(tornado.web.RequestHandler):
8 | @property
9 | def zk(self):
10 | """
11 | :rtype: kazoo.client.KazooClient
12 | """
13 | return self.application.zk
14 |
15 | @property
16 | def app_state_path(self):
17 | """
18 | :rtype: str
19 | """
20 | return self.application.configuration.application_state_path
21 |
22 | @TimeThis(__file__)
23 | def post(self):
24 | """
25 | @api {post} /api/v1/delete/ Delete path in Zookeeper
26 | @apiParam {String} login_user The user that submitted the task
27 | @apiParam {String} delete The Zookeeper path to delete
28 | @apiVersion 1.0.0
29 | @apiName DeletePath
30 | @apiGroup DeletePath
31 | """
32 | login_name = self.get_argument("loginName")
33 | path = self.get_argument("delete")
34 | split_path = path.split('/')
35 | scounter = len(split_path)
36 | while scounter != 0:
37 | new_path = '/'.join(split_path[0:scounter])
38 | if new_path == self.app_state_path:
39 | break
40 | elif not self.zk.get_children(new_path):
41 | self.zk.delete(new_path)
42 | logging.info("Delete initiated by user {0} for path {1}"
43 | .format(login_name, new_path))
44 | else:
45 | break
46 | scounter -= 1
47 |
--------------------------------------------------------------------------------
/server/zoom/agent/check/findstring:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Script to find string in a file.
4 | # Required:
5 | # -f FILENAME file to search
6 | # -s STRING string to find
7 | #
8 | # Optional:
9 | # -k whether to exit with 1 if the string is found
10 | #
11 | USAGE="USAGE: $0 -f FILENAME -s STRING [-k] "
12 | KILL=false
13 |
14 | # Parse Parameters
15 | while getopts ":f:s:k" param
16 | do
17 | case "$param" in
18 | "f")
19 | FILENAME=$OPTARG;
20 | /bin/echo "FILENAME=$FILENAME";
21 | ;;
22 | "s")
23 | STRING="$OPTARG";
24 | /bin/echo "STRING='$STRING'";
25 | ;;
26 | "k")
27 | KILL=true;
28 | /bin/echo "KILL=True";
29 | ;;
30 | esac
31 | done;
32 |
33 | # Make sure parameters are set/file exists.
34 | if [ -z "$FILENAME" ]
35 | then
36 | /bin/echo ${USAGE}
37 | /bin/echo "FILENAME parameter not set. Exiting with 2." 1>&2;
38 | exit 2;
39 | elif [ -z "$STRING" ]
40 | then
41 | /bin/echo ${USAGE}
42 | /bin/echo "STRING parameter not set. Exiting with 2." 1>&2;
43 | exit 2;
44 | elif [ ! -f ${FILENAME} ];
45 | then
46 | /bin/echo "FILENAME $FILENAME does not exist or is not a file. Exiting with 1." 1>&2;
47 | exit 1;
48 | fi;
49 |
50 | # evaluate exit code to use if KILL is set
51 | if ${KILL};
52 | then
53 | EXIT=1;
54 | else
55 | EXIT=0
56 | fi;
57 |
58 | # Check file for string:
59 | RESULT=$(/bin/grep "$STRING" ${FILENAME})
60 | if [ -n "$RESULT" ];
61 | then
62 | /bin/echo "Found string \"$STRING\".";
63 | /bin/echo "String: \"$RESULT\".";
64 | /bin/echo "Exiting with $EXIT."
65 | exit ${EXIT}
66 | else
67 | /bin/echo "Did not find string. Exiting with $EXIT.";
68 | exit ${EXIT}
69 | fi;
--------------------------------------------------------------------------------
/server/zoom/www/handlers/time_estimate_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import tornado.web
3 | import os.path
4 |
5 | from httplib import INTERNAL_SERVER_ERROR
6 | from zoom.common.decorators import TimeThis
7 |
8 |
9 | class TimeEstimateHandler(tornado.web.RequestHandler):
10 | @property
11 | def data_store(self):
12 | """
13 | :rtype: zoom.www.cache.data_store.DataStore
14 | """
15 | return self.application.data_store
16 |
17 | @property
18 | def app_state_path(self):
19 | """
20 | :rtype: str
21 | """
22 | return self.application.configuration.application_state_path
23 |
24 | @TimeThis(__file__)
25 | def get(self, path):
26 | """
27 | @api {get} /api/v1/timingestimate[/:path] Get an estimate on when all apps will be up
28 | @apiVersion 1.0.0
29 | @apiName GetEstimate
30 | @apiGroup Estimate
31 | """
32 | try:
33 | logging.info('Retrieving Timing Estimate')
34 | if path:
35 | if not path.startswith(self.app_state_path):
36 | # be able to search by comp id, not full path
37 | path = os.path.join(self.app_state_path, path[1:])
38 |
39 | self.write(self.data_store.get_start_time(path))
40 | else:
41 | self.write(self.data_store.load_time_estimate_cache().to_json())
42 |
43 | except Exception as e:
44 | self.set_status(INTERNAL_SERVER_ERROR)
45 | self.write({'errorText': str(e)})
46 | logging.exception(e)
47 |
48 | self.set_header('Content-Type', 'application/json')
49 |
--------------------------------------------------------------------------------
/server/zoom/www/cache/global_cache.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from zoom.common.decorators import connected_with_return
4 | from zoom.www.messages.global_mode_message import GlobalModeMessage
5 |
6 |
7 | class GlobalCache(object):
8 | def __init__(self, configuration, zoo_keeper, web_socket_clients):
9 | """
10 | :type configuration: zoom.www.config.configuration.Configuration
11 | :type zoo_keeper: kazoo.client.KazooClient
12 | :type web_socket_clients: list
13 | """
14 | self._configuration = configuration
15 | self._zoo_keeper = zoo_keeper
16 | self._web_socket_clients = web_socket_clients
17 |
18 | def start(self):
19 | pass
20 |
21 | def stop(self):
22 | pass
23 |
24 | @connected_with_return(GlobalModeMessage('{"mode":"Unknown"}'))
25 | def get_mode(self):
26 | data, stat = self._zoo_keeper.get(
27 | self._configuration.global_mode_path, watch=self.on_update)
28 |
29 | logging.info("Global Mode retrieved from ZooKeeper {0}"
30 | .format(self._configuration.global_mode_path))
31 | return GlobalModeMessage(data)
32 |
33 | def on_update(self, event=None):
34 | """
35 | :type event: kazoo.protocol.states.WatchedEvent or None
36 | """
37 | try:
38 | message = self.get_mode()
39 | logging.debug('Sending update: {0}'.format(message.to_json()))
40 |
41 | for client in self._web_socket_clients:
42 | client.write_message(message.to_json())
43 |
44 | except Exception:
45 | logging.exception('An unhandled Exception has occurred')
46 |
--------------------------------------------------------------------------------
/server/zoom/www/messages/application_dependencies.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 |
4 | from zoom.common.types import UpdateType
5 |
6 |
7 | class ApplicationDependenciesMessage(object):
8 | def __init__(self):
9 | self._message_type = UpdateType.APPLICATION_DEPENDENCY_UPDATE
10 | self._operation_type = None
11 | self._application_dependencies = dict()
12 |
13 | @property
14 | def message_type(self):
15 | return self._message_type
16 |
17 | @property
18 | def application_dependencies(self):
19 | return self._application_dependencies
20 |
21 | def update(self, item):
22 | """
23 | :type item: dict
24 | """
25 | self._application_dependencies.update(item)
26 |
27 | def combine(self, message):
28 | """
29 | :type message: ApplicationDependenciesMessage
30 | """
31 | self._application_dependencies.update(message.application_dependencies)
32 |
33 | def remove(self, item):
34 | """
35 | :type item: dict
36 | """
37 | for key in item.keys():
38 | try:
39 | logging.debug('Removing from cache: {0}'.format(key))
40 | del self._application_dependencies[key]
41 | except KeyError:
42 | continue
43 |
44 | def clear(self):
45 | self._application_dependencies.clear()
46 |
47 | def to_json(self):
48 | return json.dumps({
49 | "update_type": self._message_type,
50 | "application_dependencies": self._application_dependencies.values()
51 | })
52 |
53 | def __len__(self):
54 | return len(self._application_dependencies)
55 |
--------------------------------------------------------------------------------
/client/model/appInfoModel.js:
--------------------------------------------------------------------------------
1 | define(['jquery', 'knockout' ], function($, ko) {
2 | return function AppInfoModel(configPath, login) {
3 | // Application info box
4 | var self = this;
5 |
6 | self.data = ko.observable('');
7 | self.showInfo = ko.observable(false);
8 | self.maxLength = 120;
9 |
10 | self.toggle = function() {
11 | self.showInfo(!self.showInfo());
12 | };
13 |
14 | self.save = function() {
15 | self.data(document.getElementsByName(configPath)[0].textContent);
16 | if (self.data().length > 120) {
17 | swal('Text too long.', 'The maximum comment length is 120 characters. It will not be saved until it is shorter.', 'error');
18 | return;
19 | }
20 | var dict = {
21 | loginName: login.elements.username(),
22 | configurationPath: configPath,
23 | serviceInfo: self.data()
24 | };
25 |
26 | $.post('/api/v1/serviceinfo/', dict).fail(function(data) {
27 | swal('Error Posting ServiceInfo.', JSON.stringify(data), 'error');
28 | });
29 | };
30 |
31 | self.getInfo = ko.computed(function() {
32 | if (self.showInfo()) {
33 | var dict = {configurationPath: configPath};
34 | if (self.showInfo()) {
35 | $.getJSON('/api/v1/serviceinfo/', dict, function(data) {
36 | self.data(data.servicedata);
37 | }).fail(function(data) {
38 | swal('Failed GET for ServiceInfo.', JSON.stringify(data), 'error');
39 | });
40 | }
41 | }
42 | });
43 |
44 | };
45 | });
46 |
--------------------------------------------------------------------------------
/client/model/environmentModel.js:
--------------------------------------------------------------------------------
1 | define(['knockout', 'service' ], function(ko, service) {
2 | var environment = {};
3 | environment.env = ko.observable('Unknown');
4 |
5 | var envColor = {
6 | staging: '#FFDA47',
7 | stagText: '#000000',
8 | production: '#E64016',
9 | prodText: '#FFFFFF',
10 | unknown: '#FF33CC'
11 | };
12 |
13 | environment.envType = {
14 | stg: 'staging',
15 | uat: 'uat',
16 | prod: 'production'
17 | };
18 |
19 | environment.envColor = ko.computed(function() {
20 | switch (environment.env().toLowerCase()) {
21 | case environment.envType.stg:
22 | return envColor.staging;
23 | case environment.envType.uat:
24 | return envColor.uat;
25 | case environment.envType.prod:
26 | return envColor.production;
27 | default:
28 | return envColor.unknown;
29 | }
30 | });
31 |
32 | environment.envTextColor = ko.computed(function() {
33 | switch (environment.env().toLowerCase()) {
34 | case environment.envType.prod:
35 | return envColor.prodText;
36 | default:
37 | return envColor.stagText;
38 | }
39 | });
40 |
41 | var onSuccess = function(data) {
42 | environment.env(data.environment);
43 | var stylename = data.environment.toLowerCase().concat('_style');
44 | document.getElementById(stylename).removeAttribute('disabled');
45 | };
46 |
47 | var onFailure = function(data) {
48 | swal('Well shoot...', 'There was an error getting environment', 'error');
49 | };
50 |
51 | service.get('api/v1/environment/', onSuccess, onFailure);
52 |
53 | return environment;
54 | });
55 |
--------------------------------------------------------------------------------
/client/main.js:
--------------------------------------------------------------------------------
1 | requirejs.config({
2 | baseUrl: 'front-end',
3 | paths: {
4 | 'text': './libs/text',
5 | 'durandal': './libs/durandal',
6 | 'plugins' : './libs/durandal/plugins',
7 | 'transitions': './libs/durandal/transitions',
8 | 'knockout': './libs/knockout-3.2.0',
9 | 'bootstrap': './libs/bootstrap-3.2.0.min',
10 | 'jquery': './libs/jquery-2.1.1.min',
11 | 'jq-throttle': './libs/jquery.ba-throttle-debounce.min',
12 | 'jq-mousewh': './libs/jquery.mousewheel.min',
13 | 'd3': './libs/d3.min',
14 | 'vkbeautify': './libs/vkbeautify.0.99.00.beta',
15 | 'sweet-alert': './libs/sweet-alert.min',
16 | 'jsonlint': './libs/jsonlint'
17 | },
18 | shim: {
19 | 'jq-mousewh': {
20 | deps: ['jquery']
21 | },
22 | 'jq-throttle': {
23 | deps: ['jquery']
24 | },
25 | 'bootstrap': {
26 | deps: ['jquery'],
27 | exports: 'jQuery'
28 | }
29 | }
30 | });
31 |
32 | define(['durandal/system', 'durandal/app', 'durandal/viewLocator'], function(system, app, viewLocator) {
33 | // >>excludeStart("build", true);
34 | system.debug(true);
35 | // >>excludeEnd("build");
36 |
37 | app.title = 'Zoom';
38 |
39 | app.configurePlugins({
40 | router: true,
41 | dialog: true
42 | });
43 |
44 | app.start().then(function() {
45 | // Replace 'viewmodels' in the moduleId with 'views' to locate the view.
46 | // Look for partial views in a 'views' folder in the root.
47 | viewLocator.useConvention();
48 |
49 | // Show the app by setting the root view model for our application with a transition.
50 | app.setRoot('viewmodels/navbar');
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/test/cache/global_cache_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 |
3 | from unittest import TestCase
4 | from kazoo.client import KazooClient
5 | from zoom.www.cache.global_cache import GlobalCache
6 | from test.test_utils import ConfigurationMock, EventMock, FakeMessage
7 |
8 |
9 | class GlobalCacheTest(TestCase):
10 |
11 | def setUp(self):
12 | self.mox = mox.Mox()
13 | self.socket_client1 = self.mox.CreateMockAnything()
14 | self.socket_client2 = self.mox.CreateMockAnything()
15 |
16 | self.web_socket_clients = [self.socket_client1, self.socket_client2]
17 | self.configuration = ConfigurationMock
18 | self.zoo_keeper = self.mox.CreateMock(KazooClient)
19 |
20 | def tearDown(self):
21 | self.mox.UnsetStubs()
22 |
23 | def test_construct(self):
24 | self.mox.ReplayAll()
25 | self._create_global_cache()
26 | self.mox.VerifyAll()
27 |
28 | def test_get_mode(self):
29 | self.configuration.global_mode_path = "mode/path"
30 | cache = self._create_global_cache()
31 |
32 | self.zoo_keeper.connected = True
33 | self.zoo_keeper.get("mode/path",
34 | watch=mox.IgnoreArg()).AndReturn((None, None))
35 | self.mox.ReplayAll()
36 | cache.get_mode()
37 | self.mox.VerifyAll()
38 |
39 | def test_on_update(self):
40 | event = EventMock()
41 | cache = self._create_global_cache()
42 | self.socket_client1.write_message("globalmodejson")
43 | self.socket_client2.write_message("globalmodejson")
44 |
45 | self.mox.StubOutWithMock(cache, "get_mode")
46 | cache.get_mode().AndReturn(FakeMessage("globalmodejson"))
47 |
48 | self.mox.ReplayAll()
49 | cache.on_update(event)
50 | self.mox.VerifyAll()
51 |
52 | def _create_global_cache(self):
53 | return GlobalCache(self.configuration, self.zoo_keeper,
54 | self.web_socket_clients)
--------------------------------------------------------------------------------
/scripts/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # allow for where we're setting the virtual environment
4 | if [[ $# -eq 0 ]] ;
5 | then
6 | echo 'No arguments detected. Setting default VENV_DIR.'
7 | VENV_DIR=/opt/spot/zoom/venv
8 | else
9 | VENV_DIR=$1
10 | fi
11 | echo "VENV_DIR=$VENV_DIR"
12 |
13 | WEB_SERVER=http://spotpypi01.spottrading.com/pypi
14 | PY_3RDPARTY=${WEB_SERVER}/3rdparty/python
15 |
16 | # if exists, delete virtual environment
17 | if [ -d ${VENV_DIR} ]; then
18 | rm -rf ${VENV_DIR} || exit 1
19 | fi
20 |
21 | /opt/python-2.7.3/bin/virtualenv ${VENV_DIR} || exit 1
22 |
23 | source ${VENV_DIR}/bin/activate || exit 1
24 |
25 | if [ -f /usr/bin/lsb_release ]; then
26 | linux_version=$(lsb_release -a | awk '/^Release/ {print $NF}')
27 | else
28 | linux_version=$(awk 'NR==1{print $(NF-1)}' /etc/issue)
29 | fi
30 |
31 | function install_package () {
32 | # $1 = package name
33 | FULLPATH=${PY_3RDPARTY}/$1
34 | /bin/echo -n "Installing ${FULLPATH}...";
35 | easy_install ${FULLPATH} > /dev/null 2>&1 || exit 2;
36 | /bin/echo "Done";
37 | }
38 |
39 | for PACKAGE in tornado-3.1.1.tar.gz \
40 | six-1.9.0.tar.gz \
41 | kazoo-2.2.1.tar.gz \
42 | setproctitle-1.1.8.tar.gz \
43 | requests-2.2.1.tar.gz \
44 | nose-1.3.0.tar.gz \
45 | mox-0.5.3.tar.gz \
46 | coverage-3.6.tar.gz \
47 | psutil-1.2.1.tar.gz \
48 | zope.interface-4.0.5.tar.gz \
49 | pygerduty-0.23-py2.7.egg
50 |
51 | do
52 | install_package ${PACKAGE};
53 | done
54 |
55 | # these packages do not install correctly on CentOS 5.x machines
56 | if [ $(echo "${linux_version:0:3} >= 6" | bc) -eq 1 ]; then
57 | echo
58 | echo 'Linux version equal or greater than 6. Installing additional packages.'
59 |
60 | for PACKAGE in python-ldap-2.4.10.tar.gz \
61 | pyodbc-3.0.6-py2.7-linux-x86_64.egg
62 |
63 | do
64 | install_package ${PACKAGE};
65 | done
66 |
67 | fi
68 |
--------------------------------------------------------------------------------
/server/zoom/common/constants.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | __env_connections = {
5 | "local": "localhost:2181",
6 | "Staging": ('ZooStaging01:2181,'
7 | 'ZooStaging02:2181,'
8 | 'ZooStaging03:2181,'
9 | 'ZooStaging04:2181,'
10 | 'ZooStaging05:2181'),
11 | "QA": ('ZooStaging01:2181,'
12 | 'ZooStaging02:2181,'
13 | 'ZooStaging03:2181,'
14 | 'ZooStaging04:2181,'
15 | 'ZooStaging05:2181'),
16 | # "UAT": ('ZooUat01:2181,'
17 | # 'ZooUat01:2181,'
18 | # 'ZooUat01:2181,'
19 | # 'ZooUat01:2181,'
20 | # 'ZooUat01:2181'),
21 | "UAT": ('ZooProduction01:2181,' # UAT servers will route to Production ZK
22 | 'ZooProduction02:2181,'
23 | 'ZooProduction03:2181,'
24 | 'ZooProduction04:2181,'
25 | 'ZooProduction05:2181'),
26 | "Production": ('ZooProduction01:2181,'
27 | 'ZooProduction02:2181,'
28 | 'ZooProduction03:2181,'
29 | 'ZooProduction04:2181,'
30 | 'ZooProduction05:2181')
31 | }
32 |
33 |
34 | ZK_AGENT_CONFIG = '/spot/software/config/application/sentinel'
35 | ZOOM_CONFIG = '/spot/software/config/application/zoom'
36 |
37 |
38 | def get_zk_conn_string(env=None):
39 | default = os.environ.get('EnvironmentToUse', 'Staging')
40 | if env and env in __env_connections:
41 | return __env_connections.get(env)
42 | else:
43 | return __env_connections.get(default)
44 |
45 | # This is a dictionary of available methods in Sentinel and their
46 | # runtime priorities. For example, we always want to run stop before start.
47 | SENTINEL_METHODS = {
48 | "stop": 1,
49 | "unregister": 2,
50 | "notify": 3,
51 | "start": 4,
52 | "register": 5,
53 | "restart": 6,
54 | "dep_restart": 99,
55 | "ignore": 99,
56 | "react": 99,
57 | "terminate": 99,
58 | "start_if_ready": 99,
59 | "cancel": 99,
60 | "status": 99
61 | }
62 |
--------------------------------------------------------------------------------
/scripts/win_install.py:
--------------------------------------------------------------------------------
1 | import os
2 | import psutil
3 | import time
4 | import win32event
5 | import win32evtlogutil
6 | import win32service
7 | import win32serviceutil
8 |
9 |
10 | class PythonService(win32serviceutil.ServiceFramework):
11 | _svc_name_ = "sentinel"
12 | _svc_display_name_ = "sentinel"
13 | _svc_deps_ = ["EventLog"]
14 | _proc = None
15 |
16 | def __init__(self, args):
17 | win32serviceutil.ServiceFramework.__init__(self, args)
18 | self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
19 |
20 | def SvcStop(self):
21 | if self._proc:
22 | self._proc.terminate()
23 |
24 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
25 | win32event.SetEvent(self.hWaitStop)
26 |
27 | def SvcDoRun(self):
28 | import servicemanager
29 | # Write a 'started' event to the event log...
30 | win32evtlogutil.ReportEvent(self._svc_name_,
31 | servicemanager.PYS_SERVICE_STARTED,
32 | 0, # category
33 | servicemanager.EVENTLOG_INFORMATION_TYPE,
34 | (self._svc_name_, ''))
35 | self.main()
36 |
37 | # and write a 'stopped' event to the event log.
38 | win32evtlogutil.ReportEvent(self._svc_name_,
39 | servicemanager.PYS_SERVICE_STOPPED,
40 | 0, # category
41 | servicemanager.EVENTLOG_INFORMATION_TYPE,
42 | (self._svc_name_, ''))
43 |
44 | def main(self):
45 | os.chdir(r"C:\Program Files\Spot Trading LLC\zoom\server")
46 | self._proc = psutil.Popen(r"C:\Python27\python.exe sentinel.py")
47 | try:
48 | while self._proc.status == psutil.STATUS_RUNNING:
49 | time.sleep(1)
50 | except psutil.NoSuchProcess:
51 | pass
52 |
53 |
54 | if __name__ == '__main__':
55 | win32serviceutil.HandleCommandLine(PythonService)
56 |
--------------------------------------------------------------------------------
/server/zoom/common/types.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class AlertActionType():
4 | TRIGGER = 'trigger'
5 | RESOLVE = 'resolve'
6 |
7 | class AlertReason():
8 | CRASHED = 'crashed'
9 | FAILEDTOSTART = 'failed to start'
10 | RESOLVED = 'resolved'
11 |
12 | class ApplicationType():
13 | JOB = "job"
14 | APPLICATION = "application"
15 |
16 |
17 | class ApplicationState():
18 | OK = "ok"
19 | ERROR = "error"
20 | CONFIG_ERROR = "config_error"
21 | STAGGERED = "staggered"
22 | STARTING = "starting"
23 | STARTED = "started"
24 | STOPPING = "stopping"
25 | STOPPED = "stopped"
26 | NOTIFY = "notify"
27 |
28 |
29 | class ApplicationStatus():
30 | RUNNING = 1
31 | STARTING = 2
32 | STOPPED = 3
33 | CANCELLED = -2
34 | CRASHED = -99
35 |
36 |
37 | class CommandType():
38 | CANCEL = "cancel"
39 |
40 |
41 | class JobState():
42 | RUNNING = 'running'
43 | SUCCESS = 'success'
44 | FAILURE = 'failure'
45 |
46 |
47 | class OperationType():
48 | ADD = 'add'
49 | REMOVE = 'remove'
50 |
51 |
52 | class PlatformType():
53 | UNKNOWN = -1
54 | LINUX = 0
55 | WINDOWS = 1
56 |
57 |
58 | class PredicateType():
59 | AND = "and"
60 | OR = "or"
61 | NOT = "not"
62 | API = 'api'
63 | HEALTH = "health"
64 | HOLIDAY = "holiday"
65 | PROCESS = "process"
66 | TIMEWINDOW = "timewindow"
67 | WEEKEND = "weekend"
68 | ZOOKEEPERNODEEXISTS = "zookeepernodeexists"
69 | ZOOKEEPERHASCHILDREN = "zookeeperhaschildren"
70 | ZOOKEEPERHASGRANDCHILDREN = "zookeeperhasgrandchildren"
71 | ZOOKEEPERGOODUNTILTIME = "zookeepergooduntiltime"
72 | ZOOKEEPERGLOB = "zookeeperglob"
73 |
74 |
75 | class UpdateType():
76 | APPLICATION_STATE_UPDATE = "application_state"
77 | APPLICATION_DEPENDENCY_UPDATE = "application_dependency"
78 | GLOBAL_MODE_UPDATE = "global_mode"
79 | TIMING_UPDATE = "timing_estimate"
80 |
81 |
82 | class Weekdays():
83 | MONDAY = 0
84 | TUESDAY = 1
85 | WEDNESDAY = 2
86 | THURSDAY = 3
87 | FRIDAY = 4
88 | SATURDAY = 5
89 | SUNDAY = 6
90 |
--------------------------------------------------------------------------------
/test/entities/test_application_state.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from zoom.www.entities.application_state import ApplicationState
3 |
4 |
5 | class TestApplicationState(TestCase):
6 | def setUp(self):
7 | self.state = ApplicationState(application_name="1",
8 | configuration_path="2",
9 | application_status="3",
10 | application_host=None,
11 | last_update=1388556000,
12 | start_stop_time="6",
13 | error_state="7",
14 | delete="8",
15 | local_mode="9",
16 | login_user="10",
17 | last_command="12",
18 | pd_disabled=False,
19 | grayed=True,
20 | read_only=True,
21 | load_times=1,
22 | restart_count=0,
23 | platform=0)
24 |
25 | def test_to_dictionary(self):
26 | self.assertEquals(
27 | {
28 | 'application_name': "1",
29 | 'configuration_path': "2",
30 | 'application_status': "unknown",
31 | 'application_host': "",
32 | 'last_update': '2014-01-01 00:00:00',
33 | 'start_stop_time': "6",
34 | 'error_state': "7",
35 | 'delete': "8",
36 | 'local_mode': "9",
37 | 'login_user': "10",
38 | 'last_command': "12",
39 | 'pd_disabled': False,
40 | 'grayed': True,
41 | 'read_only': True,
42 | 'load_times': 1,
43 | 'restart_count': 0,
44 | 'platform': 0
45 | },
46 | self.state.to_dictionary()
47 | )
48 |
--------------------------------------------------------------------------------
/server/zoom/www/messages/message_throttler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | from multiprocessing import Lock
4 | from threading import Thread
5 |
6 |
7 | class MessageThrottle(object):
8 | """
9 | Send at most throttle_interval message per second to zoom clients
10 | """
11 | def __init__(self, configuration, clients):
12 | self._interval = configuration.throttle_interval
13 | self._lock = Lock()
14 | self._clients = clients
15 | self._message = None
16 | self._thread = Thread(target=self._run,
17 | name='message_throttler')
18 | self._running = True
19 |
20 | def start(self):
21 | self._thread.start()
22 |
23 | def add_message(self, message):
24 | self._lock.acquire()
25 | try:
26 | if self._message is None:
27 | self._message = message
28 | else:
29 | self._message.combine(message)
30 | finally:
31 | self._lock.release()
32 |
33 | def _run(self):
34 |
35 | while self._running:
36 | self._lock.acquire()
37 | try:
38 | if self._message is not None:
39 | logging.debug('Sending message: {0}'
40 | .format(self._message.to_json()))
41 | for client in self._clients:
42 | try:
43 | client.write_message(self._message.to_json())
44 | except IndexError:
45 | logging.debug('Client closed when trying to send '
46 | 'update.')
47 | continue
48 | self._message = None
49 | except AttributeError as e:
50 | logging.exception('Exception in MessageThrottle: {0}'.format(e))
51 | finally:
52 | self._lock.release()
53 |
54 | time.sleep(float(self._interval))
55 |
56 | def stop(self):
57 | if self._thread.is_alive():
58 | self._running = False
59 | self._thread.join()
60 |
--------------------------------------------------------------------------------
/test/cache/data_store_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 |
3 | from unittest import TestCase
4 | from kazoo.client import KazooClient
5 | from zoom.www.entities.task_server import TaskServer
6 | from zoom.www.cache.data_store import DataStore
7 | from zoom.www.cache.global_cache import GlobalCache
8 | from zoom.www.cache.application_state_cache import ApplicationStateCache
9 | from test.test_utils import ConfigurationMock
10 |
11 |
12 | class DataStoreTest(TestCase):
13 |
14 | def setUp(self):
15 | self.mox = mox.Mox()
16 | self.configuration = ConfigurationMock()
17 |
18 | self.zoo_keeper = self.mox.CreateMock(KazooClient)
19 | self.zoo_keeper.connected = True
20 |
21 | self.task_server = self.mox.CreateMock(TaskServer)
22 |
23 | def tearDown(self):
24 | self.mox.UnsetStubs()
25 |
26 | def test_construct(self):
27 | self.mox.ReplayAll()
28 | self._create_datastore()
29 | self.mox.VerifyAll()
30 |
31 | def test_clients_empty(self):
32 | client = self.mox.CreateMockAnything()
33 | self.mox.ReplayAll()
34 | store = self._create_datastore()
35 | self.assertEquals(store.web_socket_clients, [])
36 | store._web_socket_clients.append(client)
37 | self.assertEquals(store.web_socket_clients, [client])
38 | self.mox.VerifyAll()
39 |
40 | def test_global(self):
41 | global_cache = self.mox.CreateMock(GlobalCache)
42 | global_cache.get_mode()
43 | self.mox.ReplayAll()
44 | store = self._create_datastore()
45 | store._global_cache = global_cache
46 | store.get_global_mode()
47 | self.mox.VerifyAll()
48 |
49 | def test_app_state_load(self):
50 | application_state_cache = self.mox.CreateMock(ApplicationStateCache)
51 | application_state_cache.load()
52 | self.mox.ReplayAll()
53 | store = self._create_datastore()
54 | store._application_state_cache = application_state_cache
55 | store.load_application_state_cache()
56 | self.mox.VerifyAll()
57 |
58 | def _create_datastore(self):
59 | return DataStore(self.configuration, self.zoo_keeper, self.task_server)
--------------------------------------------------------------------------------
/client/styles/app/spot.css:
--------------------------------------------------------------------------------
1 | body{background-color: #F7F7F6;}
2 |
3 |
4 | .center {
5 | float: none;
6 | margin-left: auto;
7 | margin-right: auto;
8 | }
9 |
10 | table{width: 100%;}
11 |
12 | .table tbody>tr>td.vert-align
13 | {
14 | vertical-align: bottom;
15 | }
16 |
17 | .splash {
18 | text-align: center;
19 | }
20 |
21 | .splash .message {
22 | font-size: 5em;
23 | line-height: 1.5em;
24 | /*text-transform: uppercase;*/
25 | }
26 |
27 | .splash .fa-spinner {
28 | text-align: center;
29 | display: inline-block;
30 | font-size: 5em;
31 | margin-top: 50px;
32 | }
33 |
34 | .navbar-nav li.loader {
35 | margin: 12px 6px 0 6px;
36 | visibility: hidden;
37 | }
38 |
39 | .navbar-nav li.loader.active {
40 | visibility: visible;
41 | }
42 |
43 | .caret-reversed {
44 | border-right: 4px solid transparent;
45 | border-left: 4px solid transparent;
46 | border-bottom: 4px solid;
47 | display: inline-block;
48 | height: 0;
49 | vertical-align: middle;
50 | width: 0;
51 | }
52 |
53 | .caret-left {
54 | border-right: 4px solid;
55 | border-top: 4px solid transparent;
56 | border-bottom: 4px solid transparent;
57 | display: inline-block;
58 | height: 0;
59 | vertical-align: middle;
60 | width: 0;
61 | }
62 |
63 | input:required:invalid {
64 | background-color: #d9534f;
65 | }
66 |
67 | /* This is for form placeholders */
68 | input:required:invalid::-webkit-input-placeholder {
69 | color: #fff;
70 | }
71 | *::-webkit-input-placeholder {
72 | font-size: 12px;
73 | }
74 |
75 | /* Overrides bootstrap to properly display modal
76 | * header with a background color
77 | */
78 | .modal-header {
79 | padding:9px 15px;
80 | border-bottom:1px solid #eee;
81 | background-color: #0480be;
82 | -webkit-border-top-left-radius: 5px;
83 | -webkit-border-top-right-radius: 5px;
84 | -moz-border-radius-topleft: 5px;
85 | -moz-border-radius-topright: 5px;
86 | border-top-left-radius: 5px;
87 | border-top-right-radius: 5px;
88 | }
89 |
90 | .envbanner {
91 | text-align: center;
92 | font-size: 16px;
93 | font-weight: bold;
94 | }
95 |
96 | .navbar {
97 | border: 0px;
98 | }
99 |
--------------------------------------------------------------------------------
/server/zoom/common/handlers.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from tornado.web import RequestHandler
3 |
4 | from zoom.agent.util.helpers import create_temporary_znode
5 |
6 | class LogVerbosityHandler(RequestHandler):
7 | def post(self, level):
8 | """
9 | @api {get} /loglevel/:level Set logging level for log file
10 | @apiDescription Available parameters are debug, info, and warning.
11 | Everything else will be dropped.
12 | @apiVersion 1.0.0
13 | @apiName SetLogLevel
14 | @apiGroup Common
15 | """
16 | logger = logging.getLogger('')
17 | level = level.lower()
18 | if level == 'debug':
19 | logger.setLevel(logging.DEBUG)
20 | elif level == 'info':
21 | logger.setLevel(logging.INFO)
22 | elif level == 'warning':
23 | logger.setLevel(logging.WARNING)
24 | else:
25 | return
26 |
27 | msg = 'Changed log level to {0}'.format(level)
28 | logging.info(msg)
29 | self.write(msg)
30 |
31 |
32 | class RUOKHandler(RequestHandler):
33 | def get(self):
34 | """
35 | @api {get} /ruok Return whether the web server is available
36 | @apiDescription This is currently used by the init script so that it
37 | will block until the web server is available and verify sentinel is
38 | connected to ZooKeeper.
39 | @apiVersion 1.0.0
40 | @apiName WebAvailable
41 | @apiGroup Common
42 | """
43 | # Verify connectivity
44 | _ret = create_temporary_znode(self.application.zk, self.application.temp_dir,
45 | self.application.hostname)
46 | if _ret:
47 | self.write('ok')
48 | else:
49 | self.set_status(503)
50 | self.write('bad_state')
51 |
52 | class VersionHandler(RequestHandler):
53 | def get(self):
54 | """
55 | @api {get} /version Return the version of the software
56 | @apiDescription Return the running version of the software
57 | @apiVersion 1.0.0
58 | @apiName Version
59 | @apiGroup Common
60 | """
61 | self.write(self.application.version)
62 |
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/zknode_exists.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from zoom.agent.predicate.simple import SimplePredicate
4 | from zoom.common.decorators import connected
5 |
6 |
7 | class ZookeeperNodeExists(SimplePredicate):
8 | def __init__(self, comp_name, zkclient, nodepath,
9 | operational=False, parent=None):
10 | """
11 | :type comp_name: str
12 | :type zkclient: kazoo.client.KazooClient
13 | :type nodepath: str
14 | :type operational: bool
15 | :type parent: str or None
16 | """
17 | SimplePredicate.__init__(self, comp_name, operational=operational, parent=parent)
18 | self.node = nodepath
19 | self.zkclient = zkclient
20 | self._log = logging.getLogger('sent.{0}.pred.ne'.format(comp_name))
21 | self._log.info('Registered {0}'.format(self))
22 |
23 | def start(self):
24 | if not self._started:
25 | self._log.debug('Starting {0}'.format(self))
26 | self._started = True
27 | self._watch_node()
28 | else:
29 | self._log.debug('Already started {0}'.format(self))
30 |
31 | @connected
32 | def _watch_node(self, event=None):
33 | """
34 | :type event: kazoo.protocol.states.WatchedEvent or None
35 | """
36 | exists = self.zkclient.exists(self.node, watch=self._watch_node)
37 | self.set_met(bool(exists))
38 |
39 | def __repr__(self):
40 | return ('{0}(component={1}, parent={2}, zkpath={3}, started={4}, '
41 | 'operational={5}, met={6})'
42 | .format(self.__class__.__name__,
43 | self._comp_name,
44 | self._parent,
45 | self.node,
46 | self.started,
47 | self._operational,
48 | self.met))
49 |
50 | def __eq__(self, other):
51 | return all([
52 | type(self) == type(other),
53 | self.node == getattr(other, 'node', None)
54 | ])
55 |
56 | def __ne__(self, other):
57 | return any([
58 | type(self) != type(other),
59 | self.node != getattr(other, 'node', None)
60 | ])
61 |
--------------------------------------------------------------------------------
/client/classes/dependency-maps/ClusterTree.js:
--------------------------------------------------------------------------------
1 | function ClusterTree(d3, ko, parent, divName) {
2 | var self = this;
3 | self.parent = parent;
4 |
5 | self.name = 'cluster-tree';
6 |
7 | self.width = 7000;
8 | self.height = 20000;
9 |
10 | self.cluster = d3.layout.cluster()
11 | .size([self.height, self.width - 160]);
12 |
13 | self.svg = d3.select('#d3-view-area').append('svg')
14 | .attr('width', self.width)
15 | .attr('height', self.height)
16 | .attr('id', self.name)
17 | .attr('display', 'none')
18 | .append('g')
19 | .attr('transform', 'translate(40,0)');
20 |
21 | self.visible = ko.observable(false);
22 |
23 | self.hide = function() {
24 | d3.select('#' + self.name).attr('display', 'none');
25 | self.visible(false);
26 | };
27 |
28 | self.show = function() {
29 | d3.select('#' + self.name).attr('display', 'inline');
30 | self.visible(true);
31 | };
32 |
33 | d3.json('test.json', function(json) {
34 | var nodes = self.cluster.nodes(json);
35 |
36 | var link = self.svg.selectAll('path.link')
37 | .data(self.cluster.links(nodes))
38 | .enter().append('path')
39 | .attr('class', 'link')
40 | .attr('d', self.elbow);
41 |
42 | var node = self.svg.selectAll('.node')
43 | .data(nodes)
44 | .enter().append('g')
45 | .attr('class', 'node')
46 | .attr('transform', function(d) {
47 | return 'translate(' + d.y + ',' + d.x + ')';
48 | });
49 |
50 | node.append('circle')
51 | .attr('r', 4.5);
52 |
53 | node.append('text')
54 | .attr('dx', function(d) {
55 | return d.children ? -8 : 8;
56 | })
57 | .attr('dy', 3)
58 | .style('text-anchor', function(d) {
59 | return d.children ? 'end' : 'start';
60 | })
61 | .text(function(d) {
62 | return d.name;
63 | });
64 |
65 | });
66 |
67 | self.elbow = function(d, i) {
68 | return 'M' + d.source.y + ',' + d.source.x + 'V' + d.target.x + 'H' + d.target.y;
69 | };
70 | }
--------------------------------------------------------------------------------
/client/model/loginModel.js:
--------------------------------------------------------------------------------
1 | define(['knockout', 'service', 'jquery' ], function(ko, service, $) {
2 |
3 | var login = {};
4 |
5 | login.elements = {
6 | username: ko.observable(''),
7 | password: ko.observable(''),
8 | showError: ko.observable(false),
9 | error: ko.observable(''),
10 | readWrite: ko.observable(false),
11 | authenticated: ko.observable(false)
12 | };
13 |
14 | login.advertise = ko.computed(function() {
15 | if (login.elements.authenticated()) {
16 | return login.elements.username();
17 | }
18 | else {
19 | return 'Sign In';
20 | }
21 | });
22 |
23 | login.setUserFromCookie = function() {
24 | login.elements.username(service.getCookie('username'));
25 | if (service.getCookie('read_write')) {
26 | login.elements.readWrite(true);
27 | }
28 |
29 | if (login.elements.username() && login.elements.readWrite()) {
30 | login.elements.authenticated(true);
31 | }
32 | };
33 |
34 | login.onSuccess = function(data) {
35 | login.setUserFromCookie();
36 | login.hide();
37 | };
38 |
39 | login.onFailure = function(data) {
40 | // expecting data in the form:
41 | // {"method": "POST", "type": "login", "code": 500, "data": null, "error": null}
42 | if (login.elements.password() !== '') {
43 | var pw = $('#password');
44 | pw.attr('data-content', data.error);
45 | pw.popover('show');
46 | }
47 | };
48 |
49 | login.submit = function() {
50 |
51 | var params = {
52 | username: login.elements.username(),
53 | password: login.elements.password()
54 | };
55 |
56 | return service.post('login', params, login.onSuccess, login.onFailure);
57 |
58 | };
59 |
60 | login.reset = function() {
61 | login.elements.username('');
62 | login.elements.password('');
63 | login.elements.authenticated(false);
64 | login.hide();
65 | login.submit();
66 | };
67 |
68 | login.hide = function() {
69 | $('#password').popover('destroy');
70 | $('#loginDropDown').click();
71 | };
72 |
73 | login.setUserFromCookie();
74 |
75 | return login;
76 | });
77 |
--------------------------------------------------------------------------------
/server/zoom/www/messages/application_states.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 |
4 | from zoom.common.types import UpdateType
5 |
6 |
7 | class ApplicationStatesMessage(object):
8 | def __init__(self):
9 | self._message_type = UpdateType.APPLICATION_STATE_UPDATE
10 | self._application_states = dict()
11 | self._environment = None
12 |
13 | @property
14 | def message_type(self):
15 | return self._message_type
16 |
17 | @property
18 | def environment(self):
19 | return self._environment
20 |
21 | def set_environment(self, env):
22 | self._environment = env
23 |
24 | @property
25 | def application_states(self):
26 | return self._application_states
27 |
28 | def update(self, item):
29 | """
30 | :type item: dict
31 | """
32 | self._application_states.update(item)
33 |
34 | def combine(self, message):
35 | """
36 | :type message: ApplicationStatesMessage
37 | """
38 | self._application_states.update(message.application_states)
39 |
40 | def remove(self, item):
41 | """
42 | :type item: dict
43 | """
44 | for key in item.keys():
45 | try:
46 | logging.debug('Removing from cache: {0}'.format(key))
47 | del self._application_states[key]
48 | except KeyError:
49 | continue
50 |
51 | def clear(self):
52 | self._application_states.clear()
53 |
54 | def to_json(self):
55 | _dict = {}
56 | _dict.update({
57 | "update_type": self._message_type,
58 | "application_states": self._application_states.values()
59 | })
60 | if self.environment is not None:
61 | _dict.update({"environment": self._environment})
62 | return json.dumps(_dict)
63 |
64 | def remove_deletes(self):
65 | dels = []
66 | for key, value in self.application_states.iteritems():
67 | if value['delete']:
68 | dels.append(key)
69 | for key in dels:
70 | self.application_states.pop(key)
71 |
72 | def __len__(self):
73 | return len(self._application_states)
74 |
75 | def __eq__(self, other):
76 | return self._application_states == other
77 |
78 | def __ne__(self, other):
79 | return self._application_states != other
80 |
--------------------------------------------------------------------------------
/server/zoom/agent/check/logtick:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Check designed to test whether a file has been modified within a specified
5 | amount of time.
6 |
7 | EXAMPLE:
8 | # python logtick.py -f /tmp/test.log -i 10
9 | # python logtick.py --file /tmp/test.log --interval 3m
10 | """
11 |
12 | import os
13 | import re
14 | import sys
15 | import datetime
16 | from argparse import ArgumentParser
17 |
18 |
19 | _UNIT_DICT = {
20 | 's': 1,
21 | 'm': 60,
22 | 'h': 60 * 60,
23 | 'd': 60 * 60 * 24,
24 | 'w': 60 * 60 * 24 * 7,
25 | }
26 |
27 |
28 | def get_timedelta(string):
29 | lstring = string.lower()
30 | match = re.search('(\d+)(\w+)?', lstring)
31 | if match:
32 | value = int(match.group(1))
33 | multiplier = _UNIT_DICT.get(match.group(2), 1)
34 |
35 | seconds = value * multiplier
36 | return datetime.timedelta(seconds=seconds)
37 |
38 |
39 | if __name__ == "__main__":
40 |
41 | parser = ArgumentParser(description='Check whether a file had been modified'
42 | ' in some amount of time.')
43 | parser.add_argument('-f', '--file', required=True,
44 | help='Path to the file to check.')
45 | parser.add_argument('-i', '--interval', required=True,
46 | help=('Time within which the file should have been '
47 | 'updated. The scipt understands parameters '
48 | '(s)econd, (m)inute (h)our, (d)ay, (w)eek in the '
49 | 'form 1d, 2w, etc. Unless specified, it will '
50 | 'assume seconds.'))
51 |
52 | args = parser.parse_args()
53 |
54 | if os.path.exists(args.file):
55 | now = datetime.datetime.now()
56 | check_time = get_timedelta(args.interval)
57 | stats = os.stat(args.file)
58 | dt_mtime = datetime.datetime.fromtimestamp(stats.st_mtime)
59 |
60 | if now - dt_mtime > check_time:
61 | print ('The file {0} has not been modified within the configured '
62 | 'interval time {1}. Exiting with 1.'.format(args.file,
63 | args.interval))
64 | sys.exit(1)
65 | else:
66 | sys.exit(0)
67 |
68 | else:
69 | print ('The file {0} does not exist. Exiting with 1.'.format(args.file))
70 | sys.exit(1)
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/pred_not.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from zoom.agent.predicate.simple import SimplePredicate
3 |
4 |
5 | class PredicateNot(SimplePredicate):
6 | def __init__(self, comp_name, pred, parent=None):
7 | """
8 | :type comp_name: str
9 | :type pred: zoom.agent.entities.dependency object
10 | :type parent: str or None
11 | """
12 | SimplePredicate.__init__(self, comp_name, parent=parent)
13 | self.dependency = pred
14 | self._log = logging.getLogger('sent.{0}.pred.not'.format(comp_name))
15 | self._log.info('Registered {0}'.format(self))
16 |
17 | @property
18 | def met(self):
19 | return not self.dependency.met
20 |
21 | @property
22 | def operationally_relevant(self):
23 | return self.dependency.operationally_relevant and self.dependency.met
24 |
25 | @property
26 | def started(self):
27 | return all([self._started, self.dependency.started])
28 |
29 | def start(self):
30 | if self._started is False:
31 | self._log.debug('Starting {0}'.format(self))
32 | self._started = True
33 | self.dependency.start()
34 | else:
35 | self._log.debug('Already started {0}'.format(self))
36 |
37 | def stop(self):
38 | if self._started is True:
39 | self._log.debug('Stoping {0}'.format(self))
40 | self._started = False
41 | self.dependency.stop()
42 | else:
43 | self._log.debug('Already stopped {0}'.format(self))
44 |
45 | def __repr__(self):
46 | indent_count = len(self._parent.split('/'))
47 | indent = '\n' + ' ' * indent_count
48 | return ('{0}(component={1}, parent={2}, started={3}, met={4}, '
49 | 'predicate={5}{6})'
50 | .format(self.__class__.__name__,
51 | self._comp_name,
52 | self._parent,
53 | self.started,
54 | self.met,
55 | indent,
56 | self.dependency))
57 |
58 | def __eq__(self, other):
59 | return all([
60 | type(self) == type(other),
61 | self.dependency == getattr(other, 'dependency', None)
62 | ])
63 |
64 | def __ne__(self, other):
65 | return any([
66 | type(self) != type(other),
67 | self.dependency != getattr(other, 'dependency', None)
68 | ])
69 |
--------------------------------------------------------------------------------
/client/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Zoom
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Zoom
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/test/messages/app_dep_test.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from zoom.www.messages.application_dependencies import ApplicationDependenciesMessage
3 |
4 |
5 | class AppDependencyMessageTest(TestCase):
6 |
7 | def test_update(self):
8 | path = '/foo'
9 | mes = ApplicationDependenciesMessage()
10 | d1 = {"configuration_path": path, "dependencies": list()}
11 |
12 | # test dict gets update
13 | data = {path: d1}
14 | mes.update(data)
15 | self.assertEqual(mes.application_dependencies, data)
16 |
17 | # test data change for same key
18 | d2 = d1.copy()
19 | d2['dependencies'] = [1, 2, 3]
20 | data = {path: d2}
21 | mes.update(data)
22 |
23 | self.assertEqual(mes.application_dependencies.get(path), d2)
24 |
25 | def test_combine(self):
26 | """
27 | Test that two ApplicationDependenciesMessage can be combined into 1
28 | """
29 | path1 = '/foo'
30 | mes1 = self._create_dep_message(path1)
31 |
32 | path2 = '/bar'
33 | mes2 = self._create_dep_message(path2)
34 |
35 | expected = {
36 | path2: {"configuration_path": path2, "dependencies": list()},
37 | path1: {"configuration_path": path1, "dependencies": list()}
38 | }
39 | mes1.combine(mes2)
40 |
41 | self.assertEqual(mes1.application_dependencies, expected)
42 |
43 | def test_remove(self):
44 | path1 = '/foo'
45 | path2 = '/bar'
46 | mes = self._create_dep_message(path1, path2)
47 |
48 | expected = {
49 | path2: {"configuration_path": path2, "dependencies": list()},
50 | }
51 | mes.remove({
52 | path1: {"configuration_path": path1, "dependencies": list()}
53 | })
54 |
55 | self.assertEqual(mes.application_dependencies, expected)
56 |
57 | def test_clear(self):
58 | mes = self._create_dep_message('/foo', '/bar')
59 | mes.clear()
60 | self.assertEqual(mes.application_dependencies, {})
61 |
62 | def test_len(self):
63 | mes = self._create_dep_message('/foo', '/bar')
64 | self.assertEqual(len(mes), 2)
65 |
66 | def _create_dep_message(self, *args):
67 | """
68 | :rtype: ApplicationDependenciesMessage
69 | """
70 | mes = ApplicationDependenciesMessage()
71 | for path in args:
72 | data = {
73 | path: {"configuration_path": path, "dependencies": list()}
74 | }
75 | mes.update(data)
76 |
77 | return mes
78 |
--------------------------------------------------------------------------------
/client/viewmodels/navbar.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'plugins/router',
4 | 'durandal/app',
5 | 'jquery',
6 | 'knockout',
7 | 'service',
8 | 'model/loginModel',
9 | 'model/adminModel',
10 | 'model/environmentModel',
11 | 'model/pillarModel',
12 | 'model/toolsModel',
13 | 'model/externalLinkModel',
14 | 'bootstrap'
15 | ],
16 | function(router, app, $, ko, service, login, admin, environment, pillar, tools, exlink) {
17 | var self = this;
18 | self.connection = {};
19 |
20 | // Create the websocket right away so we know if we lose connection to server on any page
21 | $(document).ready(function() {
22 | self.connection = new WebSocket('ws://' + document.location.host + '/zoom/ws');
23 |
24 | self.connection.onopen = function() {
25 | console.log('websocket connected');
26 | };
27 |
28 | self.connection.onclose = function(evt) {
29 | console.log('websocket closed');
30 | document.getElementById("applicationHost").style.backgroundColor = '#FF7BFE';
31 | swal('Uh oh...', 'You will need to refresh the page to receive updates.', 'error');
32 | };
33 | });
34 |
35 |
36 | return {
37 | router: router,
38 | login: login,
39 | admin: admin,
40 | environment: environment,
41 | pillar: pillar,
42 | tools: tools,
43 | exlink: exlink,
44 | connection: self.connection,
45 | isFAQ: function(title) {return title.search('FAQ') !== -1;},
46 | activate: function() {
47 | router.map([
48 | { route: '', title: 'Application State', moduleId: 'viewmodels/applicationState', nav: true },
49 | { route: 'config(/:server)', title: 'Sentinel Config', moduleId: 'viewmodels/sentinelConfig', nav: true, hash: '#config' },
50 | { route: 'pillar(/:server)', title: 'Pillar Config', moduleId: 'viewmodels/pillarConfig', nav: true, hash: '#pillar'},
51 | { route: 'appFAQ', title: 'App State FAQ', moduleId: 'viewmodels/faq/applicationState', nav: true }
52 | // { route: 'configFAQ', title: 'Sentinel Config FAQ', moduleId: 'viewmodels/faq/sentinelConfig', nav: true }
53 | ]).buildNavigationModel();
54 |
55 | return router.activate();
56 | }
57 | };
58 | });
59 |
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/pred_or.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from zoom.agent.predicate.simple import SimplePredicate
3 |
4 |
5 | class PredicateOr(SimplePredicate):
6 | def __init__(self, comp_name, predicates, parent=None):
7 | """
8 | :type comp_name: str
9 | :type predicates: list of zoom.agent.entities.predicate objects
10 | :type parent: str or None
11 | """
12 | SimplePredicate.__init__(self, comp_name, parent=parent)
13 | self.dependencies = predicates
14 | self._log = logging.getLogger('sent.{0}.pred.or'.format(comp_name))
15 | self._log.info('Registered {0}'.format(self))
16 | self._started = False
17 |
18 | @property
19 | def met(self):
20 | return any([d.met for d in self.dependencies])
21 |
22 | @property
23 | def operationally_relevant(self):
24 | return any([d.operationally_relevant for d in self.dependencies])
25 |
26 | @property
27 | def started(self):
28 | return all([
29 | self._started,
30 | all([d.started for d in self.dependencies])
31 | ])
32 |
33 | def start(self):
34 | if self._started is False:
35 | self._log.debug('Starting {0}'.format(self))
36 | self._started = True
37 | map(lambda x: x.start(), self.dependencies)
38 | else:
39 | self._log.debug('Already started {0}'.format(self))
40 |
41 | def stop(self):
42 | if self._started is True:
43 | self._log.debug('Stopping {0}'.format(self))
44 | self._started = False
45 | map(lambda x: x.stop(), self.dependencies)
46 | del self.dependencies[:]
47 | else:
48 | self._log.debug('Already stopped {0}'.format(self))
49 |
50 | def __repr__(self):
51 | return ('{0}(component={1}, parent={2}, started={3}, met={4}, '
52 | 'group=[\n\t{5})]'
53 | .format(self.__class__.__name__,
54 | self._comp_name,
55 | self._parent,
56 | self.started,
57 | self.met,
58 | '\n\t'.join([str(x) for x in self.dependencies])))
59 |
60 | def __eq__(self, other):
61 | return all([
62 | type(self) == type(other),
63 | self.dependencies == getattr(other, 'dependencies', None)
64 | ])
65 |
66 | def __ne__(self, other):
67 | return any([
68 | type(self) != type(other),
69 | self.dependencies != getattr(other, 'dependencies', None)
70 | ])
71 |
--------------------------------------------------------------------------------
/client/classes/dependency-maps/TreeMap.js:
--------------------------------------------------------------------------------
1 | function TreeMap(d3, ko, parent, divName) {
2 |
3 | var self = this;
4 | self.parent = parent;
5 |
6 | self.w = 1280 - 80;
7 | self.h = 800 - 180;
8 | self.x = d3.scale.linear().range([0, self.w]);
9 | self.y = d3.scale.linear().range([0, self.h]);
10 | self.color = d3.scale.category20c();
11 | self.root = null;
12 | self.node = null;
13 |
14 | self.name = 'tree-map';
15 |
16 | self.partition = d3.layout.partition()
17 | .children(function(d) {
18 | return isNaN(d.value) ? d3.entries(d.value) : null;
19 | })
20 | .value(function(d) {
21 | return d.value;
22 | });
23 |
24 | self.svg = d3.select('body').append('svg')
25 | .attr('width', self.width)
26 | .attr('height', self.height);
27 |
28 | self.rect = self.svg.selectAll('rect');
29 |
30 | self.visible = ko.observable(false);
31 |
32 | self.show = function() {
33 | self.rect.data(self.partition(d3.entries(self.parent.dependents())[0]))
34 | .enter().append('rect')
35 | .attr('x', function(d) {
36 | return self.x(d.x);
37 | })
38 | .attr('y', function(d) {
39 | return self.y(d.y);
40 | })
41 | .attr('width', function(d) {
42 | return self.x(d.dx);
43 | })
44 | .attr('height', function(d) {
45 | return self.y(d.dy);
46 | })
47 | .attr('fill', function(d) {
48 | return self.color((d.children ? d : d.parent).key);
49 | })
50 | .on('click', self.clicked);
51 | };
52 |
53 | self.hide = function() {
54 | d3.select('#' + self.name).style('display', 'none');
55 | self.visible(false);
56 | };
57 |
58 |
59 | self.size = function(d) {
60 | return d.size;
61 | };
62 |
63 | self.clicked = function(d) {
64 | self.x.domain([d.x, d.x + d.dx]);
65 | self.y.domain([d.y, 1]).range([d.y ? 20 : 0, self.height]);
66 |
67 | self.rect.transition()
68 | .duration(750)
69 | .attr('x', function(d) {
70 | return self.x(d.x);
71 | })
72 | .attr('y', function(d) {
73 | return self.y(d.y);
74 | })
75 | .attr('width', function(d) {
76 | return self.x(d.x + d.dx) - self.x(d.x);
77 | })
78 | .attr('height', function(d) {
79 | return self.y(d.y + d.dy) - self.y(d.y);
80 | });
81 | };
82 | }
--------------------------------------------------------------------------------
/client/libs/jquery.mousewheel.min.js:
--------------------------------------------------------------------------------
1 | /*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
2 | * Licensed under the MIT License (LICENSE.txt).
3 | *
4 | * Version: 3.1.12
5 | *
6 | * Requires: jQuery 1.2.2+
7 | */
8 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})});
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/pred_and.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from zoom.agent.predicate.simple import SimplePredicate
3 |
4 |
5 | class PredicateAnd(SimplePredicate):
6 | def __init__(self, comp_name, predicates, parent=None):
7 | """
8 | :type comp_name: str
9 | :type predicates: list of zoom.agent.entities.predicate objects
10 | :type parent: str or None
11 | """
12 | SimplePredicate.__init__(self, comp_name, parent=parent)
13 | self.dependencies = predicates
14 | self._log = logging.getLogger('sent.{0}.pred.and'.format(comp_name))
15 | self._log.info('Registered {0}'.format(self))
16 | self._started = False
17 |
18 | @property
19 | def met(self):
20 | return all([d.met for d in self.dependencies])
21 |
22 | @property
23 | def operationally_relevant(self):
24 | return any([d.operationally_relevant for d in self.dependencies])
25 |
26 | @property
27 | def started(self):
28 | return all([
29 | self._started,
30 | all([d.started for d in self.dependencies])
31 | ])
32 |
33 | def start(self):
34 | if self._started is False:
35 | self._log.debug('Starting {0}'.format(self))
36 | self._started = True
37 | map(lambda x: x.start(), self.dependencies)
38 | self._block_until_started()
39 | else:
40 | self._log.debug('Already started {0}'.format(self))
41 |
42 | def stop(self):
43 | if self._started is True:
44 | self._log.debug('Stopping {0}'.format(self))
45 | self._started = False
46 | map(lambda x: x.stop(), self.dependencies)
47 | del self.dependencies[:]
48 | else:
49 | self._log.debug('Already stopped {0}'.format(self))
50 |
51 | def __repr__(self):
52 | indent_count = len(self._parent.split('/'))
53 | indent = '\n' + ' ' * indent_count
54 | return ('{0}(component={1}, parent={2}, started={3}, met={4}, '
55 | 'group=[{5}{6})]'
56 | .format(self.__class__.__name__,
57 | self._comp_name,
58 | self._parent,
59 | self.started,
60 | self.met,
61 | indent,
62 | indent.join([str(x) for x in self.dependencies])))
63 |
64 | def __eq__(self, other):
65 | return all([
66 | type(self) == type(other),
67 | self.dependencies == getattr(other, 'dependencies', None)
68 | ])
69 |
70 | def __ne__(self, other):
71 | return any([
72 | type(self) != type(other),
73 | self.dependencies != getattr(other, 'dependencies', None)
74 | ])
75 |
--------------------------------------------------------------------------------
/server/zoom/agent/task/zk_task_client.py:
--------------------------------------------------------------------------------
1 | from kazoo.exceptions import NoNodeError
2 |
3 | from zoom.agent.task.task import Task
4 | from zoom.agent.task.base_task_client import BaseTaskClient
5 | from zoom.common.types import ApplicationState, CommandType
6 | from zoom.common.decorators import connected
7 | from zoom.common.constants import SENTINEL_METHODS
8 |
9 |
10 | class ZKTaskClient(BaseTaskClient):
11 | def __init__(self, children, zkclient, path):
12 | """
13 | :type children: dict
14 | :type zkclient: kazoo.client.KazooClient
15 | :type path: str or None
16 | """
17 | BaseTaskClient.__init__(self, children)
18 | self.zkclient = zkclient
19 | if path is None:
20 | self._log.warning('Was given no path. This sentinel will not be '
21 | 'able to receive commands from Zoom.')
22 | return
23 |
24 | self._path = '/'.join([path, self._host])
25 | self.clear_task_queue()
26 | self.reset_watches()
27 |
28 | @connected
29 | def on_exist(self, event=None):
30 | try:
31 | if self.zkclient.exists(self._path, watch=self.on_exist):
32 | data, stat = self.zkclient.get(self._path)
33 | task = Task.from_json(data)
34 | self._log.info('Found work to do: {0}'.format(task))
35 | if task.result == ApplicationState.OK:
36 | self._log.debug('Task is already complete: {0}'.format(task))
37 | return # ignore tasks that are already done
38 |
39 | if task.name in SENTINEL_METHODS:
40 | if task.target is not None:
41 | self.send_work_single(task)
42 | else:
43 | self.send_work_all(task)
44 | self._log.info("Submitted task {0} for {1}"
45 | .format(task.name, task.target))
46 | else:
47 | err = 'Invalid work submitted: {0}'.format(task.name)
48 | self._log.warning(err)
49 |
50 | task.result = ApplicationState.OK
51 | self._log.info(task.to_json())
52 | self.zkclient.set(self._path, task.to_json())
53 |
54 | except NoNodeError:
55 | self._log.debug('No Node at {0}'.format(self._path))
56 |
57 | def reset_watches(self):
58 | self.on_exist()
59 |
60 | def clear_task_queue(self):
61 | self._log.info('Starting, so clearing task queue.')
62 | if self.zkclient.exists(self._path):
63 | data, stat = self.zkclient.get(self._path)
64 | task = Task.from_json(data)
65 | task.result = CommandType.CANCEL
66 | self.zkclient.set(self._path, task.to_json())
67 |
--------------------------------------------------------------------------------
/server/zoom/agent/entities/restart.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | class RestartLogic(object):
5 | """
6 | Determines if service restarts are allowed or not
7 | """
8 | def __init__(self, component, restart_max, count_callback=None):
9 | """
10 | :type component: str
11 | :type restart_max: int
12 | :type count_callback: types.FunctionType or None
13 | """
14 | self._log = logging.getLogger('sent.{0}.restart'.format(component))
15 | self._restart_max = restart_max
16 | self._agent_restarted = True
17 | self._callback = count_callback
18 | self.stay_down = False
19 | self.ran_stop = False
20 | self.crashed = False
21 | self.count = 0
22 |
23 | @property
24 | def restart_max_reached(self):
25 | """
26 | Determines if number of restarts reached the max restart count
27 | :rtype: bool
28 | """
29 | result = self.count >= self._restart_max
30 | if result:
31 | self._log.error('The restart max {0} has been reached. The '
32 | 'process will no longer try to start.'
33 | .format(self._restart_max))
34 | return result
35 |
36 | def set_stay_down(self, val):
37 | self._log.info('Setting stay_down to {0}.'.format(val))
38 | self.stay_down = bool(val)
39 | self._agent_restarted = False
40 |
41 | def set_ran_stop(self, val):
42 | self._log.info('Setting ran_stop to {0}.'.format(val))
43 | self.ran_stop = bool(val)
44 | self._agent_restarted = False
45 |
46 | def reset_count(self):
47 | self._log.debug('Resetting start count to 0.')
48 | self.count = 0
49 |
50 | def increment_count(self):
51 | self.count += 1
52 | if self._callback is not None:
53 | self._callback()
54 |
55 | def check_for_crash(self, running):
56 | """
57 | :type running: bool
58 | """
59 | self._log.debug(
60 | 'running={0}, crashed={1}, ran_stop={2}, staydown={3}, agent={4}'
61 | .format(running, self.crashed, self.ran_stop, self.stay_down,
62 | self._agent_restarted))
63 |
64 | # if it's running, we don't care that the agent just restarted
65 | if running:
66 | self._agent_restarted = False
67 | self.crashed = False
68 | return
69 |
70 | if not running \
71 | and not self.ran_stop \
72 | and not self.stay_down \
73 | and not self._agent_restarted:
74 |
75 | if not self.crashed: # only log on change
76 | self._log.warning('Crash detected!')
77 |
78 | self.crashed = True
79 |
80 | else:
81 | self.crashed = False
82 |
--------------------------------------------------------------------------------
/server/zoom/agent/task/base_task_client.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import socket
3 | import time
4 | from zoom.common.types import CommandType
5 |
6 |
7 | class BaseTaskClient(object):
8 | def __init__(self, children):
9 | """
10 | :type children: dict
11 | """
12 | self._log = logging.getLogger('sent.task_client')
13 | self._children = children
14 | self._host = socket.getfqdn()
15 |
16 | def send_work_all(self, task, wait=False, immediate=False):
17 | """
18 | Send work to all children.
19 | :type task: zoom.agent.task.Task
20 | :type wait: bool
21 | :param wait: Whether to wait for the function to finish before exiting
22 | :type immediate: bool
23 | :param immediate: Whether to put the task at the head of the queue
24 |
25 | :rtype: dict
26 | """
27 | result = dict()
28 | for child in self._children.keys():
29 | task.target = child
30 | result[child] = self.send_work_single(task,
31 | wait=wait,
32 | immediate=immediate)
33 |
34 | return result
35 |
36 | def send_work_single(self, task, wait=False, immediate=False, timeout=None):
37 | """
38 | Send work to targeted child.
39 | :type task: zoom.agent.task.task.Task
40 | :type wait: bool
41 | :param wait: Whether to wait for the function to finish before exiting
42 | :type immediate: bool
43 | :param immediate: Whether to put the task at the head of the queue
44 | :type timeout: int or None
45 | :rtype: dict
46 | """
47 | child = self._children.get(task.target, None)
48 | if child is None:
49 | self._log.warning('The targeted child "{0}" does not exists.'
50 | .format(task.target))
51 | return {'target': task.target, 'work': task.name,
52 | 'result': '404: Not Found'}
53 |
54 | else:
55 | process = child['process']
56 | if task.name == CommandType.CANCEL:
57 | process.cancel_current_task()
58 | return {'target': task.target, 'work': task.name,
59 | 'result': CommandType.CANCEL}
60 | else:
61 | process.add_work(task, immediate=immediate)
62 |
63 | wait_time = 0
64 | if wait:
65 | while task.result is None:
66 | time.sleep(1)
67 | if timeout is not None:
68 | wait_time += 1
69 | if wait_time > timeout:
70 | break
71 |
72 | return {'target': task.target, 'work': task.name,
73 | 'result': task.result}
74 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/disable_app_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import httplib
3 | import os.path
4 | from xml.etree import ElementTree
5 | from tornado.web import RequestHandler
6 |
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class DisableAppHandler(RequestHandler):
11 | @property
12 | def zk(self):
13 | """
14 | :rtype: kazoo.client.KazooClient
15 | """
16 | return self.application.zk
17 |
18 | @property
19 | def agent_config_path(self):
20 | """
21 | :rtype: str
22 | """
23 | return self.application.configuration.agent_configuration_path
24 |
25 | @TimeThis(__file__)
26 | def post(self):
27 | """
28 | @api {post} /api/v1/disable Enable/Disable Zoom startup for an application
29 | @apiParam {String} host The user that submitted the task
30 | @apiParam {String} id The application id to disable/enable
31 | @apiParam {Boolean} Whether to disable Zoom startup
32 | @apiVersion 1.0.0
33 | @apiName DisableApp
34 | @apiGroup DisableApp
35 | """
36 | try:
37 | user = self.get_argument('user')
38 | component_id = self.get_argument("id")
39 | host = self.get_argument("host")
40 | disable = self.get_argument("disable")
41 | logging.info('User: {0}, App: {1}, Disable: {2}'
42 | .format(user, component_id, disable))
43 |
44 | path = os.path.join(self.agent_config_path, host)
45 | update = False
46 |
47 | if self.zk.exists(path):
48 | data, stat = self.zk.get(path)
49 | config = ElementTree.fromstring(data)
50 | for component in config.iter('Component'):
51 | cid = component.attrib.get('id')
52 | if cid == component_id:
53 | update = True
54 | for action in component.iter('Action'):
55 | aid = action.attrib.get('id')
56 | # we want the app to register/unregister/stop
57 | # even if it is disabled
58 | if aid not in ('register', 'unregister', 'stop'):
59 | action.attrib['disabled'] = disable
60 | logging.info('{0}abled {1}:{2}'.format(('Dis' if disable else 'En'), cid, aid))
61 |
62 | if update:
63 | self.zk.set(path, ElementTree.tostring(config))
64 |
65 | else:
66 | self.set_status(httplib.NOT_FOUND)
67 | self.write('Could not find {0} on host {1}'.format(component_id, host))
68 |
69 | except Exception as e:
70 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
71 | self.write(str(e))
72 | logging.exception(e)
73 |
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/weekend.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import datetime
3 | from time import sleep
4 | from threading import Thread
5 |
6 | from zoom.common.types import Weekdays
7 | from zoom.agent.predicate.simple import SimplePredicate
8 | from zoom.agent.entities.thread_safe_object import ThreadSafeObject
9 |
10 |
11 | class PredicateWeekend(SimplePredicate):
12 | def __init__(self, comp_name, operational=False, parent=None, interval=10):
13 | """
14 | :type comp_name: str
15 | :type operational: bool
16 | :type parent: str or None
17 | :type interval: int or float
18 | """
19 | SimplePredicate.__init__(self, comp_name, operational=operational, parent=parent)
20 | self.interval = interval
21 | self._log = logging.getLogger('sent.{0}.weekend'.format(comp_name))
22 | self._log.info('Registered {0}'.format(self))
23 |
24 | self._operate = ThreadSafeObject(True)
25 | self._thread = Thread(target=self._run_loop, name=str(self))
26 | self._thread.daemon = True
27 | self._started = False
28 |
29 | @property
30 | def weekday(self):
31 | """
32 | :rtype: int
33 | 0=Sunday, 1=Monday, etc.
34 | """
35 | return datetime.date.today().weekday()
36 |
37 | def start(self):
38 | if self._started is False:
39 | self._log.debug('Starting {0}'.format(self))
40 | self._started = True
41 | self._thread.start()
42 | self._block_until_started()
43 | else:
44 | self._log.debug('Already started {0}'.format(self))
45 |
46 | def stop(self):
47 | if self._started is True:
48 | self._log.info('Stopping {0}'.format(self))
49 | self._started = False
50 | self._operate.set_value(False)
51 | self._thread.join()
52 | self._log.info('{0} stopped'.format(self))
53 | else:
54 | self._log.debug('Already stopped {0}'.format(self))
55 |
56 | def _run_loop(self):
57 | while self._operate == True:
58 | self._process_met()
59 | sleep(self.interval)
60 | self._log.info('Done checking for weekend.')
61 |
62 | def _process_met(self):
63 | self.set_met(self.weekday in [Weekdays.SATURDAY, Weekdays.SUNDAY])
64 |
65 | def __repr__(self):
66 | return ('{0}(component={1}, parent={2}, started={3}, '
67 | 'operational={4}, met={5})'
68 | .format(self.__class__.__name__,
69 | self._comp_name,
70 | self._parent,
71 | self.started,
72 | self._operational,
73 | self._met))
74 |
75 | def __eq__(self, other):
76 | return type(self) == type(other)
77 |
78 | def __ne__(self, other):
79 | return type(self) != type(other)
80 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/service_info_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import tornado.web
3 |
4 | from httplib import INTERNAL_SERVER_ERROR
5 |
6 | from zoom.www.entities.database import Database
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class ServiceInfoHandler(tornado.web.RequestHandler):
11 | @property
12 | def configuration(self):
13 | """
14 | :rtype: zoom.www.config.configuration.Configuration
15 | """
16 | return self.application.configuration
17 |
18 | @TimeThis(__file__)
19 | def post(self):
20 | """
21 | Save service info
22 | @api {post} /api/v1/serviceinfo/ Set server notes
23 | @apiParam {String} loginName The user that submitted the task
24 | @apiParam {String} configurationPath A Zookeeper path corresponding with an Application
25 | @apiParam {String} serviceInfo The notes about an application
26 | @apiVersion 1.0.0
27 | @apiName SetNotes
28 | @apiGroup Server Notes
29 | """
30 | try:
31 | login_name = self.get_argument("loginName")
32 | configuration_path = self.get_argument("configurationPath")
33 | service_info = self.get_argument("serviceInfo")
34 |
35 | db = Database(self.configuration)
36 | query = db.save_service_info(login_name, configuration_path,
37 | service_info)
38 |
39 | if query:
40 | logging.info("User {0} saved service information for {1}"
41 | .format(login_name, configuration_path))
42 | self.write(query)
43 | else:
44 | logging.info("Error occurred while saving service info for {0} by "
45 | "user {1}".format(login_name, configuration_path))
46 | self.write("Error: Info for {0} could not be saved!"
47 | .format(configuration_path))
48 |
49 | except Exception as e:
50 | self.set_status(INTERNAL_SERVER_ERROR)
51 | self.write({'errorText': str(e)})
52 | logging.exception(e)
53 |
54 | @TimeThis(__file__)
55 | def get(self):
56 | """
57 | Get service info
58 | @api {get} /api/v1/serviceinfo/ Get server Notes
59 | @apiParam {String} login_user The user that submitted the task
60 | @apiVersion 1.0.0
61 | @apiName GetNotes
62 | @apiGroup Server Notes
63 | """
64 | try:
65 | configuration_path = self.get_argument("configurationPath")
66 |
67 | db = Database(self.configuration)
68 | query = db.fetch_service_info(configuration_path)
69 |
70 | self.write({'servicedata': query})
71 |
72 | except Exception as e:
73 | self.set_status(INTERNAL_SERVER_ERROR)
74 | self.write({'errorText': str(e)})
75 | logging.exception(e)
76 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/application_dependencies_handler.py:
--------------------------------------------------------------------------------
1 | import os.path
2 | import json
3 | import logging
4 | import httplib
5 | from tornado.web import RequestHandler
6 |
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class ApplicationDependenciesHandler(RequestHandler):
11 |
12 | @property
13 | def data_store(self):
14 | """
15 | :rtype: zoom.www.cache.data_store.DataStore
16 | """
17 | return self.application.data_store
18 |
19 | @property
20 | def app_state_path(self):
21 | """
22 | :rtype: str
23 | """
24 | return self.application.configuration.application_state_path
25 |
26 | @TimeThis(__file__)
27 | def get(self, path):
28 | """
29 | @api {get} /api/v1/application/dependencies/[:id] Get Application's dependencies
30 | @apiDescription Retrieve the upstream and downstream dependencies for an app.
31 | You can provide the full path in Zookeeper or the ComponentID.
32 | @apiVersion 1.0.0
33 | @apiName GetAppDep
34 | @apiGroup Dependency
35 | @apiSuccessExample {json} Success-Response:
36 | HTTP/1.1 200 OK
37 | {
38 | "configuration_path": "/spot/software/state/application/foo",
39 | "dependencies": [
40 | {
41 | "path": "/spot/software/state/application/bar",
42 | "type": "zookeeperhaschildren",
43 | "operational": true
44 | },
45 | {
46 | "path": "/spot/software/state/application/baz",
47 | "type": "zookeeperhasgrandchildren",
48 | "operational": false
49 | }
50 | ],
51 | "downstream": [
52 | "/spot/software/state/application/qux",
53 | "/spot/software/state/application/quux",
54 | ]
55 | }
56 | """
57 | logging.info('Retrieving Application Dependency Cache for client {0}'
58 | .format(self.request.remote_ip))
59 | try:
60 | result = self.data_store.load_application_dependency_cache()
61 | if path:
62 | if not path.startswith(self.app_state_path):
63 | # be able to search by comp id, not full path
64 | path = os.path.join(self.app_state_path, path[1:])
65 |
66 | item = result.application_dependencies.get(path, {})
67 | self.write(item)
68 | else:
69 | self.write(result.to_json())
70 |
71 | except Exception as e:
72 | logging.exception(e)
73 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
74 | self.write(json.dumps({'errorText': str(e)}))
75 |
76 | self.set_header('Content-Type', 'application/json')
77 | logging.info('Done Retrieving Application Depends Cache')
78 |
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/zkglob.py:
--------------------------------------------------------------------------------
1 | import fnmatch
2 | import logging
3 | import os.path
4 | from kazoo.exceptions import NoNodeError
5 | from zoom.common.decorators import connected, catch_exception
6 | from zoom.agent.predicate.zkhas_grandchildren import ZookeeperHasGrandChildren
7 | from zoom.agent.util.helpers import zk_path_join
8 |
9 |
10 | class ZookeeperGlob(ZookeeperHasGrandChildren):
11 | def __init__(self, comp_name, zkclient, nodepattern,
12 | ephemeral_only=True, operational=False, parent=None):
13 | """
14 | Predicate for watching Zookeeper nodes using unix-style glob matching.
15 |
16 | :type comp_name: str
17 | :type zkclient: kazoo.client.KazooClient
18 | :type nodepattern: str
19 | :type operational: bool
20 | :type parent: str or None
21 | """
22 | self.nodepattern = nodepattern
23 | self.node = self._get_deepest_non_glob_start(nodepattern)
24 | ZookeeperHasGrandChildren.__init__(self, comp_name, zkclient, self.node,
25 | ephemeral_only=ephemeral_only,
26 | operational=operational,
27 | parent=parent)
28 |
29 | self._log = logging.getLogger('sent.{0}.pred.glob'.format(comp_name))
30 | self._log.info('Registered {0}'.format(self))
31 |
32 | @catch_exception(NoNodeError, msg='A node has been removed during walk.')
33 | @connected
34 | def _walk(self, node, node_list):
35 | """
36 | Recursively walk a ZooKeeper path and add all children to the _children
37 | list as ZookeeperHasChildren objects.
38 | :type node: str
39 | """
40 | children = self.zkclient.get_children(node, watch=self._rewalk_tree)
41 | if children:
42 | for c in children:
43 | path = zk_path_join(node, c)
44 | self._walk(path, node_list)
45 | else:
46 | data, stat = self.zkclient.get(node)
47 | if stat.ephemeralOwner == 0: # not ephemeral
48 | if fnmatch.fnmatch(node, self.nodepattern):
49 | node_list.append(node)
50 | else:
51 | if fnmatch.fnmatch(os.path.dirname(node), self.nodepattern):
52 | node_list.append(os.path.dirname(node))
53 |
54 | def _get_deepest_non_glob_start(self, p):
55 | if "*" in p:
56 | s = p.split('*')
57 | return s[0].rstrip('/')
58 | else:
59 | return p
60 |
61 | def __repr__(self):
62 | indent_count = len(self._parent.split('/'))
63 | indent = '\n' + ' ' * indent_count
64 | return ('{0}(component={1}, parent={2}, glob_pattern={3}, started={4}, '
65 | 'ephemeral_only={5}, operational={6}, met={7}, group=[{8}{9}]))'
66 | .format(self.__class__.__name__,
67 | self._comp_name,
68 | self._parent,
69 | self.nodepattern,
70 | self.started,
71 | self._ephemeral_only,
72 | self._operational,
73 | self.met,
74 | indent,
75 | indent.join([str(x) for x in self._children])))
76 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/global_mode_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import httplib
4 | import tornado.web
5 | import tornado.httpclient
6 |
7 | from kazoo.exceptions import NoNodeError
8 |
9 | from zoom.agent.entities.thread_safe_object import ApplicationMode
10 | from zoom.common.decorators import TimeThis
11 |
12 |
13 | class GlobalModeHandler(tornado.web.RequestHandler):
14 | @property
15 | def zk(self):
16 | """
17 | :rtype: kazoo.client.KazooClient
18 | """
19 | return self.application.zk
20 |
21 | @property
22 | def global_mode_path(self):
23 | """
24 | :rtype: str
25 | """
26 | return self.application.configuration.global_mode_path
27 |
28 | @property
29 | def data_store(self):
30 | """
31 | :rtype: zoom.www.cache.data_store.DataStore
32 | """
33 | return self.application.data_store
34 |
35 | @TimeThis(__file__)
36 | def get(self):
37 | """
38 | @api {get} /api/v1/mode/ Get global mode
39 | @apiVersion 1.0.0
40 | @apiName GetMode
41 | @apiGroup Mode
42 | @apiSuccessExample {json} Success-Response:
43 | HTTP/1.1 200 OK
44 | {
45 | "operation_type": null,
46 | "global_mode": "{\"mode\":\"manual\"}",
47 | "update_type": "global_mode"
48 | }
49 | """
50 | try:
51 | message = self.data_store.get_global_mode()
52 |
53 | self.write(message.to_json())
54 |
55 | except Exception as e:
56 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
57 | self.write(json.dumps({'errorText': str(e)}))
58 | logging.exception(e)
59 |
60 | self.set_header('Content-Type', 'application/json')
61 |
62 | @TimeThis(__file__)
63 | def post(self):
64 | """
65 | @api {post} /api/v1/mode/ Set global Mode
66 | @apiParam {String} command What to set the mode to (auto|manual)
67 | @apiParam {String} user The user that submitted the task
68 | @apiVersion 1.0.0
69 | @apiName SetMode
70 | @apiGroup Mode
71 | """
72 | try:
73 | # parse JSON dictionary from POST
74 | command = self.get_argument("command")
75 | user = self.get_argument("user")
76 |
77 | logging.info("Received {0} config for Zookeeper from user {1}:{2}"
78 | .format(command, user, self.request.remote_ip))
79 |
80 | if command == ApplicationMode.MANUAL:
81 | self._update_mode(ApplicationMode.MANUAL)
82 | elif command == ApplicationMode.AUTO:
83 | self._update_mode(ApplicationMode.AUTO)
84 | else:
85 | logging.info("bad command")
86 |
87 | except NoNodeError:
88 | output = 'Could not find global mode node.'
89 | logging.error(output)
90 | self.write(output)
91 |
92 | def _update_mode(self, mode):
93 | logging.info('Updating Zookeeper global mode to {0}'.format(mode))
94 | data = {"mode": mode}
95 | self.zk.set(self.global_mode_path, json.dumps(data))
96 | self.write('Node successfully updated.')
97 | logging.info('Updated Zookeeper global mode to {0}'.format(mode))
98 |
--------------------------------------------------------------------------------
/test/predicate/pred_not_test.py:
--------------------------------------------------------------------------------
1 | import mox
2 | import unittest
3 |
4 | from zoom.agent.predicate.simple import SimplePredicate
5 | from zoom.agent.predicate.pred_not import PredicateNot
6 |
7 |
8 | class PredicateNotTest(unittest.TestCase):
9 | def setUp(self):
10 | self.mox = mox.Mox()
11 | self.comp_name = "Test Predicate Not"
12 |
13 | def tearDown(self):
14 | pass
15 |
16 | def testmet_true(self):
17 |
18 | pred = self._create_simple_pred(met=False)
19 |
20 | self.mox.ReplayAll()
21 |
22 | pred_not = self._create_pred_not(pred)
23 | self.assertTrue(pred_not.met)
24 |
25 | self.mox.VerifyAll()
26 |
27 | def testmet_false(self):
28 |
29 | pred = self._create_simple_pred(met=True)
30 |
31 | self.mox.ReplayAll()
32 |
33 | pred_not = self._create_pred_not(pred)
34 | self.assertFalse(pred_not.met)
35 |
36 | self.mox.VerifyAll()
37 |
38 | def test_start(self):
39 |
40 | pred = self._create_simple_pred(met=False)
41 | pred.start()
42 |
43 | self.mox.ReplayAll()
44 |
45 | pred_not = self._create_pred_not(pred)
46 |
47 | pred_not.start()
48 | pred_not.start() # should noop
49 |
50 | self.mox.VerifyAll()
51 |
52 | def test_no_stop(self):
53 |
54 | pred = self._create_simple_pred(met=False)
55 |
56 | self.mox.ReplayAll()
57 |
58 | pred_not = self._create_pred_not(pred)
59 |
60 | # test stop isn't called without starting
61 | pred_not.stop()
62 |
63 | self.mox.VerifyAll()
64 |
65 | def test_stop(self):
66 |
67 | pred = self._create_simple_pred(met=False)
68 |
69 | pred.start()
70 | pred.stop()
71 | pred.start()
72 |
73 | self.mox.ReplayAll()
74 |
75 | pred_not = self._create_pred_not(pred)
76 |
77 | pred_not.start()
78 | pred_not.stop()
79 | pred_not.stop()
80 | pred_not.start()
81 |
82 | self.mox.VerifyAll()
83 |
84 | def test_equal(self):
85 |
86 | predmet1 = self._create_simple_pred(met=True)
87 | predmet2 = self._create_simple_pred(met=True)
88 |
89 | self.mox.ReplayAll()
90 |
91 | pred1 = self._create_pred_not(predmet1)
92 | pred2 = self._create_pred_not(predmet2)
93 |
94 | self.assertTrue(pred1 == pred2)
95 |
96 | self.mox.VerifyAll()
97 |
98 | def test_not_equal(self):
99 |
100 | pred1 = self._create_simple_pred(met=False)
101 | pred2 = self._create_simple_pred(met=True)
102 |
103 | self.mox.ReplayAll()
104 |
105 | pred_and1 = self._create_pred_not(pred1)
106 | pred_and2 = self._create_pred_not(pred2)
107 |
108 | self.assertNotEqual(pred_and1, pred_and2)
109 |
110 | self.mox.VerifyAll()
111 |
112 | def _create_pred_not(self, predicate, parent='foo'):
113 | return PredicateNot(self.comp_name, predicate, parent=parent)
114 |
115 | def _create_simple_pred(self, cname=None, met=None):
116 | if cname is None:
117 | cname = self.comp_name
118 | s = SimplePredicate(cname, parent='foo')
119 | if met is not None:
120 | s.set_met(met)
121 |
122 | return s
--------------------------------------------------------------------------------
/server/zoom/agent/entities/work_manager.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import pprint
3 | import time
4 | from threading import Thread
5 |
6 | from zoom.agent.entities.thread_safe_object import ThreadSafeObject
7 |
8 |
9 | class ThreadWithReturn(Thread):
10 | def __init__(self, *args, **kwargs):
11 | super(ThreadWithReturn, self).__init__(*args, **kwargs)
12 |
13 | self._return = None
14 |
15 | def run(self):
16 | if self._Thread__target is not None:
17 | self._return = self._Thread__target(*self._Thread__args,
18 | **self._Thread__kwargs)
19 |
20 | def join(self, *args, **kwargs):
21 | super(ThreadWithReturn, self).join(*args, **kwargs)
22 |
23 | return self._return
24 |
25 |
26 | class WorkManager(object):
27 | def __init__(self, comp_name, queue, work_dict):
28 | """
29 | :type comp_name: str
30 | :type queue: zoom.agent.entities.unique_queue.UniqueQueue
31 | :type work_dict: dict
32 | """
33 | self._operate = ThreadSafeObject(True)
34 | self._thread = Thread(target=self._run,
35 | name='work_manager',
36 | args=(self._operate, queue, work_dict))
37 | self._thread.daemon = True
38 | self._log = logging.getLogger('sent.{0}.wm'.format(comp_name))
39 |
40 | def start(self):
41 | self._log.info('starting work manager')
42 | self._thread.start()
43 |
44 | def stop(self):
45 | self._log.info('Stopping work manager.')
46 | self._operate.set_value(False)
47 | self._thread.join()
48 | self._log.info('Stopped work manager.')
49 |
50 | def _run(self, operate, queue, work_dict):
51 | """
52 | :type operate: zoom.agent.entities.thread_safe_object.ThreadSafeObject
53 | :type queue: zoom.agent.entities.unique_queue.UniqueQueue
54 | :type work_dict: dict
55 | """
56 | while operate == True:
57 | if queue: # if queue is not empty
58 | self._log.info('Current Task Queue:\n{0}'
59 | .format(pprint.pformat(list(queue))))
60 | task = queue[0] # grab task, but keep it in the queue
61 |
62 | if task.func is None:
63 | func_to_run = work_dict.get(task.name, None)
64 | else:
65 | func_to_run = task.func
66 |
67 | if func_to_run is not None:
68 | self._log.info('Found work "{0}" in queue.'
69 | .format(task.name))
70 | t = ThreadWithReturn(target=func_to_run, name=task.name,
71 | args=task.args, kwargs=task.kwargs)
72 | t.start()
73 |
74 | if task.block:
75 | task.result = t.join()
76 |
77 | else:
78 | self._log.warning('Cannot do "{0}", it is not a valid '
79 | 'action.'.format(task.name))
80 | try:
81 | queue.remove(task)
82 | except ValueError:
83 | self._log.debug('Item no longer exists in the queue: {0}'
84 | .format(task))
85 | else:
86 | time.sleep(1)
87 |
88 | self._log.info('Done listening for work.')
89 | return
90 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/filters_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import tornado.web
4 |
5 | from httplib import INTERNAL_SERVER_ERROR
6 |
7 | from zoom.www.entities.database import Database
8 | from zoom.common.decorators import TimeThis
9 | from zoom.www.entities.custom_filter import CustomFilter
10 | from zoom.common.types import OperationType
11 |
12 |
13 | class FiltersHandler(tornado.web.RequestHandler):
14 | @property
15 | def configuration(self):
16 | """
17 | :rtype: zoom.www.config.configuration.Configuration
18 | """
19 | return self.application.configuration
20 |
21 | @TimeThis(__file__)
22 | def get(self):
23 | """
24 | @api {get} /api/v1/filters Retrieve filters for user
25 | @apiParam {String} loginName The user that submitted the task
26 | @apiVersion 1.0.0
27 | @apiName GetFilters
28 | @apiGroup Filters
29 | """
30 | try:
31 | login_name = self.get_argument("loginName")
32 |
33 | db = Database(self.configuration)
34 | filters = db.fetch_all_filters(login_name)
35 |
36 | arr = []
37 | for f in filters:
38 | arr.append(f.to_dictionary())
39 |
40 | self.write(json.dumps(arr))
41 | except Exception as e:
42 | self.set_status(INTERNAL_SERVER_ERROR)
43 | self.write({'errorText': str(e)})
44 | logging.exception(e)
45 |
46 | @TimeThis(__file__)
47 | def post(self):
48 | """
49 | @api {post} /api/v1/filters Save|Delete filter for user
50 | @apiParam {String} operation add|remove
51 | @apiParam {String} name The name of the filter
52 | @apiParam {String} loginName The user that submitted the task
53 | @apiParam {String} parameter The type of search filter
54 | @apiParam {String} searchTerm The search variable
55 | @apiParam {Boolean} inversed Whether to inverse the search
56 | @apiVersion 1.0.0
57 | @apiName ManageFilter
58 | @apiGroup Filters
59 | """
60 | try:
61 | operation = self.get_argument("operation")
62 | name = self.get_argument("name")
63 | login_name = self.get_argument("loginName")
64 | parameter = self.get_argument("parameter")
65 | search_term = self.get_argument("searchTerm")
66 | inversed = self.get_argument("inversed")
67 |
68 | f = CustomFilter(name, login_name, parameter, search_term, inversed)
69 |
70 | db = Database(self.configuration)
71 |
72 | if operation == OperationType.ADD:
73 | query = db.save_filter(f)
74 | elif operation == OperationType.REMOVE:
75 | query = db.delete_filter(f)
76 | else:
77 | query = None
78 |
79 | if query:
80 | logging.info("User {0} {1} filter {2}: success"
81 | .format(login_name, operation, name))
82 | self.write(query)
83 | else:
84 | output = ("Could not {0} filter '{1}' for user {2}"
85 | .format(operation, name, login_name))
86 | logging.warning(output)
87 | self.write(output)
88 |
89 | except Exception as e:
90 | self.set_status(INTERNAL_SERVER_ERROR)
91 | self.write({'errorText': str(e)})
92 | logging.exception(e)
93 |
--------------------------------------------------------------------------------
/server/zoom/agent/entities/job.py:
--------------------------------------------------------------------------------
1 | import json
2 | from time import sleep
3 | from datetime import datetime
4 |
5 | from kazoo.exceptions import (
6 | NodeExistsError,
7 | NoNodeError,
8 | SessionExpiredError
9 | )
10 |
11 | from zoom.common.types import JobState
12 | from zoom.agent.entities.application import Application
13 | from zoom.common.decorators import (
14 | connected,
15 | time_this,
16 | catch_exception
17 | )
18 |
19 |
20 | class Job(Application):
21 | def __init__(self, *args, **kwargs):
22 | Application.__init__(self, *args, **kwargs)
23 | self._paths['zk_state_path'] = \
24 | self._pathjoin(self._paths['zk_state_base'], 'gut')
25 |
26 | @time_this
27 | def start(self, **kwargs):
28 | """Start actual process"""
29 | if kwargs.get('reset', True):
30 | self._proc_client.reset_counters()
31 | if kwargs.get('pause', False):
32 | self.ignore()
33 |
34 | self.unregister()
35 | self._register_job_state(JobState.RUNNING)
36 |
37 | start_time = datetime.now()
38 | retcode = self._proc_client.start()
39 | stop_time = datetime.now()
40 |
41 | if retcode == 0:
42 | self.register(stop_time)
43 | self._register_job_state(JobState.SUCCESS,
44 | runtime=str(stop_time - start_time))
45 | else:
46 | self._register_job_state(JobState.FAILURE,
47 | runtime=str(stop_time - start_time))
48 |
49 | return 'OK'
50 |
51 | @catch_exception(NodeExistsError)
52 | @connected
53 | def register(self, now):
54 | """
55 | Add entry to the state tree
56 | :type now: datetime.datetime
57 | """
58 | stoptime = now.replace(hour=23, minute=59, second=59, microsecond=0)
59 | data = {'stop': str(stoptime)}
60 | self._log.info('Registering %s in state tree.' % self.name)
61 | self.zkclient.create(self._paths['zk_state_path'],
62 | value=json.dumps(data),
63 | makepath=True)
64 |
65 | @catch_exception(NoNodeError)
66 | @connected
67 | def unregister(self):
68 | """Remove entry from state tree"""
69 | self._log.info('Un-registering %s from state tree.' % self.name)
70 | self.zkclient.delete(self._paths['zk_state_path'])
71 |
72 | def run(self):
73 | self.zkclient.start()
74 | self._check_mode()
75 | self._log.info('Starting to process Actions.')
76 | map(lambda x: x.start(), self._actions.values()) # start actions
77 |
78 | while self._running:
79 | sleep(1)
80 |
81 | self.uninitialize()
82 |
83 | @catch_exception(SessionExpiredError)
84 | @connected
85 | def _register_job_state(self, state, runtime=''):
86 | """
87 | :type state: zoom.common.types.JobState
88 | :param runtime: str
89 | """
90 | data = {
91 | 'name': self.name,
92 | 'state': state,
93 | 'runtime': runtime
94 | }
95 |
96 | if not self.zkclient.exists(self._paths['zk_state_base']):
97 | self.zkclient.create(self._paths['zk_state_base'],
98 | value=json.dumps(data),
99 | makepath=True)
100 | else:
101 | self.zkclient.set(self._paths['zk_state_base'], json.dumps(data))
102 |
--------------------------------------------------------------------------------
/client/model/toolsModel.js:
--------------------------------------------------------------------------------
1 | define( [
2 | 'knockout',
3 | 'service',
4 | 'jquery',
5 | 'model/constants'
6 | ],
7 | function(ko, service, $, constants) {
8 | return function toolsModel(login) {
9 | var self = this;
10 | self.login = login;
11 | self.oldPath = ko.observable('');
12 | self.newPath = ko.observable(constants.zkPaths.appStatePath);
13 |
14 | self.setOldPath = function(path){
15 | self.oldPath(path)
16 | };
17 |
18 | self.setNewPath = function(path){
19 | self.newPath(path)
20 | };
21 |
22 | self.showPaths = function() {
23 | var paths_dict = {
24 | 'oldPath': self.oldPath(),
25 | 'newPath': self.newPath()
26 | };
27 | if (self.oldPath() === '' || self.newPath() === ''){
28 | swal('Please specify both paths!');
29 | }
30 | else if (self.oldPath() === self.newPath()){
31 | swal('Paths are the same! Not Replacing');
32 | }
33 | else{
34 | swal({
35 | title: 'Please double check!',
36 | text: 'Replace old path: ' + self.oldPath() + '\n with new path: ' + self.newPath() + '?',
37 | type: "warning",
38 | showCancelButton: true,
39 | confirmButtonColor: "#DD6B55",
40 | confirmButtonText: "Yes, Replace it!",
41 | closeOnConfirm: false
42 | },
43 | function(isConfirm){
44 | if (isConfirm) {
45 | $.ajax({
46 | url: '/tools/refactor_path/',
47 | async: true,
48 | data: paths_dict,
49 | type: 'POST',
50 | success: function(data){
51 | swal({
52 | title:"Success!",
53 | text: self.path_message(data.config_dict),
54 | closeOnConfirm: false,
55 | allowOutsideClick: true
56 | });
57 | },
58 | error: function(data) {
59 | swal({
60 | title:"Error!",
61 | text: data.responseJSON.errorText,
62 | closeOnConfirm: false
63 | });
64 | }
65 | });
66 | } else {
67 | return;
68 | }
69 | })
70 | }
71 | };
72 |
73 | // function for creating a string with a list
74 | self.path_message = function(path_dict){
75 | var message = 'Replaced for paths: \n';
76 | ko.utils.arrayForEach(path_dict, function(path) {
77 | message = message + path + '\n';
78 | });
79 | return message
80 | };
81 |
82 | }
83 | });
84 |
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zoom
2 |
3 | Zoom is a platform created by Spot Trading to manage real-time application startup, configuration and dependency management. Zoom provides a server-side agent and front-end web interface to allow for easy administration of our software infrastructure.
4 |
5 | ## About
6 |
7 | ### The Agent
8 |
9 | The Zoom agent is python service called Sentinel. It is responsible for watching the status of Linux daemons and Windows services. When the service it is watching is running, the agent creates a node in ZooKeeper.
10 |
11 | All interaction with Sentinel happens in ZooKeeper:
12 |
13 | - Configuration: hosted as xml text within a node in ZooKeeper. The Agent will automatically restart upon a configuration change.
14 | - Commands to underlying daemons: To send a 'restart' to the daemon being monitored, we created a node in ZooKeeper with JSON data inside.
15 |
16 |
17 | ### The Web Front-end
18 |
19 | The Web Front-end (which we simply call Zoom) is completely abstracted from the agents. This means that the agents will still run as they are configured to without the Zoom web server running. Zoom is more a means for humans to get involved should they need to start/stop an application, change the configuration of an agent, etc.
20 |
21 | ### Technologies used
22 |
23 | Zoom's web front-end is powered by [Tornado Web](https://github.com/tornadoweb)'s [Tornado](https://github.com/tornadoweb/tornado) web server. Our web interface was designed using Twitter's [Bootstrap](http://getbootstrap.com/) framework. In implementing Zoom's front-end, we used a handful of open-source JavaScript libraries. We simplified the inclusion and handling of these modules using [jrburke](https://github.com/jrburke)'s [RequireJS](http://requirejs.org/) library. To support real-time updates to Zoom's web portal, we used the [Sammy.js](http://sammyjs.org/), [Knockout](http://knockoutjs.com/index.html), and [jQuery](http://jquery.com/) libraries. We implemented dependency visualization using [mbostock](https://github.com/mbostock)'s [D3.js](http://d3js.org/) library, and we provide users with formatted XML in our sentinel configuration tool using [vkiryukhin](https://github.com/vkiryukhin)'s elegant [vkBeautify](http://www.eslinstructor.net/vkbeautify/) formatter.
24 |
25 | ## Motivation
26 |
27 | The concept behind the whole project is that certain applications require specific resources in order to start correctly. App1 may need App2 to be running, for example. We manage this complexity by representing every application as a node in ZooKeeper. We then set watches (A ZooKeeper concept) on the nodes each application requires. When these nodes are created or destroyed, the watch triggers a callback within the agent. In this manner, applications will not start until they are ready to, and will do so automatically.
28 |
29 | ## System Requirements and Prerequisites
30 |
31 | It's assumed that you are familiar with [Apache](https://github.com/apache)'s [ZooKeeper](https://github.com/apache/zookeeper) and the basics of administering it. Lots of useful information regarding ZooKeeper can be found in the [ZooKeeper Administrator's Guide](http://zookeeper.apache.org/doc/trunk/zookeeperAdmin.html#sc_administering).
32 |
33 | To use Zoom, you must have [Python](https://www.python.org/) installed. For your convenience, we have provided a ```bootstrap.sh``` file in ```sentinel/agent/scripts/``` which uses Python's ```easy_install``` module to include the ```python-ldap```, ```tornado```, ```kazoo```, ```setproctitle```, ```requests```, and ```pyodbc``` Python packages.
34 |
35 |
36 | ## Goals
37 |
38 | Why are we open-sourcing? Read about at Spot's engineering blog [here](http://www.spottradingllc.com/hello-world/).
39 |
--------------------------------------------------------------------------------
/client/viewmodels/applicationState.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'durandal/app',
4 | 'knockout',
5 | 'service',
6 | 'jquery',
7 | 'd3',
8 | 'model/loginModel',
9 | 'model/ApplicationStateModel',
10 | 'model/GlobalMode',
11 | 'viewmodels/navbar',
12 | 'bindings/radio',
13 | 'bindings/tooltip'
14 | ],
15 | function(app, ko, service, $, d3, login, ApplicationStateModel, GlobalMode, navbar) {
16 | var self = this;
17 | self.navbar = navbar;
18 | self.login = login;
19 | self.appStateModel = new ApplicationStateModel(self.login);
20 | self.mode = GlobalMode;
21 | self.initialLoad = ko.observable(false);
22 |
23 | var callbackInstance = {};
24 | var callbackObj = function() {
25 | this.callback = function() {
26 | self.navbar.connection.onmessage = function (evt) {
27 | var message = JSON.parse(evt.data);
28 |
29 | if ('update_type' in message) {
30 |
31 | if (message.update_type === 'application_state') {
32 |
33 | $.each(message.application_states, function () {
34 | self.appStateModel.handleApplicationStatusUpdate(this);
35 | });
36 |
37 | // resort the column, holding its sorted direction
38 | self.appStateModel.holdSortDirection(true);
39 | self.appStateModel.sort(self.appStateModel.activeSort());
40 | }
41 |
42 | else if (message.update_type === 'global_mode') {
43 | self.mode.handleModeUpdate(message);
44 | }
45 | else if (message.update_type === 'timing_estimate') {
46 | self.mode.handleTimingUpdate(message);
47 | }
48 | else if (message.update_type === 'application_dependency') {
49 | self.appStateModel.handleApplicationDependencyUpdate(message);
50 | }
51 | else {
52 | console.log('unknown type in message: ' + message.update_type);
53 | }
54 | }
55 | else {
56 | console.log('no type in message');
57 | }
58 | };
59 | };
60 | };
61 |
62 |
63 | self.attached = function() {
64 | if (!self.initialLoad()) {
65 | self.initialLoad(true);
66 | self.appStateModel.loadApplicationStates(); // load initial data
67 | self.appStateModel.loadApplicationDependencies(); // load initial data
68 | // resort after all the dependencies trickle in.
69 | setTimeout(function() { self.appStateModel.sort(self.appStateModel.headers[0]); }, 3000);
70 | callbackInstance = new callbackObj;
71 | callbackInstance.callback();
72 | // Select the search box so clients can begin typing immediately
73 | $('#searchBox').select();
74 | }
75 |
76 | };
77 |
78 | self.detached = function() {
79 | self.appStateModel.clearGroupControl();
80 | };
81 |
82 | return {
83 | appStateModel: self.appStateModel,
84 | mode: self.mode,
85 | login: self.login,
86 | detached: self.detached,
87 | attached: self.attached
88 | };
89 | });
90 |
--------------------------------------------------------------------------------
/server/zoom/www/handlers/application_opdep_handler.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import httplib
4 | import os.path
5 | import tornado.web
6 |
7 | from zoom.common.decorators import TimeThis
8 |
9 |
10 | class ApplicationOpdepHandler(tornado.web.RequestHandler):
11 | @property
12 | def data_store(self):
13 | """
14 | :rtype: zoom.www.cache.data_store.DataStore
15 | """
16 | return self.application.data_store
17 |
18 | @property
19 | def app_state_path(self):
20 | """
21 | :rtype: str
22 | """
23 | return self.application.configuration.application_state_path
24 |
25 | @TimeThis(__file__)
26 | def get(self, path):
27 | """
28 | @api {get} /api/v1/application/opdep/[:id] Get Application's operational dependencies
29 | @apiVersion 1.0.0
30 | @apiName GetAppOpDep
31 | @apiGroup Dependency
32 | @apiSuccessExample {json} Success-Response:
33 | HTTP/1.1 200 OK
34 | {
35 | "opdep": [
36 | "/spot/software/state/application/foo",
37 | "/spot/software/state/application/bar"
38 | ]
39 | }
40 | """
41 | opdep_array = []
42 | opdep_dict = {}
43 |
44 | if not path.startswith(self.app_state_path):
45 | # be able to search by comp id, not full path
46 | path = os.path.join(self.app_state_path, path[1:])
47 |
48 | logging.info('Retrieving Application Operational Dependency Cache for '
49 | 'client {0}'.format(self.request.remote_ip))
50 | try:
51 | # append the service we want opdep for to the array
52 | opdep_array.append(path)
53 | if path:
54 | opdep_array = self._downstream_recursive(path, opdep_array)
55 | opdep_dict['opdep'] = opdep_array
56 | self.write(opdep_dict)
57 | else:
58 | self.write('Please specify a path for operational '
59 | 'dependency lookup')
60 |
61 | except Exception as e:
62 | logging.exception(e)
63 | self.set_status(httplib.INTERNAL_SERVER_ERROR)
64 | self.write(json.dumps({'errorText': str(e)}))
65 |
66 | self.set_header('Content-Type', 'application/json')
67 | logging.info('Done Retrieving Application Operational Dependency Cache')
68 |
69 | def _downstream_recursive(self, parent_path, opdep_array):
70 | app_cache = self.data_store.load_application_dependency_cache()
71 | item = app_cache.application_dependencies.get(parent_path, {})
72 |
73 | for downstream in item.get("downstream", {}):
74 | item = app_cache.application_dependencies.get(downstream, {})
75 | for dependency in item.get('dependencies', None):
76 | # Checks both HasChildren and HasGrandChildren predicates
77 | if dependency.get('path', None) in parent_path:
78 | if dependency.get('operational', None) is True:
79 | # only append elements not in the array
80 | if downstream not in opdep_array:
81 | opdep_array.append(downstream)
82 | # Recursive with the operational downstream path
83 | self._downstream_recursive(downstream, opdep_array)
84 | else:
85 | logging.debug('{0} is not operational '
86 | 'dependency of {1}'
87 | .format(parent_path, downstream))
88 |
89 | return opdep_array
90 |
--------------------------------------------------------------------------------
/server/zoom/agent/task/task.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import logging
4 |
5 |
6 | class Task(object):
7 | def __init__(self, name,
8 | func=None, args=(), kwargs={}, block=True,
9 | retval=True, target=None, host=None, result=None,
10 | submitted=None):
11 | """
12 | :type name: str
13 | :type func: types.FunctionType or None
14 | :type args: tuple
15 | :type kwargs: dict
16 | :type block: bool
17 | :type retval: bool
18 | :type target: str or None
19 | :type host: str or None
20 | :type result: str or None
21 | :type submitted: str or None
22 | """
23 | self.name = name
24 | self.func = func
25 | self.args = args
26 | self.kwargs = kwargs
27 | self.block = block
28 | self.retval = retval
29 | self.target = target
30 | self.host = host
31 | self.result = result
32 | self.submitted = submitted
33 |
34 | def to_json(self):
35 | """
36 | :rtype: str
37 | """
38 | return json.dumps(self.__dict__)
39 |
40 | def to_dict(self):
41 | """
42 | :rtype: dict
43 | """
44 | return self.__dict__
45 |
46 | @staticmethod
47 | def from_json(json_data):
48 | """
49 | :type json_data: str or dict
50 | :rtype: zoom.agent.task.task.Task
51 | """
52 | if isinstance(json_data, str):
53 | try:
54 | json_data = json.loads(json_data)
55 | except ValueError:
56 | logging.warning('Data could not be parsed: {0}'
57 | .format(json_data))
58 | return None
59 |
60 | return Task(json_data.get('name'),
61 | func=json_data.get('func', None),
62 | args=json_data.get('args', ()),
63 | kwargs=json_data.get('kwargs', {}),
64 | block=json_data.get('block', True),
65 | retval=json_data.get('retval', False),
66 | target=json_data.get('target', None),
67 | host=json_data.get('host', None),
68 | result=json_data.get('result', None),
69 | submitted=json_data.get('submitted', datetime.datetime.now().strftime('%Y%m%d %H:%M:%S')),
70 | )
71 |
72 | def __eq__(self, other):
73 | return all([
74 | type(self) == type(other),
75 | self.name == getattr(other, 'name', None),
76 | self.target == getattr(other, 'target', None),
77 | self.host == getattr(other, 'host', None)
78 | ])
79 |
80 | def __ne__(self, other):
81 | return any([
82 | type(self) != type(other),
83 | self.name != getattr(other, 'name', None),
84 | self.target != getattr(other, 'target', None),
85 | self.host != getattr(other, 'host', None)
86 | ])
87 |
88 | def __str__(self):
89 | return self.__repr__()
90 |
91 | def __repr__(self):
92 | return ('{0}(name={1}, args={2}, kwargs={3}, block={4}, '
93 | 'target={5}, host={6}, result={7}, submitted={8})'
94 | .format(self.__class__.__name__,
95 | self.name,
96 | self.args,
97 | self.kwargs,
98 | self.block,
99 | self.target,
100 | self.host,
101 | self.result,
102 | self.submitted))
103 |
--------------------------------------------------------------------------------
/client/model/GlobalMode.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'knockout',
4 | 'service',
5 | 'jquery',
6 | 'model/loginModel'
7 | ],
8 | function(ko, service, $, login) {
9 | var self = this;
10 | self.login = login;
11 | self.current = ko.observable('Unknown');
12 | self.maxTimingEstimate = ko.observable('');
13 | self.minTimingEstimate = ko.observable('');
14 |
15 | self.isOn = ko.computed(function() {
16 | return self.current() === 'auto';
17 | });
18 |
19 | self.OnClass = ko.computed(function() {
20 | if (self.isOn()) {
21 | return 'btn btn-default active';
22 | }
23 | else {
24 | return 'btn btn-default';
25 | }
26 | });
27 | self.OffClass = ko.computed(function() {
28 | if (self.current() === 'manual') {
29 | return 'btn btn-default active';
30 | }
31 | else {
32 | return 'btn btn-default';
33 | }
34 | });
35 |
36 | self.handleModeUpdate = function(data) {
37 | var update = JSON.parse(data.global_mode);
38 | self.current(update.mode);
39 | };
40 |
41 | self.handleTimingUpdate = function(data) {
42 | var error_msg = data.error_msg;
43 | if (error_msg !== undefined) {
44 | swal('Well shoot...', error_msg, 'error');
45 | return;
46 | }
47 |
48 | var maxtime = JSON.parse(data.maxtime);
49 | var endMs = Date.now() + maxtime * 1000;
50 | var end = new Date(endMs);
51 | self.maxTimingEstimate(end.toLocaleTimeString());
52 |
53 | var mintime = JSON.parse(data.mintime);
54 | var endMs = Date.now() + mintime * 1000;
55 | end = new Date(endMs);
56 | self.minTimingEstimate(end.toLocaleTimeString());
57 | };
58 |
59 | self.timingString = ko.computed(function() {
60 | if (self.maxTimingEstimate() === self.minTimingEstimate()) {
61 | return self.maxTimingEstimate();
62 | }
63 | else {
64 | return self.minTimingEstimate() + ' - ' + self.maxTimingEstimate();
65 | }
66 | });
67 |
68 | var onGlobalModeError = function(data) {
69 | swal('Well shoot...', 'An Error has occurred while getting the global mode.', 'error');
70 | };
71 |
72 | self.getGlobalMode = function() {
73 | return service.get('api/v1/mode/', self.handleModeUpdate, onGlobalModeError);
74 | };
75 |
76 | self.getTimingEstimate = function() {
77 | return service.get('api/v1/timingestimate',
78 | self.handleTimingUpdate,
79 | function() {
80 | swal('Well shoot...', 'An Error has occurred while getting the initial time estimate', 'error');
81 | });
82 | };
83 |
84 | self.setGlobalMode = function(mode) {
85 | if (confirm('Please confirm that you want to set Zookeeper to ' + mode + ' mode by pressing OK.')) {
86 | var dict = {
87 | 'command': mode,
88 | 'user': self.login.elements.username()
89 | };
90 | $.post('/api/v1/mode/', dict).fail(function(data) {
91 | swal('Error Posting Mode Control.', JSON.stringify(data), 'error');
92 | });
93 | }
94 | };
95 |
96 | self.getGlobalMode(); // get initial data
97 | self.getTimingEstimate(); // get initial data
98 |
99 | return self;
100 | });
101 |
--------------------------------------------------------------------------------
/client/classes/LogicPredicate.js:
--------------------------------------------------------------------------------
1 | define(['knockout'],
2 | function(ko) {
3 | return function LogicPredicate(Factory, predType, parent) {
4 | var self = this;
5 | self.expanded = ko.observable(false);
6 | self.predType = predType;
7 | self.title = self.predType.toUpperCase();
8 | self.predicates = ko.observableArray();
9 |
10 | self.parent = parent;
11 | self.isLogicalPred = true;
12 |
13 | self.addPredicate = function(type) {
14 | var pred = Factory.newPredicate(self, type);
15 | self.expanded(true);
16 | self.predicates.unshift(pred); // add to front of array
17 | };
18 |
19 | self.remove = function() {
20 | self.parent.predicates.remove(self);
21 | };
22 |
23 | self.toggleExpanded = function(expand) {
24 | if (typeof expand !== 'undefined') {
25 | self.expanded(expand);
26 | }
27 | else {
28 | self.expanded(!self.expanded());
29 | }
30 | ko.utils.arrayForEach(self.predicates(), function(predicate) {
31 | predicate.toggleExpanded(self.expanded());
32 | });
33 | };
34 |
35 | var getErrors = function() {
36 | // return only errors related to this object
37 | var errors = [];
38 |
39 | if (self.predType === 'not' && self.predicates().length !== 1) {
40 | errors.push('NOT Predicate accepts exactly one child predicate.');
41 | }
42 | else if (self.predType === 'or' && self.predicates().length < 2) {
43 | errors.push('OR Predicate needs two or more child predicates.');
44 | }
45 | else if (self.predType === 'and' && self.predicates().length < 2) {
46 | errors.push('AND Predicate needs two or more child predicates.');
47 | }
48 |
49 | return errors;
50 | };
51 |
52 | self.error = ko.computed(function() {
53 | var e = getErrors();
54 | return e.join(', ');
55 | });
56 |
57 | self.validate = function() {
58 | // return errors for this object and all child objects
59 | var allErrors = getErrors();
60 |
61 | ko.utils.arrayForEach(self.predicates(), function(predicate) {
62 | allErrors = allErrors.concat(predicate.validate());
63 | });
64 |
65 | return allErrors;
66 | };
67 |
68 | self.createPredicateXML = function() {
69 | var XML = '';
70 | var header = '';
71 | XML = XML.concat(header);
72 |
73 | for (var i = 0; i < self.predicates().length; i++) {
74 | XML = XML.concat(self.predicates()[i].createPredicateXML());
75 | }
76 |
77 | var footer = '';
78 | XML = XML.concat(footer);
79 |
80 | return XML;
81 | };
82 |
83 | self.loadXML = function(node) {
84 | self.predicates.removeAll();
85 | var child = Factory.firstChild(node);
86 | while (child !== null) {
87 | var type = child.getAttribute('type');
88 | var predicate = Factory.newPredicate(self, type);
89 | predicate.loadXML(child);
90 | self.predicates.push(predicate);
91 | child = Factory.nextChild(child);
92 | }
93 |
94 | };
95 | };
96 | });
97 |
--------------------------------------------------------------------------------
/server/zoom/common/pagerduty.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import pygerduty
3 |
4 |
5 | class PagerDuty(object):
6 | """
7 | Create or resolve PagerDuty alerts.
8 | """
9 | def __init__(self, subdomain, organization_token, default_svc_key,
10 | alert_footer=''):
11 | """
12 | :type subdomain: str
13 | :type organization_token: str
14 | :type default_svc_key: str
15 | :type alert_footer: str
16 | """
17 | self._log = logging.getLogger('pagerduty')
18 | self._pager = pygerduty.PagerDuty(subdomain,
19 | api_token=organization_token)
20 | self._org_token = organization_token
21 | self._default_svc_key = default_svc_key # Zoom Service api key
22 | self._alert_footer = alert_footer
23 |
24 | def trigger(self, svc_key, incident_key, description, details):
25 | """
26 | :type svc_key: str
27 | :type incident_key: str
28 | :type description: str
29 | :type details: str
30 | """
31 | full_details = details + '\n' + self._alert_footer
32 | try:
33 | if svc_key is None:
34 | svc_key = self._default_svc_key
35 | self._log.info('Creating incident for api key: {0}'
36 | ' and incident key: {1}'
37 | .format(svc_key, incident_key))
38 | self._pager.trigger_incident(service_key=svc_key,
39 | incident_key=incident_key,
40 | description=description,
41 | details=full_details)
42 | except Exception as ex:
43 | self._log.error('An Exception occurred trying to trigger incident '
44 | 'with key {0}: {1}'.format(incident_key, ex))
45 |
46 | def resolve(self, svc_key, incident_key):
47 | """
48 | :type svc_key: str
49 | :type incident_key: str
50 | """
51 | if svc_key is None:
52 | svc_key = self._default_svc_key
53 | self._log.info('Resolving incident for api key: {0}'
54 | ' and incident key: {1}'.format(svc_key, incident_key))
55 | if incident_key in self.get_open_incidents():
56 | try:
57 | self._pager.resolve_incident(service_key=svc_key,
58 | incident_key=incident_key)
59 | except Exception as ex:
60 | self._log.error('An Exception occurred trying to resolve '
61 | 'incident with key {0}: {1}'
62 | .format(incident_key, ex))
63 |
64 | def get_open_incidents(self):
65 | """
66 | :rtype: list
67 | """
68 | try:
69 | triggered = [incident.incident_key for incident in
70 | self._pager.incidents.list(status="triggered")]
71 | acknowledged = [incident.incident_key for incident in
72 | self._pager.incidents.list(status="acknowledged")]
73 | return triggered + acknowledged
74 | except Exception as ex:
75 | self._log.error('An Exception occurred trying to get open '
76 | 'incidents: {0}'.format(ex))
77 | return list()
78 |
79 | def get_service_dict(self):
80 | """
81 | Return dict of configured services in PagerDuty
82 | :rtype: dict
83 | """
84 | pd_service_dict = {}
85 | pd_services = self._pager.services.list(limit=100)
86 | for pd_service in pd_services:
87 | if pd_service.email_incident_creation is None:
88 | pd_service_dict[pd_service.name] = pd_service.service_key
89 | return pd_service_dict
90 |
--------------------------------------------------------------------------------
/server/zoom/www/entities/application_state.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from zoom.common.types import ApplicationStatus
4 |
5 |
6 | class ApplicationState(object):
7 | def __init__(self, application_name=None, configuration_path=None,
8 | application_status=None, application_host=None,
9 | last_update=None, start_stop_time=None, error_state=None,
10 | local_mode=None, delete=False, login_user=None,
11 | last_command=None, read_only=None, grayed=None,
12 | pd_disabled=None, platform=None, restart_count=None, load_times=None):
13 | self._application_name = application_name
14 | self._configuration_path = configuration_path
15 | self._application_status = application_status
16 | self._application_host = application_host
17 | self._last_update = last_update
18 | self._start_stop_time = start_stop_time
19 | self._error_state = error_state
20 | self._local_mode = local_mode
21 | self._delete = delete
22 | self._login_user = login_user
23 | self._last_command = last_command
24 | self._read_only = read_only
25 | self._grayed = grayed
26 | self._pd_disabled = pd_disabled
27 | self._platform = platform
28 | self._restart_count = restart_count
29 | self._load_times = load_times
30 |
31 | def __del__(self):
32 | pass
33 |
34 | def __enter__(self):
35 | pass
36 |
37 | def __exit__(self, type, value, traceback):
38 | pass
39 |
40 | def __eq__(self, other):
41 | return self._configuration_path == other.configuration_path
42 |
43 | def __ne__(self, other):
44 | return self._configuration_path != other.configuration_path
45 |
46 | def __repr__(self):
47 | return str(self.to_dictionary())
48 |
49 | @property
50 | def application_name(self):
51 | return self._application_name
52 |
53 | @property
54 | def configuration_path(self):
55 | return self._configuration_path
56 |
57 | @property
58 | def application_status(self):
59 | if self._application_status == ApplicationStatus.RUNNING:
60 | return "running"
61 | elif self._application_status == ApplicationStatus.STARTING:
62 | return "starting"
63 | elif self._application_status == ApplicationStatus.STOPPED:
64 | return "stopped"
65 |
66 | return "unknown"
67 |
68 | @property
69 | def application_host(self):
70 | if self._application_host is not None:
71 | return self._application_host
72 |
73 | return ""
74 |
75 | @property
76 | def last_update(self):
77 | if self._last_update is not None:
78 | return datetime.fromtimestamp(
79 | self._last_update).strftime('%Y-%m-%d %H:%M:%S')
80 |
81 | return ""
82 |
83 | def to_dictionary(self):
84 | return {
85 | 'application_name': self.application_name,
86 | 'configuration_path': self.configuration_path,
87 | 'application_status': self.application_status,
88 | 'application_host': self.application_host,
89 | 'last_update': self.last_update,
90 | 'start_stop_time': self._start_stop_time,
91 | 'error_state': self._error_state,
92 | 'delete': self._delete,
93 | 'local_mode': self._local_mode,
94 | 'login_user': self._login_user,
95 | 'last_command': self._last_command,
96 | 'read_only': self._read_only,
97 | 'grayed': self._grayed,
98 | 'pd_disabled': self._pd_disabled,
99 | 'platform': self._platform,
100 | 'restart_count': self._restart_count,
101 | 'load_times': self._load_times
102 | }
103 |
--------------------------------------------------------------------------------
/server/zoom/agent/client/wmi_client.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import pythoncom
3 |
4 | from subprocess import Popen, PIPE
5 | from time import sleep
6 | from wmi import WMI, x_wmi
7 |
8 |
9 | class WMIServiceClient(object):
10 | def __init__(self, service_name):
11 | self._service_name = service_name
12 | self._log = logging.getLogger('sent.process')
13 |
14 | @property
15 | def functional(self):
16 | """
17 | This function checks whether a WMI call will be successful.
18 | :rtype: bool
19 | """
20 | try:
21 | c = WMI()
22 | service = c.Win32_Service(Name=self._service_name)
23 | if not service:
24 | self._log.warning('WMI success, but could not find service')
25 | raise Exception('Script was unable to find service on the '
26 | 'server. Check service name for validity.')
27 | else:
28 | self._log.debug('WMI success, & found service')
29 | return True
30 | except x_wmi:
31 | self._log.warning('COM Error: The RPC server is unavailable. '
32 | 'WMI call has failed.')
33 | return False
34 |
35 | def start(self):
36 | """
37 | Start service with WMI
38 | :rtype: int
39 | """
40 | pythoncom.CoInitialize()
41 | c = WMI()
42 | for service in c.Win32_Service(Name=self._service_name):
43 | retval = service.StartService()
44 | return retval[0]
45 |
46 | def stop(self):
47 | """
48 | Start service with WMI
49 | :rtype: int
50 | """
51 | returncode = -1
52 | counter = 0
53 |
54 | pythoncom.CoInitialize()
55 | c = WMI()
56 |
57 | # Run stop with WMI
58 | for service in c.Win32_Service(Name=self._service_name):
59 | retval = service.StopService()
60 | returncode = retval[0]
61 |
62 | # Check if service has stopped yet
63 | while self.status() and counter < 10:
64 | counter += 1
65 | sleep(1)
66 |
67 | # If still running after stop command
68 | pid = self.status(pid=True)
69 | if pid:
70 | self._log.info('Service did not respond to "stop" in a timely '
71 | 'manner. Killing the PID {0}.'.format(pid))
72 | for process in c.Win32_Process(ProcessId=pid):
73 | retval = process.Terminate()
74 | returncode = retval[0]
75 |
76 | # If still running after attempt at process termination via WMI
77 | if self.status():
78 | self._log.warning('Service could not be killed with WMI. '
79 | 'Trying console command: TASKKILL')
80 | cmd = "TASKKILL /F /PID {0}".format(pid)
81 | p = Popen(cmd, stdout=PIPE, stderr=PIPE)
82 | p.wait()
83 | returncode = p.returncode
84 |
85 | return returncode
86 |
87 | def status(self, pid=False):
88 | """
89 | Make WMI call to server to get whether the service is running.
90 | :type pid: bool
91 | :rtype: int or bool
92 | """
93 | process_id = 0
94 | pythoncom.CoInitialize()
95 | c = WMI()
96 | for service in c.Win32_Service(Name=self._service_name):
97 | process_id = service.ProcessId
98 | self._log.debug('Process is running: {0}'.format(bool(process_id)))
99 | if pid:
100 | return process_id
101 | else:
102 | return bool(process_id)
103 |
104 | def __repr__(self):
105 | return '{0}(service="{1}")'.format(self.__class__.__name__,
106 | self._service_name)
107 |
108 | def __str__(self):
109 | return self.__repr__()
110 |
--------------------------------------------------------------------------------
/server/zoom/agent/config/config_schema.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/server/zoom/agent/predicate/process.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from threading import Thread
3 | from time import sleep
4 | from multiprocessing import Lock
5 |
6 | from zoom.agent.predicate.simple import SimplePredicate
7 | from zoom.agent.entities.thread_safe_object import ThreadSafeObject
8 |
9 |
10 | class PredicateProcess(SimplePredicate):
11 | def __init__(self, comp_name, proc_client, interval,
12 | operational=False, parent=None):
13 | """
14 | :type comp_name: str
15 | :type proc_client: zoom.agent.client.process_client.ProcessClient
16 | :type interval: int or float
17 | :type operational: bool
18 | :type parent: str or None
19 | """
20 | SimplePredicate.__init__(self, comp_name, operational=operational, parent=parent)
21 | self._log = logging.getLogger('sent.{0}.pred.process'.format(comp_name))
22 | self._proc_client = proc_client
23 |
24 | # lock for synchronous decorator
25 | if proc_client:
26 | self.process_client_lock = proc_client.process_client_lock
27 | else:
28 | self.process_client_lock = Lock()
29 |
30 | self.interval = interval
31 | self._operate = ThreadSafeObject(True)
32 | self._thread = Thread(target=self._run_loop, name=str(self))
33 | self._thread.daemon = True
34 | self._started = False
35 |
36 | def running(self):
37 | """
38 | With the synchronous decorator, this shares a Lock object with the
39 | ProcessClient. While ProcessClient.start is running, this will not
40 | return.
41 | :rtype: bool
42 | """
43 | return self._proc_client.running()
44 |
45 | def start(self):
46 | if self._started is False:
47 | self._log.debug('Starting {0}'.format(self))
48 | self._started = True
49 | self._thread.start()
50 | self._block_until_started()
51 | else:
52 | self._log.debug('Already started {0}'.format(self))
53 |
54 | def stop(self):
55 | if self._started is True:
56 | self._log.info('Stopping {0}'.format(self))
57 | self._started = False
58 | self._operate.set_value(False)
59 | self._thread.join()
60 | self._log.info('{0} stopped'.format(self))
61 | else:
62 | self._log.debug('Already stopped {0}'.format(self))
63 |
64 | def _run_loop(self):
65 | cancel_counter = 0
66 | while self._operate == True:
67 | if self._proc_client.cancel_flag == False:
68 | self.set_met(self.running())
69 | cancel_counter = 0
70 | elif cancel_counter > 1:
71 | self._log.info('Waited long enough. Resetting cancel flag.')
72 | self._proc_client.cancel_flag.set_value(False)
73 | cancel_counter = 0
74 | else:
75 | cancel_counter += 1
76 | self._log.info('Cancel Flag detected, skipping status check.')
77 |
78 | sleep(self.interval)
79 | self._log.info('Done watching process.')
80 |
81 | def __repr__(self):
82 | return ('{0}(component={1}, parent={2}, interval={3}, started={4}, '
83 | 'operational={5}, met={6})'
84 | .format(self.__class__.__name__,
85 | self._comp_name,
86 | self._parent,
87 | self.interval,
88 | self.started,
89 | self._operational,
90 | self._met)
91 | )
92 |
93 | def __eq__(self, other):
94 | return all([
95 | type(self) == type(other),
96 | self.interval == getattr(other, 'interval', None)
97 | ])
98 |
99 | def __ne__(self, other):
100 | return any([
101 | type(self) != type(other),
102 | self.interval != getattr(other, 'interval', None)
103 | ])
104 |
--------------------------------------------------------------------------------
/client/service.js:
--------------------------------------------------------------------------------
1 | define(['jquery'], function($) {
2 |
3 | var getUrl = function(path) {
4 | return '' + location.origin + '/' + path;
5 | };
6 |
7 | var getCookie = function(c_name) {
8 | var c_value = document.cookie;
9 | // c_value looks like: user='foo.bar@spottrading.com'
10 | var c_start = c_value.indexOf(' ' + c_name + '=');
11 | if (c_start == -1) {
12 | c_start = c_value.indexOf(c_name + '=');
13 | }
14 | if (c_start == -1) {
15 | c_value = null;
16 | }
17 | else {
18 | c_start = c_value.indexOf('=', c_start) + 1;
19 | var c_end = c_value.indexOf(';', c_start);
20 | if (c_end === -1) {
21 | c_end = c_value.length;
22 | }
23 | c_value = c_value.substring(c_start, c_end);
24 | }
25 | return c_value;
26 | };
27 |
28 | return {
29 | getCookie: getCookie,
30 |
31 | get: function(path, callback, errorCallback) {
32 | return $.ajax(getUrl(path), {
33 | dataType: 'json',
34 | type: 'GET',
35 | success: function(data) {
36 | return callback(data);
37 | },
38 | error: function(jqxhr) {
39 | return errorCallback(jqxhr.responseJSON);
40 | }
41 | });
42 | },
43 |
44 | synchronousGet: function(path, callback, errorCallback) {
45 | return $.ajax(getUrl(path), {
46 | async: false,
47 | dataType: 'json',
48 | type: 'GET',
49 | success: function(data) {
50 | return callback(data);
51 | },
52 | error: function(jqxhr) {
53 | return errorCallback(jqxhr.responseJSON);
54 | }
55 | });
56 | },
57 |
58 | post: function(path, params, callback, errorCallback) {
59 | return $.ajax(getUrl(path), {
60 | data: JSON.stringify(params),
61 | dataType: 'json',
62 | type: 'POST',
63 | success: function(data) {
64 | return callback(data);
65 | },
66 | error: function(jqxhr) {
67 | return errorCallback(jqxhr.responseJSON);
68 | }
69 | });
70 | },
71 |
72 | put: function(path, params, callback, errorCallback) {
73 | return $.ajax(getUrl(path), {
74 | data: JSON.stringify(params),
75 | dataType: 'json',
76 | type: 'PUT',
77 | success: function(data) {
78 | return callback(data);
79 | },
80 | error: function(jqxhr) {
81 | return errorCallback(jqxhr.responseJSON);
82 | }
83 | });
84 | },
85 |
86 | del: function(path, params, callback, errorCallback) {
87 | return $.ajax(getUrl(path), {
88 | type: 'DELETE',
89 | success: function(data) {
90 | if (callback) {
91 | return callback(data);
92 | }
93 | },
94 | error: function(jqxhr) {
95 | if (errorCallback) {
96 | return errorCallback(jqxhr.responseJSON);
97 | }
98 | }
99 | });
100 | }
101 | };
102 | });
103 |
104 |
105 | // ...modern way of doing things....using promises/futures
106 | //
107 | // var jqxhr = $.post('/login', JSON.stringify(data), function(response) {
108 | // console.log('POST (response):' + JSON.stringify(response));
109 | // });
110 | //
111 | // jqxhr.fail(function(response) {
112 | // console.log('POST (response):' + JSON.stringify(response));
113 | // self.showErrorText(true);
114 | // });
115 |
--------------------------------------------------------------------------------