├── 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 |
2 |
3 |

TODO

4 |
5 |
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 | --------------------------------------------------------------------------------