├── .gitignore ├── AUTHORS ├── LICENSE.txt ├── MANIFEST.in ├── README ├── README.md ├── demo.conf ├── demo.py ├── docs ├── bootstrap-cst.md ├── diagrams.graffle ├── features.md ├── manual │ ├── .gitignore │ ├── Makefile │ ├── _static │ │ └── .gitignore │ ├── _templates │ │ ├── .gitignore │ │ └── layout.html │ ├── cmdline-console.rst │ ├── cmdline-restart.rst │ ├── cmdline-server.rst │ ├── cmdline-start.rst │ ├── cmdline-status.rst │ ├── cmdline-stop.rst │ ├── cmdline-tail.rst │ ├── cmdline-who.rst │ ├── cmdline-wininstall.rst │ ├── cmdline-winuninstall.rst │ ├── cmdline.rst │ ├── conf.py │ ├── config.rst │ ├── credits.rst │ ├── features.rst │ ├── img │ │ ├── execmodel.png │ │ ├── httpfend.png │ │ └── progstates.png │ ├── index.rst │ ├── install.rst │ ├── intro.rst │ ├── make.bat │ ├── nondeamon.rst │ └── tools.rst ├── release-proc.md ├── todo.md └── vocab.txt ├── gource.config ├── ramona-dev.py ├── ramona.conf ├── ramona ├── __init__.py ├── __utest__.py ├── cnscom.py ├── config.py ├── console │ ├── __init__.py │ ├── cmd │ │ ├── __init__.py │ │ ├── _completions.py │ │ ├── console.py │ │ ├── exit.py │ │ ├── help.py │ │ ├── notify.py │ │ ├── restart.py │ │ ├── server.py │ │ ├── start.py │ │ ├── status.py │ │ ├── stop.py │ │ ├── tail.py │ │ ├── who.py │ │ ├── wininstall.py │ │ └── winuninstall.py │ ├── cnsapp.py │ ├── exception.py │ ├── parser.py │ └── winsvc.py ├── httpfend │ ├── 401.tmpl.html │ ├── __init__.py │ ├── __main__.py │ ├── _request_handler.py │ ├── _tailf.py │ ├── app.py │ ├── index.tmpl.html │ ├── log_frame.tmpl.html │ └── static │ │ ├── bootstrap │ │ └── css │ │ │ ├── .gitignore │ │ │ └── bootstrap.min.css │ │ ├── img │ │ ├── ajax-loader.gif │ │ ├── favicon.ico │ │ └── favicon.svg │ │ └── miniajax │ │ └── miniajax.js ├── kmpsearch.py ├── sendmail.py ├── server │ ├── __init__.py │ ├── __main__.py │ ├── __utest__.py │ ├── call_status.py │ ├── cnscon.py │ ├── idlework.py │ ├── logmed.py │ ├── notify.py │ ├── proaster.py │ ├── program.py │ ├── seqctrl.py │ ├── singleton.py │ └── svrapp.py ├── socketuri.py └── utils.py ├── setup.py ├── test.conf └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | /ramonadev_history 2 | *.py[co] 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | # Ramona specific ignores 31 | *.sublime-* 32 | 33 | demo_history 34 | log/*.log 35 | log 36 | 37 | MANIFEST 38 | 39 | # Eclipse/PyDev 40 | .project 41 | .pydevproject 42 | .settings 43 | /log-test 44 | *.pid 45 | /ramona-test-site.conf 46 | gource.log 47 | gource.mp4 48 | .ramona.stash 49 | 50 | # Idea/PyCharm 51 | .idea 52 | 53 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ales Teska design, initial implementation 2 | Jan Stastny HTTP console 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Ales Teska 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE.txt 3 | include README.md 4 | 5 | recursive-include ramona *.py 6 | 7 | include ramona/httpfend/index.tmpl.html 8 | include ramona/httpfend/401.tmpl.html 9 | include ramona/httpfend/log_frame.tmpl.html 10 | recursive-include ramona/httpfend/static/bootstrap *.css 11 | recursive-include ramona/httpfend/static/miniajax *.js 12 | recursive-include ramona/httpfend/static/img *.ico *.gif 13 | 14 | exclude ramona/server/__utest__.py 15 | exclude ramona/__utest__.py 16 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ====== 2 | Ramona 3 | ====== 4 | 5 | Ramona is an enterprise-grade runtime supervisor that allows controlling and monitoring software programs during their execution life cycle. 6 | 7 | It is primarily meant to be blended into your project source code set to provide supervisor/console functionality of init.d-like start/stop control, task frontend (e.g. unit/functional/performance test launcher) and other command-line oriented features. It should ideally represent the only executable of the project - kind of 'dispatcher' to rest of a project. It is design the way that you should be able to extend that easily if needed (e.g. to include your own tasks). 8 | 9 | For more info see http://ateska.github.com/ramona/ 10 | 11 | Documentation can be found here: http://ateska.github.com/ramona/manual 12 | 13 | Mailing list is here: https://groups.google.com/forum/#!forum/ramona-supervisor 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ramona 2 | ====== 3 | 4 | Your next favorite supervisor component. 5 | 6 | Ramona is an enterprise-grade **runtime supervisor** that allows controlling and monitoring software programs during their execution life cycle. 7 | 8 | It provides supervisor/console functionality of init.d-like start/stop control, continuous integration (e.g. unit/functional/performance test launcher), deployment automation and other command-line oriented features. It is design the way that you should be able to extend that easily if needed (e.g. to include your own commands or tasks). 9 | 10 | It is implemented in Python but it is not limited to be used only in Python projects. 11 | 12 | Target platforms are all modern UNIXes, BSD derivates and Windows. 13 | 14 | Quick introduction 15 | ------------------ 16 | 17 | Let's assume your project (named _foo_) directory looks as follow: 18 | ```shell 19 | foo/ 20 | bin/ 21 | share/ 22 | src/ 23 | docs/ 24 | foo.py <--- this is Ramona 25 | foo.conf 26 | ``` 27 | 28 | Ramona system will the provide you with following command-line API: 29 | ``` 30 | $ ./foo.py --help 31 | usage: foo.py [-h] [-c CONFIGFILE] [-d] [-s] 32 | {start,stop,restart,status,help,console,server,clean,unittests} 33 | ... 34 | 35 | optional arguments: 36 | -h, --help show this help message and exit 37 | -c CONFIGFILE, --config CONFIGFILE 38 | Specify configuration file(s) to read (this option can 39 | be given more times). This will override build-in 40 | application level configuration. 41 | -d, --debug Enable debug (verbose) output. 42 | -s, --silent Enable silent mode of operation (only errors are 43 | printed). 44 | 45 | subcommands: 46 | {start,stop,restart,status,help,console,server} 47 | start Launch subprocess(es) 48 | stop Terminate subprocess(es) 49 | restart Restart subprocess(es) 50 | status Show status of subprocess(es) 51 | help Display help 52 | console Enter interactive console mode 53 | server Launch server in the foreground 54 | ``` 55 | 56 | Links 57 | ----- 58 | 59 | * [Ramona project page](http://ateska.github.com/ramona/) 60 | * [Ramona documentation](http://ateska.github.com/ramona/manual) 61 | * [Ramona mailing list](https://groups.google.com/forum/#!forum/ramona-supervisor) 62 | * [Ramona @ GitHub](https://github.com/ateska/ramona) 63 | * [Ramona @ PyPi](http://pypi.python.org/pypi/ramona) 64 | * [Ramona @ Ohloh](https://www.ohloh.net/p/ateska_ramona) 65 | -------------------------------------------------------------------------------- /demo.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | appname=ramona-demo 3 | 4 | logdir=./log 5 | logmaxsize=10000 6 | 7 | [env] 8 | RAMONADEMO=test 9 | RAMONAdemo1=test2 10 | STRIGAROOT= 11 | DEVNULL=/dev/null 12 | FOO=bar 13 | 14 | [env:alternative1] 15 | BAR=foo 16 | 17 | [ramona:server] 18 | #consoleuri=unix:///tmp/ramona-demo.sock?mode=0600 19 | #consoleuri=tcp://127.0.0.1:8899?ssl=1&certfile=democert.pem 20 | consoleuri=tcp://127.0.0.1:8889 21 | pidfile=./ramona-demo.pid 22 | log=/ramona-demo.log 23 | 24 | [ramona:notify] 25 | #delivery=smtp://user:password@smtp.gmail.com:587/?tls=1 26 | receiver=foo@bar.com 27 | #sender=ramona@bar.com 28 | 29 | [ramona:console] 30 | #serveruri=unix:///tmp/ramona-demo.sock 31 | #serveruri=tcp://127.0.0.1:8899?ssl=1&certfile=democert.pem 32 | serveruri=tcp://127.0.0.1:8889 33 | history=~/.ramona_demo_history 34 | 35 | [program:hellocycle] 36 | command=bash -c "sleep 1; echo ahoj1 ttot neni error nebo je; sleep 1; echo ja nevim;sleep 1; echo error to je; sleep 1; echo -n err; sleep 2; echo or; sleep 1; ls --help; sleep 1" 37 | stdout= 38 | stderr= 39 | 40 | [program:testdisabled] 41 | disabled=true 42 | command=tail -f /dev/null 43 | 44 | [program:two] 45 | # Uses [env:alternative1] 46 | env=alternative1 47 | command=bash -c "while true; do date; echo Bar '(should print foo):' ${BAR}; echo 'Foo (should be blank):' ${FOO}; sleep 1; done" 48 | 49 | [program:ramonahttpfend] 50 | command= 51 | loglevel=DEBUG 52 | listen=tcp://localhost:4455,tcp://[::1]:5588 53 | #username=admin 54 | # Can get either plain text or a SHA1 hash, if the password starts with {SHA} prefix 55 | #password=password 56 | # SHA example. To generate use for example: echo -n "secret" | sha1sum 57 | #password={SHA}e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 58 | 59 | [program:testexit] 60 | command=bash -c "exit 2" 61 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # 3 | # Released under the BSD license. See LICENSE.txt file for details. 4 | # 5 | import os 6 | import ramona 7 | 8 | class MyDemoConsoleApp(ramona.console_app): 9 | 10 | @ramona.tool 11 | def tool_demo(self): 12 | """Printing message about demo of custom ramona.tool""" 13 | print "This is implementation of custom tool (see ./demo.sh --help)" 14 | # Example how to access configuration from tool: 15 | print "Value of env:RAMONADEMO = {0}".format(self.config.get("env", "RAMONADEMO")) 16 | env = ramona.config.get_env() 17 | print "All environment variables", env 18 | print 19 | env_alternative1 = ramona.config.get_env("alternative1") 20 | print "All alternative1 environment variables", env_alternative1 21 | 22 | 23 | 24 | @ramona.tool 25 | class tool_class_demo(object): 26 | """Demo of custom ramona.tool (class)""" 27 | 28 | def init_parser(self, cnsapp, parser): 29 | parser.description = 'You can use methods from argparse module of Python to customize tool (sub)parser.' 30 | parser.add_argument('integers', metavar='N', type=int, nargs='+', 31 | help='an integer for the accumulator' 32 | ) 33 | parser.add_argument('--sum', dest='accumulate', action='store_const', 34 | const=sum, default=max, 35 | help='sum the integers (default: find the max)' 36 | ) 37 | 38 | def main(self, cnsapp, args): 39 | print args.accumulate(args.integers) 40 | 41 | 42 | @ramona.proxy_tool 43 | def proxy_tool_demo(self, argv): 44 | """Proxying execution of /bin/ls""" 45 | os.execv('/bin/ls', argv) 46 | 47 | 48 | if __name__ == '__main__': 49 | app = MyDemoConsoleApp(configuration='./demo.conf') 50 | app.run() 51 | -------------------------------------------------------------------------------- /docs/bootstrap-cst.md: -------------------------------------------------------------------------------- 1 | Bootstrap customizations 2 | ======================== 3 | http://twitter.github.com/bootstrap/customize.html 4 | 5 | ## Choose components 6 | 7 | ### Scaffolding 8 | * [X] Normalize and reset 9 | * [X] Body type and links 10 | * [X] Grid system 11 | * [X] Layouts 12 | 13 | ### Base CSS 14 | * [X] Headings, body, etc 15 | * [X] Code and pre 16 | * [X] Labels and badges 17 | * [X] Tables 18 | * [X] Forms 19 | * [X] Buttons 20 | * [ ] Icons 21 | 22 | ### Components 23 | * [ ] Button groups and dropdowns 24 | * [ ] Navs, tabs, and pills 25 | * [ ] Navbar 26 | * [ ] Breadcrumbs 27 | * [ ] Pagination 28 | * [ ] Pager 29 | * [ ] Thumbnails 30 | * [X] Alerts 31 | * [ ] Progress bars 32 | * [ ] Hero unit 33 | 34 | ### JS Components 35 | * [ ] Tooltips 36 | * [ ] Popovers 37 | * [ ] Modals 38 | * [ ] Dropdowns 39 | * [ ] Collapse 40 | * [ ] Carousel 41 | 42 | ### Miscellaneous 43 | * [X] Wells 44 | * [X] Close icon 45 | * [X] Utilities 46 | * [ ] Component animations 47 | 48 | ### Responsive 49 | * [X] Visible/hidden classes 50 | * [ ] Narrow tablets and below (<767px) 51 | * [ ] Tablets to desktops (767-979px) 52 | * [ ] Large desktops (>1200px) 53 | * [ ] Responsive navbar 54 | 55 | ## Select jQuery plugins 56 | 57 | * [ ] Transitions (required for any animation) 58 | * [ ] Modals 59 | * [ ] Dropdowns 60 | * [ ] Scrollspy 61 | * [ ] Togglable tabs 62 | * [ ] Tooltips 63 | * [ ] Popovers (requires Tooltips) 64 | * [ ] Affix 65 | * [ ] Alert messages 66 | * [ ] Buttons 67 | * [ ] Collapse 68 | * [ ] Carousel 69 | * [ ] Typeahead 70 | 71 | ## Customize variables 72 | All is default 73 | 74 | ## After download 75 | 76 | * Use only bootstrap/css/bootstrap.min.css file (copy that to ramona/httpfend/static/bootstrap/css/ folder) 77 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | Features 2 | ======== 3 | 4 | - ramonactl command line usage is compatible with init.d scripts (start/stop/restart/status/...) 5 | - Also user can specify 'filtering' of start/stop/restart/status scope by: 6 | ```bash 7 | ./console start program-name1 program-name2 ... 8 | ``` 9 | 10 | - Console can be started without start of server and also server can be started without launch of any program 11 | - Each program is launched in dedicated process group 12 | - configuration is ConfigParser compatible 13 | - priority (order) in which programs are started/stopped 14 | - Following environment variables are available: 15 | - RAMONA_CONFIGS (filled by console, list of config files as given on command line or by default mechanism) 16 | - RAMONA_SECTION (name of [program:?] section in config that is relevant for current program) 17 | 18 | - Force start/restart of programs in 'FATAL' state (-f option) 19 | - Console is able to re-establish connection when server goes down during console run-time 20 | - When ramona server is exiting, it has to try to terminate all childs (using stop command) 21 | - Ramona server terminates after stopping all child programs if console stop command is issued in non-interactive mode 22 | - start command has option -S to launch server only (no program is started ... usable during development) 23 | - @tool support (in methods in console_app class) 24 | - @proxy_tool support (in methods in console_app class) 25 | - @tool can be method or class (class is useful when commandline arguments for tool needs to be used) 26 | - working directory is changed during console start to the location of console app script (should be root of the app) 27 | - immediate/yield modes of start/stop/restart commands 28 | - core dump enabled stop of program 29 | - Automatic (configurable) restart of failed program 30 | - [program:x] command now can contain environment variable reference (e.g. ${HOME}) that will be expanded; also [env] is taken in account 31 | - [program:x]'directory' option (change working directory prior program start) 32 | - [program:x]'umask' option 33 | - optional alternative configuration for environment variables: https://github.com/ateska/ramona/issues/2 34 | 35 | 36 | Console 37 | ------- 38 | - ramona console is embeddable in custom python app + it is extendable to provide similar functionality as 'pan.sh': 39 | ```python 40 | class MyConsoleApp(ramona.console_app): 41 | 42 | @ramona.tool 43 | def unittests(self): 44 | 'Seek for all unit tests and execute them' 45 | import unittest 46 | tl = unittest.TestLoader() 47 | ts = tl.discover('.', '__utest__.py') 48 | 49 | tr = unittest.runner.TextTestRunner(verbosity=2) 50 | res = tr.run(ts) 51 | 52 | return 0 if res.wasSuccessful() else 1 53 | ``` 54 | 55 | Logging 56 | ------- 57 | - logging configuration: 58 | 59 | ```ini 60 | [program:x] 61 | stdin=[] 62 | stdout=[|||FILENAME] 63 | stderr=[|||FILENAME] 64 | ``` 65 | Options: 66 | * <null> (redirect to /dev/null) 67 | * <stderr> (redirect stdout to stderr) 68 | * <stdout> (redirect stderr to stdout) 69 | * <logdir> (file in [server]logdir named [ident]-out.log, [ident]-err.log respectively [ident].log) 70 | 71 | Defaults: 72 | ``` 73 | stdin= 74 | stdout= 75 | stderr= 76 | ``` 77 | 78 | - log location is given as directory by: 79 | 1. [server] logdir option 80 | 2. environment variable LOGDIR 81 | 82 | - (-s/--silent and -d/--debug) command-line options 83 | - tail command 84 | - it works even when output is redirected or null 85 | - log rotate (options logmaxsize and logbackups) [logmaxsize is not hard limit just a trigger for rotate] 86 | - print "STARTING" and "EXITED" banners to log_err 87 | - tail '-f' mode 88 | 89 | Configuration 90 | ------------- 91 | - [program:x] disabled=true options 92 | - section [env] in config defines environment variables (blends them with actual environment vars) 93 | 94 | ```ini 95 | [env] 96 | PYTHONPATH=./libraries 97 | CLASSPATH= 98 | ``` 99 | 100 | Empty variable (e.g. CLASSPATH in previous example) will explicitly remove mentioned environment variable 101 | 102 | - includes in config files: 103 | - primary file is given by -C switch (app. level config) + user application class - both part of user application distribution 104 | - secondary (optional) files is given by -c switch (site level config) + [general]include configuration option 105 | - [general]include format is: =file1.conf:file2.conf::... (default is only) 106 | - [general]include has also 'magic' option that delivers platform specific locations of the config: 107 | - ./site.conf 108 | - [prefix]/etc/[appname].conf (Linux|MacOSX) 109 | - Application name in configuration ([general] appname) 110 | - Some options uses 'magic' values (<magic>) 111 | - [program:x] 'processgroup' switch for using/not-using process group approach (default is on) 112 | 113 | 114 | Mailing to admin 115 | ---------------- 116 | - Scan output streams of the program for keywords (by default 'error', 'fatal', 'exception') and send email when such event occurs 117 | - Config sample (from [program:x]): logscan_stdout=error>now:foo2@bar.com,fatal>now,exception>now,warn>daily 118 | 119 | HTTP frontend 120 | ------------- 121 | - standalone process 122 | - displays states of programs 123 | - allows to start/stop/restart each or all of them 124 | - allows displaying tail of log files 125 | - basic authentication 126 | 127 | Configuration: 128 | - The HTTP frontend is added to configuration file as any other program, only with the special `command=`. 129 | - Configuration sample including comments: 130 | 131 | ```ini 132 | [program:ramonahttpfend] 133 | command= 134 | # IP address/hostname where the HTTP frontend should listen 135 | host=127.0.0.1 136 | # Port where the HTTP frontend should listen 137 | port=5588 138 | # Use username and password options only if you want to enable basic authentication 139 | username=admin 140 | # Can get either plain text or a SHA1 hash, if the password starts with {SHA} prefix 141 | password=pass 142 | # SHA example. To generate use for example: echo -n "secret" | sha1sum 143 | #password={SHA}e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 144 | ``` 145 | 146 | Windows 147 | ------- 148 | - Running as Windows Service 149 | - working on Windows using pyev & Python Win32 150 | -------------------------------------------------------------------------------- /docs/manual/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | -------------------------------------------------------------------------------- /docs/manual/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # Invoke by: LC_ALL=en_US.UTF-8 make html 5 | 6 | # You can set these variables from the command line. 7 | SPHINXOPTS = 8 | SPHINXBUILD = sphinx-build 9 | PAPER = 10 | BUILDDIR = _build 11 | 12 | # Internal variables. 13 | PAPEROPT_a4 = -D latex_paper_size=a4 14 | PAPEROPT_letter = -D latex_paper_size=letter 15 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | # the i18n builder cannot share the environment and doctrees with the others 17 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 18 | 19 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 20 | 21 | help: 22 | @echo "Please use \`make ' where is one of" 23 | @echo " html to make standalone HTML files" 24 | @echo " dirhtml to make HTML files named index.html in directories" 25 | @echo " singlehtml to make a single large HTML file" 26 | @echo " pickle to make pickle files" 27 | @echo " json to make JSON files" 28 | @echo " htmlhelp to make HTML files and a HTML help project" 29 | @echo " qthelp to make HTML files and a qthelp project" 30 | @echo " devhelp to make HTML files and a Devhelp project" 31 | @echo " epub to make an epub" 32 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 33 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " linkcheck to check all external links for integrity" 41 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 42 | 43 | clean: 44 | -rm -rf $(BUILDDIR)/* 45 | 46 | html: 47 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 48 | @echo 49 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 50 | 51 | dirhtml: 52 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 53 | @echo 54 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 55 | 56 | singlehtml: 57 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 58 | @echo 59 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 60 | 61 | pickle: 62 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 63 | @echo 64 | @echo "Build finished; now you can process the pickle files." 65 | 66 | json: 67 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 68 | @echo 69 | @echo "Build finished; now you can process the JSON files." 70 | 71 | htmlhelp: 72 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 73 | @echo 74 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 75 | ".hhp project file in $(BUILDDIR)/htmlhelp." 76 | 77 | qthelp: 78 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 79 | @echo 80 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 81 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 82 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Ramona.qhcp" 83 | @echo "To view the help file:" 84 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Ramona.qhc" 85 | 86 | devhelp: 87 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 88 | @echo 89 | @echo "Build finished." 90 | @echo "To view the help file:" 91 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Ramona" 92 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Ramona" 93 | @echo "# devhelp" 94 | 95 | epub: 96 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 97 | @echo 98 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 99 | 100 | latex: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo 103 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 104 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 105 | "(use \`make latexpdf' here to do that automatically)." 106 | 107 | latexpdf: 108 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 109 | @echo "Running LaTeX files through pdflatex..." 110 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 111 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 112 | 113 | text: 114 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 115 | @echo 116 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 117 | 118 | man: 119 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 120 | @echo 121 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 122 | 123 | texinfo: 124 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 125 | @echo 126 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 127 | @echo "Run \`make' in that directory to run these through makeinfo" \ 128 | "(use \`make info' here to do that automatically)." 129 | 130 | info: 131 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 132 | @echo "Running Texinfo files through makeinfo..." 133 | make -C $(BUILDDIR)/texinfo info 134 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 135 | 136 | gettext: 137 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 138 | @echo 139 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 140 | 141 | changes: 142 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 143 | @echo 144 | @echo "The overview file is in $(BUILDDIR)/changes." 145 | 146 | linkcheck: 147 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 148 | @echo 149 | @echo "Link check complete; look for any errors in the above output " \ 150 | "or in $(BUILDDIR)/linkcheck/output.txt." 151 | 152 | doctest: 153 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 154 | @echo "Testing of doctests in the sources finished, look at the " \ 155 | "results in $(BUILDDIR)/doctest/output.txt." 156 | -------------------------------------------------------------------------------- /docs/manual/_static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/docs/manual/_static/.gitignore -------------------------------------------------------------------------------- /docs/manual/_templates/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/docs/manual/_templates/.gitignore -------------------------------------------------------------------------------- /docs/manual/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | 9 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /docs/manual/cmdline-console.rst: -------------------------------------------------------------------------------- 1 | console (command-line) 2 | ====================== 3 | 4 | Enter interactive console mode. 5 | 6 | .. code-block:: bash 7 | 8 | console [-h] 9 | 10 | -------------------------------------------------------------------------------- /docs/manual/cmdline-restart.rst: -------------------------------------------------------------------------------- 1 | restart (command-line) 2 | ====================== 3 | 4 | Restart supervised program(s) 5 | 6 | 7 | .. code-block:: bash 8 | 9 | restart [-h] [-n] [-i] [-f] [program [program ...]] 10 | 11 | 12 | .. describe:: program 13 | 14 | Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope. 15 | 16 | 17 | .. cmdoption:: restart -n 18 | restart --no-server-start 19 | 20 | Avoid eventual automatic Ramona server start. 21 | This is relevant in case command ``restart`` is issued when Ramona server is not running. 22 | 23 | 24 | .. cmdoption:: restart -i 25 | restart --immediate-return 26 | 27 | Don't wait for restart of programs and return ASAP. 28 | 29 | 30 | .. cmdoption:: restart -f 31 | restart --force-start 32 | 33 | Force restart of programs even if they are in FATAL state. 34 | 35 | .. note:: 36 | On UNIX systems, you can simulate ``restart -f`` command using ``HUP`` signal 37 | (e.g. ``kill -HUP [pid-of-ramona]`` from shell). 38 | -------------------------------------------------------------------------------- /docs/manual/cmdline-server.rst: -------------------------------------------------------------------------------- 1 | server (command-line) 2 | ===================== 3 | 4 | Start the Ramona server in the foreground. 5 | You can use `Ctrl-C` to terminate server interactively. Also you will be able to see output of a Ramona server directly on a terminal. 6 | 7 | .. code-block:: bash 8 | 9 | server [-h] [-S] [program [program ...]] 10 | 11 | 12 | .. describe:: program 13 | 14 | Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope. 15 | 16 | 17 | .. cmdoption:: server -S 18 | server --server-only 19 | 20 | Start only Ramona server, programs are not launched. 21 | -------------------------------------------------------------------------------- /docs/manual/cmdline-start.rst: -------------------------------------------------------------------------------- 1 | start (command-line) 2 | ==================== 3 | 4 | Start supervised program(s). 5 | 6 | If Ramona server is not running, initiate also its start (optionally). 7 | 8 | .. code-block:: bash 9 | 10 | start [-h] [-n] [-i] [-f] [-S] [program [program ...]] 11 | 12 | 13 | .. describe:: program 14 | 15 | Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope. 16 | 17 | 18 | .. cmdoption:: start -n 19 | start --no-server-start 20 | 21 | Avoid eventual automatic start of Ramona server. 22 | 23 | 24 | .. cmdoption:: start -i 25 | start --immediate-return 26 | 27 | Don't wait for start of programs and exit ASAP. 28 | 29 | 30 | .. cmdoption:: start -f 31 | start --force-start 32 | 33 | Force start of programs even if they are in FATAL state. 34 | 35 | 36 | .. cmdoption:: start -S 37 | start --server-only 38 | 39 | Start only server, programs are not started. 40 | -------------------------------------------------------------------------------- /docs/manual/cmdline-status.rst: -------------------------------------------------------------------------------- 1 | status (command-line) 2 | ===================== 3 | 4 | Show status of supervised program(s), respecively show program roaster. 5 | 6 | .. code-block:: bash 7 | 8 | status [-h] [program [program ...]] 9 | 10 | 11 | .. describe:: program 12 | 13 | Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope. 14 | -------------------------------------------------------------------------------- /docs/manual/cmdline-stop.rst: -------------------------------------------------------------------------------- 1 | .. _cmdline-stop: 2 | 3 | stop (command-line) 4 | =================== 5 | 6 | Stop supervised program(s). 7 | 8 | Optionally also terminate Ramona server. 9 | By default, the Ramona server will exit automatically after last supervised program terminates using the 'stop' command. 10 | 11 | .. code-block:: bash 12 | 13 | stop [-h] [-i] [-c] [-E] [-T] [program [program ...]] 14 | 15 | 16 | .. describe:: program 17 | 18 | Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope. 19 | 20 | 21 | .. cmdoption:: stop -i 22 | stop --immediate-return 23 | 24 | Dont wait for termination of programs and exit ASAP. 25 | 26 | 27 | .. cmdoption:: stop -c 28 | stop --core-dump 29 | 30 | Stop program(s) to produce core dump (core dump must be enabled in program configuration). 31 | It is archived by sending signal that lead to dumping of a core file. 32 | 33 | 34 | .. cmdoption:: stop -E 35 | stop --stop-and-exit 36 | 37 | Stop all programs and exit Ramona server. 38 | This is a default behaviour of the ``stop`` command. 39 | 40 | 41 | .. cmdoption:: stop -S 42 | stop --stop-and-stay 43 | 44 | Stop all programs but keep Ramona server running. 45 | -------------------------------------------------------------------------------- /docs/manual/cmdline-tail.rst: -------------------------------------------------------------------------------- 1 | tail (command-line) 2 | =================== 3 | 4 | Display the last part of a log (standard output and/or standard error) of specified program. 5 | 6 | .. code-block:: bash 7 | 8 | tail [-h] [-l {stdout,stderr}] [-f] [-n N] program 9 | 10 | 11 | .. describe:: program 12 | 13 | Specify the program in scope of the command. 14 | 15 | 16 | .. cmdoption:: tail -l {stdout,stderr} 17 | tail --log-stream {stdout,stderr} 18 | 19 | Specify which standard stream to use. 20 | Default is ``stderr``. 21 | 22 | 23 | .. cmdoption:: tail -f 24 | tail --follow 25 | 26 | Causes tail command to not stop when end of stream is reached, but rather to wait for additional data to be appended to the input. 27 | 28 | 29 | .. cmdoption:: tail -n N 30 | tail --lines N 31 | 32 | Output the last N lines, instead of the last 40. 33 | 34 | -------------------------------------------------------------------------------- /docs/manual/cmdline-who.rst: -------------------------------------------------------------------------------- 1 | who (command-line) 2 | ================== 3 | 4 | Show *who* is connected to the Ramona server. 5 | 6 | Ramona server allows multiple clients to be connected at the same time. This command allows users to check who is connected to given Ramona server. 7 | 8 | .. code-block:: bash 9 | 10 | who [-h] 11 | 12 | 13 | Output example: 14 | :: 15 | 16 | Connected clients: 17 | *UNIX /tmp/ramona-dev.sock @ 21-08-2013 19:49:10 18 | TCP 127.0.0.1:53211 @ 21-08-2013 19:55:22 19 | TCP [::1]:53211 @ 21-08-2013 18:35:16 20 | -------------------------------------------------------------------------------- /docs/manual/cmdline-wininstall.rst: -------------------------------------------------------------------------------- 1 | .. _cmdline-wininstall: 2 | 3 | wininstall (command-line) 4 | ========================= 5 | 6 | .. warning:: 7 | 8 | Windows only ! 9 | 10 | Install the Ramona server as a Windows service. 11 | 12 | An user application will be able to run as Windows service using this Ramona feature. 13 | For more details, see :ref:`features-windowsservice`. 14 | 15 | .. code-block:: bash 16 | 17 | wininstall [-h] [-d] [-S] [program [program ...]] 18 | 19 | 20 | .. describe:: program 21 | 22 | Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope. 23 | Programs in scope will be started when this Ramona Windows Service is started by OS. 24 | 25 | 26 | .. cmdoption:: wininstall -S 27 | wininstall --server-only 28 | 29 | When service is acticated start only the Ramona server, not supervised programs. 30 | 31 | 32 | .. cmdoption:: wininstall -d 33 | wininstall --dont-start 34 | 35 | Don't start Windows service (Ramona server) after an installation. 36 | -------------------------------------------------------------------------------- /docs/manual/cmdline-winuninstall.rst: -------------------------------------------------------------------------------- 1 | .. _cmdline-winuninstall: 2 | 3 | winuninstall (command-line) 4 | =========================== 5 | 6 | .. warning:: 7 | 8 | Windows only ! 9 | 10 | Uninstall the Ramona Windows service. 11 | 12 | For more details, see :ref:`features-windowsservice`. 13 | 14 | .. code-block:: bash 15 | 16 | winuninstall [-h] 17 | 18 | -------------------------------------------------------------------------------- /docs/manual/cmdline.rst: -------------------------------------------------------------------------------- 1 | Command line console 2 | ==================== 3 | 4 | Command-line console is a basic tool for interaction with Ramona-equipped application. 5 | 6 | User can issue commands to Ramona server thru this tool or execute any of custom tools. 7 | 8 | 9 | Generic options 10 | --------------- 11 | 12 | Generic options should be given prior actual command on the command line. 13 | 14 | .. cmdoption:: -c CONFIGFILE 15 | --config CONFIGFILE 16 | 17 | Specify configuration file(s) to read (this option can be given more times). This will override build-in application-level configuration. 18 | 19 | 20 | .. cmdoption:: -h 21 | --help 22 | 23 | Displays build-in help. 24 | 25 | 26 | .. cmdoption:: -d 27 | --debug 28 | 29 | Enable debug (verbose) output. 30 | 31 | 32 | .. cmdoption:: -s 33 | --silent 34 | 35 | Enable silent mode of operation (only errors are printed). 36 | 37 | 38 | Common commands 39 | --------------- 40 | 41 | .. toctree:: 42 | cmdline-start.rst 43 | cmdline-stop.rst 44 | cmdline-restart.rst 45 | cmdline-status.rst 46 | cmdline-tail.rst 47 | cmdline-console.rst 48 | cmdline-server.rst 49 | cmdline-who.rst 50 | cmdline-wininstall.rst 51 | cmdline-winuninstall.rst 52 | -------------------------------------------------------------------------------- /docs/manual/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Ramona documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 15 19:55:30 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Ramona' 44 | copyright = u'2012-2013, Ales Teska' 45 | 46 | # The short X.Y version. 47 | version = '1.1' 48 | 49 | # The full version, including alpha/beta/rc tags. 50 | release = '1.1b1' 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | #language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | #today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | #today_fmt = '%B %d, %Y' 61 | 62 | # List of patterns, relative to source directory, that match files and 63 | # directories to ignore when looking for source files. 64 | exclude_patterns = ['_build'] 65 | 66 | # The reST default role (used for this markup: `text`) to use for all documents. 67 | #default_role = None 68 | 69 | # If true, '()' will be appended to :func: etc. cross-reference text. 70 | #add_function_parentheses = True 71 | 72 | # If true, the current module name will be prepended to all description 73 | # unit titles (such as .. function::). 74 | #add_module_names = True 75 | 76 | # If true, sectionauthor and moduleauthor directives will be shown in the 77 | # output. They are ignored by default. 78 | #show_authors = False 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = 'sphinx' 82 | 83 | # A list of ignored prefixes for module index sorting. 84 | #modindex_common_prefix = [] 85 | 86 | 87 | # -- Options for HTML output --------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | html_theme = 'default' 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | #html_theme_options = {} 97 | 98 | # Add any paths that contain custom themes here, relative to this directory. 99 | #html_theme_path = [] 100 | 101 | # The name for this set of Sphinx documents. If None, it defaults to 102 | # " v documentation". 103 | #html_title = None 104 | 105 | # A shorter title for the navigation bar. Default is the same as html_title. 106 | #html_short_title = None 107 | 108 | # The name of an image file (relative to this directory) to place at the top 109 | # of the sidebar. 110 | #html_logo = None 111 | 112 | # The name of an image file (within the _static path) to use as favicon of the 113 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 114 | # pixels large. 115 | #html_favicon = None 116 | 117 | # Add any paths that contain custom static files (such as style sheets) here, 118 | # relative to this directory. They are copied after the builtin static files, 119 | # so a file named "default.css" will overwrite the builtin "default.css". 120 | html_static_path = ['_static'] 121 | 122 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 123 | # using the given strftime format. 124 | #html_last_updated_fmt = '%b %d, %Y' 125 | 126 | # If true, SmartyPants will be used to convert quotes and dashes to 127 | # typographically correct entities. 128 | #html_use_smartypants = True 129 | 130 | # Custom sidebar templates, maps document names to template names. 131 | #html_sidebars = {} 132 | 133 | # Additional templates that should be rendered to pages, maps page names to 134 | # template names. 135 | #html_additional_pages = {} 136 | 137 | # If false, no module index is generated. 138 | #html_domain_indices = True 139 | 140 | # If false, no index is generated. 141 | #html_use_index = True 142 | 143 | # If true, the index is split into individual pages for each letter. 144 | #html_split_index = False 145 | 146 | # If true, links to the reST sources are added to the pages. 147 | #html_show_sourcelink = True 148 | 149 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 150 | #html_show_sphinx = True 151 | 152 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 153 | #html_show_copyright = True 154 | 155 | # If true, an OpenSearch description file will be output, and all pages will 156 | # contain a tag referring to it. The value of this option must be the 157 | # base URL from which the finished HTML is served. 158 | #html_use_opensearch = '' 159 | 160 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 161 | #html_file_suffix = None 162 | 163 | # Output file base name for HTML help builder. 164 | htmlhelp_basename = 'Ramonadoc' 165 | 166 | 167 | # -- Options for LaTeX output -------------------------------------------------- 168 | 169 | latex_elements = { 170 | # The paper size ('letterpaper' or 'a4paper'). 171 | #'papersize': 'letterpaper', 172 | 173 | # The font size ('10pt', '11pt' or '12pt'). 174 | #'pointsize': '10pt', 175 | 176 | # Additional stuff for the LaTeX preamble. 177 | #'preamble': '', 178 | } 179 | 180 | # Grouping the document tree into LaTeX files. List of tuples 181 | # (source start file, target name, title, author, documentclass [howto/manual]). 182 | latex_documents = [ 183 | ('index', 'Ramona.tex', u'Ramona Documentation', 184 | u'Ales Teska', 'manual'), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | #latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | #latex_use_parts = False 194 | 195 | # If true, show page references after internal links. 196 | #latex_show_pagerefs = False 197 | 198 | # If true, show URL addresses after external links. 199 | #latex_show_urls = False 200 | 201 | # Documents to append as an appendix to all manuals. 202 | #latex_appendices = [] 203 | 204 | # If false, no module index is generated. 205 | #latex_domain_indices = True 206 | 207 | 208 | # -- Options for manual page output -------------------------------------------- 209 | 210 | # One entry per manual page. List of tuples 211 | # (source start file, name, description, authors, manual section). 212 | man_pages = [ 213 | ('index', 'ramona', u'Ramona Documentation', 214 | [u'Ales Teska'], 1) 215 | ] 216 | 217 | # If true, show URL addresses after external links. 218 | #man_show_urls = False 219 | 220 | 221 | # -- Options for Texinfo output ------------------------------------------------ 222 | 223 | # Grouping the document tree into Texinfo files. List of tuples 224 | # (source start file, target name, title, author, 225 | # dir menu entry, description, category) 226 | texinfo_documents = [ 227 | ('index', 'Ramona', u'Ramona Documentation', 228 | u'Ales Teska', 'Ramona', 'One line description of project.', 229 | 'Miscellaneous'), 230 | ] 231 | 232 | # Documents to append as an appendix to all manuals. 233 | #texinfo_appendices = [] 234 | 235 | # If false, no module index is generated. 236 | #texinfo_domain_indices = True 237 | 238 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 239 | #texinfo_show_urls = 'footnote' 240 | -------------------------------------------------------------------------------- /docs/manual/credits.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | 5 | Project links 6 | ------------- 7 | 8 | - Homepage_ 9 | - `Source code`_ @ GitHub 10 | - `Binaries`_ @ PyPi (Python Package Index) 11 | - `Documentation`_ 12 | - `Mailing list`_ 13 | - `Bug tracker`_ 14 | - `Summary`_ @ Ohloh 15 | 16 | .. _`Homepage`: http://ateska.github.com/ramona/ 17 | .. _`Source code`: https://github.com/ateska/ramona 18 | .. _`Binaries`: http://pypi.python.org/pypi/ramona 19 | .. _`Summary`: https://www.ohloh.net/p/ateska_ramona 20 | .. _`Bug tracker`: https://github.com/ateska/ramona/issues 21 | .. _`Documentation`: http://ateska.github.com/ramona/manual 22 | .. _`Mailing list`: https://groups.google.com/forum/#!forum/ramona-supervisor 23 | 24 | 25 | Contributors 26 | ------------ 27 | 28 | - `Ales Teska`_: Main developer 29 | - `Jan Stastny`_: Web console 30 | 31 | .. _`Ales Teska`: https://github.com/ateska 32 | .. _`Jan Stastny`: https://github.com/jstastny 33 | 34 | 35 | Inspiration 36 | ----------- 37 | 38 | Ramona is stronly influenced and inspired by supervisord_, another supervising tool. 39 | 40 | .. _supervisord: http://supervisord.org/ 41 | 42 | 43 | License 44 | ------- 45 | 46 | | Copyright (c) 2012, Ales Teska 47 | | All rights reserved. 48 | 49 | Redistribution and use in source and binary forms, with or without 50 | modification, are permitted provided that the following conditions are met: 51 | 52 | 1. Redistributions of source code must retain the above copyright notice, this 53 | list of conditions and the following disclaimer. 54 | 2. Redistributions in binary form must reproduce the above copyright notice, 55 | this list of conditions and the following disclaimer in the documentation 56 | and/or other materials provided with the distribution. 57 | 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 59 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 60 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 61 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 62 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 63 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 64 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 65 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 66 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 67 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 68 | 69 | .. note:: 70 | 71 | This is the `BSD 2-Clause License`_. 72 | 73 | .. _`BSD 2-Clause License`: http://opensource.org/licenses/bsd-license.php 74 | 75 | 76 | Documentation licence 77 | --------------------- 78 | 79 | Ramona Documentation is available under `Creative Commons Attribution-ShareAlike 3.0 Unported license`_ conditions. 80 | 81 | .. _`Creative Commons Attribution-ShareAlike 3.0 Unported license`: http://creativecommons.org/licenses/by-sa/3.0/ 82 | 83 | .. note:: 84 | 85 | Ramona Documentation uses texts from Wikipedia_. 86 | 87 | .. _Wikipedia: http://wikipedia.org/ 88 | 89 | -------------------------------------------------------------------------------- /docs/manual/features.rst: -------------------------------------------------------------------------------- 1 | 2 | Features 3 | ======== 4 | 5 | 6 | .. image:: img/execmodel.png 7 | 8 | 9 | Program and program roaster 10 | --------------------------- 11 | 12 | Ramona maintains pre-configured set of programs; these programs are placed in a 'roaster', list that is managed by Ramona server. 13 | Each program in this roaster has a status that reflects its current phase of life cycle. Ramona is responsible for supervising of these programs in terms of their uptime, eventual restarts, logging etc. 14 | 15 | List of program statuses 16 | ^^^^^^^^^^^^^^^^^^^^^^^^ 17 | * DISABLED - program is disabled by configuration; Ramona will not launch this program at any condition. 18 | * STOPPED - program is stopped (not running); you can launch it by 'start' command. 19 | * STARTING - program has been just launched. 20 | * RUNNING - program is running for some time already. 21 | * STOPPING - program has been asked to terminate but it has not exited yet. 22 | * FATAL - program exited in a errorneous way (maybe several times in row) and Ramona evaluated this as non-recoverable error. 23 | * CFGERROR - program is incorrectly configured and cannot be launched. 24 | 25 | .. image:: img/progstates.png 26 | 27 | 28 | Command-line console 29 | -------------------- 30 | 31 | Ramona provides command-line console, a tool that allows interaction with Ramona server and thru this controlling of all application programs. This tool can be tighly integrated with application that uses Ramona and it is designed to represent 'single point of execution' of given application. 32 | 33 | This approach simplifies maintenance of the application and allow easy operating of even complex applications consisting of many different programs. 34 | 35 | User can also add their custom commands (see `custom tools`_) to cover all needs of its application. 36 | 37 | 38 | .. _features-logging: 39 | 40 | Logging and log scanning 41 | ------------------------ 42 | 43 | Ramona monitors `standard output`_ and `standard error`_ streams of all its supervisored programs. These streams are persisted to files on a filesystem in a highly configurable way. It is a primary way of how Ramona approaches logging so programs are advised to log using standard streams instead on logging into log files. It also enables Ramona to capture any other textual output of the program that may not be captured by typical logging mechanism like unexpected kills or top-level exceptions. 44 | 45 | .. _`standard output`: http://en.wikipedia.org/wiki/Standard_streams 46 | .. _`standard error`: http://en.wikipedia.org/wiki/Standard_streams 47 | 48 | Ramona also allows to configure scanner that seeks thru log streams for given patterns and if such a pattern is found, then Ramona notifies about such an event via email. 49 | 50 | 51 | Custom tools 52 | ------------ 53 | 54 | Ramona can be easily extended by custom commands; these are implemented in Python and enables cross-platform automation of common tasks connected with the application. 55 | 56 | 57 | Example of tool function: 58 | 59 | .. code-block:: python 60 | 61 | class FooConsoleApp(ramona.console_app): 62 | 63 | @ramona.tool 64 | def mytool(self): 65 | '''This is help text of my tool''' 66 | ... 67 | 68 | 69 | Example of tool class: 70 | 71 | .. code-block:: python 72 | 73 | class FooConsoleApp(ramona.console_app): 74 | 75 | @ramona.tool 76 | class mytool(object): 77 | '''This is help text of my tool''' 78 | 79 | def init_parser(self, cnsapp, parser): 80 | parser.description = '...' 81 | parser.add_argument(...) 82 | 83 | def main(self, cnsapp, args): 84 | ... 85 | 86 | .. note:: 87 | This manual is actually build using this feature, by executing ``./ramona.py manual``. 88 | 89 | 90 | Environment variables 91 | --------------------- 92 | 93 | Ramona sets following environment variables to propagate certain information to programs, that are launched as Ramona subprocesses. 94 | This allows exchange of configuration information in a control way, helping to keep overall configuration nice and tidy. 95 | 96 | .. attribute:: RAMONA_CONFIG 97 | 98 | This environment variable specifies list of configuration files that has been used to configure Ramona server. 99 | List is ordered (configuration values can overlap so correct override behaviour needs to be maintained) and its separator is ':' for POSIX or ';' for Windows. See ``os.pathsep`` in Python. 100 | 101 | Client application can use this variable to read configuration from same place(s) as Ramona did. 102 | 103 | 104 | .. attribute:: RAMONA_CONFIG_WINC 105 | 106 | Content and format is the same as in :attr:`RAMONA_CONFIG` however this one contains also expanded includes (see ``[general]`` :attr:`include` configuration option). The order of loading is kept correctly. 107 | 108 | 109 | .. attribute:: RAMONA_SECTION 110 | 111 | This environment variable reflect name of section in Ramona configuration files that in relevant for actual program (subprocess of Ramona). Uses can use this value to reach program specific configuration options. 112 | 113 | 114 | Example: 115 | 116 | .. code-block:: ini 117 | 118 | [program:envdump] 119 | command=bash -c "echo RAMONA_CONFIG: ${RAMONA_CONFIG}; echo RAMONA_SECTION: ${RAMONA_SECTION}" 120 | 121 | 122 | This produces following output: 123 | 124 | .. code-block:: console 125 | 126 | RAMONA_CONFIG: ./test.conf 127 | RAMONA_SECTION: program:envdump 128 | 129 | .. note:: 130 | 131 | Configuration files are compatible with Python Standart Library ``ConfigParser`` module. 132 | You can read configuration files using this module in order given by ``RAMONA_CONFIG`` environment variable and access configuration values. You can use ``RAMONA_SECTION`` environment variable to identify section in configuration files that is relevant to your actual program. 133 | 134 | 135 | Web console 136 | ----------- 137 | 138 | .. image:: img/httpfend.png 139 | :width: 600px 140 | 141 | Displays states of supervised programs using web browser. It also allows user to start/stop/restart each or all of them or retrieve recent standart output and/or standard error of each program. 142 | 143 | 144 | .. _`features-windowsservice`: 145 | 146 | Windows service 147 | --------------- 148 | 149 | Ramona is using `Windows Services`_ for background execution on Windows platform. 150 | It also depends on ``pythonservice.exe`` tool from `Python for Windows extensions`_ package. Therefore it is possible to install Ramona equipped application as Windows service via commands that are provided by Ramona system. This can be used for automatic start-up after system (re)boot or to enable smooth development on Windows machine. 151 | 152 | You can have multiple Ramona Windows services installed on a box; for example for different Ramona-equipped applications or versions. 153 | 154 | For more details continue to: 155 | 156 | - :ref:`cmdline-wininstall` 157 | - :ref:`cmdline-winuninstall` 158 | 159 | .. _`Windows services`: http://en.wikipedia.org/wiki/Windows_service 160 | .. _`Python for Windows extensions`: http://sourceforge.net/projects/pywin32/ 161 | -------------------------------------------------------------------------------- /docs/manual/img/execmodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/docs/manual/img/execmodel.png -------------------------------------------------------------------------------- /docs/manual/img/httpfend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/docs/manual/img/httpfend.png -------------------------------------------------------------------------------- /docs/manual/img/progstates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/docs/manual/img/progstates.png -------------------------------------------------------------------------------- /docs/manual/index.rst: -------------------------------------------------------------------------------- 1 | .. Ramona documentation master file, created by 2 | sphinx-quickstart on Sat Sep 15 19:55:30 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Ramona Documentation 7 | ==================== 8 | 9 | Ramona is an enterprise-grade **runtime supervisor** that allows controlling and monitoring software programs during their execution life cycle. 10 | 11 | It provides supervisor/console functionality of init.d-like start/stop control, continuous integration (e.g. unit/functional/performance test launcher), deployment automation and other command-line oriented features. It is design the way that you should be able to extend that easily if needed (e.g. to include your own commands or tasks). 12 | 13 | It is implemented in Python but it is not limited to be used only in Python projects. 14 | 15 | Target platforms are all modern UNIXes, BSD derivates and Windows. 16 | 17 | Project homepage: http://ateska.github.com/ramona 18 | 19 | 20 | This product is founded on Gittip. To help us keep development of this tool sustainable, please kindly consider your contribution. 21 | 22 | .. raw:: html 23 | 24 | 25 | 26 | 27 | Content 28 | ------- 29 | 30 | .. toctree:: 31 | :maxdepth: 3 32 | 33 | intro.rst 34 | install.rst 35 | features.rst 36 | config.rst 37 | cmdline.rst 38 | nondeamon.rst 39 | tools.rst 40 | credits.rst 41 | 42 | 43 | Indices and tables 44 | ================== 45 | 46 | * :ref:`genindex` 47 | * :ref:`search` 48 | 49 | .. * :ref:`modindex` 50 | -------------------------------------------------------------------------------- /docs/manual/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Prerequisites 5 | ------------- 6 | 7 | - Python 2.7+ (currently not compatible with Python 3) 8 | - pyev_ (install via ``pip install pyev`` or ``easy_install pyev``) 9 | 10 | .. _pyev: http://pypi.python.org/pypi/pyev 11 | 12 | 13 | Installation using **pip** 14 | -------------------------- 15 | 16 | .. code-block:: bash 17 | 18 | pip install ramona 19 | 20 | 21 | Installation using **easy_install** 22 | ----------------------------------- 23 | 24 | .. code-block:: bash 25 | 26 | easy_install ramona 27 | 28 | Manual installation 29 | ------------------- 30 | 31 | .. _Pypi: http://pypi.python.org/pypi/ramona 32 | 33 | 1. Download ramona*.zip or ramona*.tar.gz from PyPi_. 34 | 2. Unpack downloaded archive into empty directory 35 | 3. Open command-line interface (shell, cmd.exe) and go to the unpacked directory 36 | 4. Execute following command: ``python setup.py install`` 37 | 38 | Inclusion of Ramona code into your project 39 | ------------------------------------------ 40 | 41 | Alternatively you can include Ramona source code folder directly into your project, effectively removing an external dependency. 42 | 43 | 1. Download ramona*.zip or ramona*.tar.gz from PyPi_. 44 | 2. Unpack downloaded archive into empty directory 45 | 3. Copy ``ramona`` subdirectory into your project directory root. 46 | 47 | Target directory structure for project called *foo* looks as follow:: 48 | 49 | foo/ 50 | bin/ 51 | share/ 52 | src/ 53 | docs/ 54 | ramona/ 55 | foo.py 56 | foo.conf 57 | 58 | -------------------------------------------------------------------------------- /docs/manual/intro.rst: -------------------------------------------------------------------------------- 1 | 2 | Introduction 3 | ============ 4 | 5 | Ramona is runtime supervisor: component of a software product that takes care of smooth start and stop of the solution (including daemonization), of it's runtime monitoring (logging, unexpected exits, etc.) and of various tasks that are connected with project like continuous integration, unit test automation, documentation builds etc. 6 | 7 | Full set of features is described here_. 8 | 9 | .. _here: features.html 10 | 11 | 12 | Integration with your project 13 | ----------------------------- 14 | 15 | Assuming you have successfully installed_ Ramona, you can start integrating it with your project. 16 | 17 | .. _installed: install.html 18 | 19 | You have to provide two files: **supervisor launcher** and its **configuration**. 20 | 21 | Supervisor launcher 22 | ################### 23 | 24 | Supervisor launcher is small piece of Python code, that is actually executable by user. 25 | 26 | Assuming your project is called *foo*, you need to create file ``foo.py`` with following content (just copy&paste it). 27 | 28 | .. code-block:: python 29 | 30 | #!/usr/bin/env python 31 | import ramona 32 | 33 | class FooConsoleApp(ramona.console_app): 34 | pass 35 | 36 | if __name__ == '__main__': 37 | app = FooConsoleApp(configuration='./foo.conf') 38 | app.run() 39 | 40 | Make sure that it is marked as executable (e.g. by ``chmod a+x ./foo.py`` on UNIX platform). 41 | 42 | .. note:: 43 | 44 | It is important to use correct version of Python interpreter with Ramona. Some systems happens to have multiple versions installed, so PATH environment variable should be correctly set for relevant user(s) to point to proper Python. 45 | 46 | 47 | Configuration 48 | ############# 49 | 50 | You also need to create application-level configuration file which will instruct Ramona what to do. 51 | 52 | Create file named ``foo.conf`` (actually referenced from ``foo.py`` you just created - you are free to change name based on your preferences). 53 | 54 | Content of the file is as follows:: 55 | 56 | [general] 57 | appname=foo 58 | 59 | [program:appserver] 60 | command=[command-to-start-your-app] 61 | 62 | 63 | ``[command-to-start-your-app]`` is command that your project uses to start. 64 | 65 | You can entry ``[program:x]`` section more times - for each 'long running' component of your project. 66 | 67 | 68 | Basic usage 69 | ----------- 70 | 71 | Ramona provides build-in help system. 72 | 73 | .. code-block:: bash 74 | 75 | $ ./foo.py --help 76 | usage: foo.py [-h] [-c CONFIGFILE] [-d] [-s] 77 | {start,stop,restart,status,help,tail,console,server} ... 78 | 79 | optional arguments: 80 | -h, --help show this help message and exit 81 | -c CONFIGFILE, --config CONFIGFILE 82 | Specify configuration file(s) to read (this option can 83 | be given more times). This will override build-in 84 | application level configuration. 85 | -d, --debug Enable debug (verbose) output. 86 | -s, --silent Enable silent mode of operation (only errors are 87 | printed). 88 | 89 | subcommands: 90 | {start,stop,restart,status,help,tail,console,server} 91 | start Launch subprocess(es) 92 | stop Terminate subprocess(es) 93 | restart Restart subprocess(es) 94 | status Show status of subprocess(es) 95 | help Display help 96 | tail Tail log of specified program 97 | console Enter interactive console mode 98 | server Launch server in the foreground 99 | 100 | Start of your application:: 101 | 102 | $ ./foo.py start 103 | 104 | 105 | Stop of your application:: 106 | 107 | $ ./foo.py stop 108 | 109 | -------------------------------------------------------------------------------- /docs/manual/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Ramona.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Ramona.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/manual/nondeamon.rst: -------------------------------------------------------------------------------- 1 | .. _nondaemon: 2 | 3 | Nondaemonizing of Programs 4 | ========================== 5 | 6 | Supervised programs should not daemonize_ themselves. Instead, they should run in the foreground. 7 | 8 | You need to consult the manual, the man page, or check other help resources to find out how to disable the eventual daemonizing of the program which you want to control with Ramona. You can check the actual behavior of a program by launching it from the command-line (e.g. bash). The program should not detach from the console, after launching the program you need to press Ctrl-C to get back to the shell prompt. In such a case, the program is configured correctly for being used with Ramona. Otherwise, if the program detaches, you will get a shell prompt without any further action, then program very likely has daemonized or send to the background and it will not operated with Ramona properly. 9 | 10 | Daemonizing of a program will basically break the connection between Ramona and the relevant program. Ramona will likely mark the program being in ``FATAL`` state (maybe even after several attempts to launch it) and certainly Ramona will not control it as a deamonized process (e.g. you will not be able to terminate it using Ramona tools). 11 | 12 | .. _daemonize: http://en.wikipedia.org/wiki/Daemon_(computing) 13 | 14 | 15 | Examples of Program Configurations 16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 17 | 18 | In this section you find some "real world" program configuration examples: 19 | 20 | 21 | MongoDB 22 | +++++++ 23 | .. code-block:: ini 24 | 25 | [program:mongodb] 26 | command=/path/to/bin/mongod -f /path/to/mongodb.conf 27 | 28 | 29 | RabbitMQ 30 | ++++++++ 31 | .. code-block:: ini 32 | 33 | [program:rabbitmq] 34 | command=/path/to/rabbitmq/bin/rabbitmq-server 35 | 36 | 37 | Apache 2.x 38 | ++++++++++ 39 | .. code-block:: ini 40 | 41 | [program:apache2] 42 | command=/path/to/httpd -c "ErrorLog /dev/stdout" -DFOREGROUND 43 | 44 | Lighttpd 45 | ++++++++ 46 | .. code-block:: ini 47 | 48 | [program:lighttpd] 49 | command=/path/to/lighttpd -D -f /path/to/lighttpd.conf 50 | -------------------------------------------------------------------------------- /docs/manual/tools.rst: -------------------------------------------------------------------------------- 1 | Tools Cookbook 2 | ============== 3 | 4 | .. [TODO]: Example of init.d script (and modern alternatives like init) for ramona-based app 5 | 6 | .. [TODO]: Using Ramona to execute unit tests 7 | 8 | 9 | .. _smtp-configs: 10 | 11 | SMTP configurations 12 | ------------------- 13 | 14 | Here is a list of common SMTP configurations usable with Ramona email notification delivery subsystem (see :attr:`delivery` in [ramona:notify] configuration section). 15 | 16 | 17 | GMail 18 | ````` 19 | GMail from Google. 20 | 21 | .. code-block:: ini 22 | 23 | [ramona:notify] 24 | delivery=smtp://[user@gmail.com]:[password]@smtp.gmail.com:587/?tls=1 25 | 26 | Credentials 'user' and 'password' need to be valid Google GMail account. 27 | 28 | Example: 29 | 30 | .. code-block:: ini 31 | 32 | [ramona:notify] 33 | delivery=smtp://joe.doe@gmail.com:password123@smtp.gmail.com:587/?tls=1 34 | 35 | 36 | Mandrill 37 | ```````` 38 | Transactional Email from MailChimp. 39 | 40 | .. code-block:: ini 41 | 42 | [ramona:notify] 43 | delivery=smtp://[user]:[API key]@smtp.mandrillapp.com:587 44 | 45 | 46 | Credentials are from "SMTP & API Credentials" section in Mandrill settings. 47 | User is "SMTP Username" respectively your account name at Mandrill. 48 | 49 | API key can be created and obtained from this page in Mandrill settings too, if you don't have any, you can create one by pressing 'New API key' button. 50 | 51 | Example: 52 | 53 | .. code-block:: ini 54 | 55 | [ramona:notify] 56 | delivery=smtp://joe.doe@example.com:WJWZcoAaEQjggzVG1Y@smtp.mandrillapp.com:587 57 | 58 | .. note:: 59 | 60 | It seems that Mandrill is somehow sensitive to :attr:`sender` configuration; if it is not properly formulated, Mandill silently ignores an email. 61 | 62 | 63 | Windows service control 64 | ----------------------- 65 | 66 | Few generic advices how to manage Windows service manually. 67 | Ramona system provides :ref:`cmdline-wininstall` and :ref:`cmdline-winuninstall` that are equivalents but generic way can sometimes become handy. 68 | 69 | To start service: 70 | 71 | .. code-block:: bash 72 | 73 | $ net start 74 | 75 | 76 | To stop service: 77 | 78 | .. code-block:: bash 79 | 80 | $ net stop 81 | 82 | 83 | To uninstall service: 84 | 85 | .. code-block:: bash 86 | 87 | $ sc delete 88 | -------------------------------------------------------------------------------- /docs/release-proc.md: -------------------------------------------------------------------------------- 1 | Release procedure 2 | ================= 3 | 4 | 1. Make sure master branch (or relevant originating branch) is stable and releasable 5 | 2. Formulate release version 'string' (e.g. 0.9b1) -> use it instead placeholder [VERSION] bellow 6 | 3. git checkout -b release-[VERSION-MASTER.VERSION-MINOR] master (git checkout -b release-0.9 master) 7 | or if merging to existing release branch, perform switch to that release branch and merge from master. 8 | DO NOT COMMIT YET! 9 | 4. Now we are working in the release branch 10 | 5. Check `./setup.py`: 11 | - version info 12 | - classifiers (e.g. Development Status) 13 | 6. Check `./ramona/__init__.py`: 14 | - version info 15 | 7. Check `./docs/manual/conf.py`: 16 | - version info (short and long) 17 | 8. Check briefly `./README.md` and `./README` 18 | 9. Check ./MANIFEST.in 19 | 10. Run tests: 20 | - `./ramona-dev.py unittests` 21 | - Functional test: 22 | - `./demo.py start` 23 | - `./demo.py status` 24 | - `./demo.py stop` 25 | 11. Run upload to testpypi.python.org: `./ramona-dev.py upload_test` 26 | 12. Check on http://testpypi.python.org/pypi/ramona 27 | 13. Prepare for final release ! 28 | 14. Commit to Git 29 | 15. Run upload to pypi.python.org: `./ramona-dev.py upload` 30 | 16. Check on http://pypi.python.org/pypi/ramona 31 | 17. Create tag 'release-[VERSION]' (e.g. release-0.9b3) with comment e.g. "Beta release 0.9b3" 32 | 18. Switch back to 'master' branch and you are done 33 | 34 | Follow-ups 35 | ---------- 36 | - Update manual (documentation) and propagate that to github pages 37 | - Mark release in Google Analytics 38 | - Publish release to Freecode 39 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | TODO list 2 | ========= 3 | 4 | Generic 5 | ------- 6 | - add version check to console-server communication handshake (and print warning if not matching) 7 | - exitcodes option for autorestart (autorestart=1,2,3) 8 | - (low prio): SSL (optional) for protecting console-server channel 9 | - ulimit/resources (similar to core dump) -> minfds, minprocs 10 | - Unify & document sys.exit codes 11 | - Reload/reset command (restarting ramona server) 12 | - Restart in yield mode should also terminate & start ramona server 13 | - [tool:x] support (how to do this properly - config is read __after__ arguments are parsed) 14 | - console command enable/disable to allow change status during runtime 15 | - [program:x] disabled 'magic' options: 16 | - e.g. 17 | - test Ramona how it runs in out-of-diskspace conditions 18 | - 'user' option - If ramona runs as root, this UNIX user account will be used as the account which runs the program. If ramona is not running as root, this option has no effect. 19 | - configuration platform selector should support OR operator (e.g. pidfile@linux|darwin) 20 | - configuration platform selector should support families (e.g. pidfile@posix); posix is so far only identified family (expanded to linux|darwin|cygwin) 21 | - `tail -f *` to show log of **ALL** running programs 22 | - Add support for [var] section - similar to [env] but in this case, values are not propagated into environment variables 23 | Otherwise it remains complementary (useful for ${VAR} expressions on command-line). 24 | Maybe this can be archived in a different (more elegant) way (e.g. not introduce [var]/[env] duality). 25 | - Better error message when directory of socket file does not exist: 26 | Current message when server starts: 2013-01-04 01:09:17,846 CRITICAL: It looks like that server is already running: [Errno 2] No such file or directory 27 | - Explore eventuality of forking TTY when launching program: http://stackoverflow.com/questions/1922254/python-when-to-use-pty-fork-versus-os-fork 28 | 29 | Logging 30 | ------- 31 | - Support for SIGHUP (reopen log files OR reset fully) 32 | - log rotate of Ramona server log (stdout/stderr redirection) 33 | 34 | Configuration 35 | ------------- 36 | - (environment) variables expansion in configuration 37 | 38 | Watchdog 39 | -------- 40 | - watchdog functionality (child process is signaling that is alive periodically) 41 | - watchdog for non-managed programs (e.g. [watchdog:apache]) + restart commands 42 | 43 | Python specific 44 | --------------- 45 | - native python program execution (using utils.get_python_exec - substitute for STRIGAPYTHON) 46 | - python version (minimal) check 47 | 48 | Mailing to admin 49 | ---------------- 50 | - On autorestart mail trigger 51 | - On FATAL mail trigger 52 | - Mailing issues to admin: https://github.com/ateska/ramona/issues/1 53 | - Standalone log scanner (not connected to particular program) to enable supervising of e.g. CGI scripts 54 | - daily/weekly/monthly targets 55 | 56 | HTTP frontend 57 | ------------- 58 | - Store static files in a way that py2exe will work correctly. 59 | - RESTful API 60 | - (low prio): HTTPS 61 | 62 | Cron 63 | ---- 64 | - Ramona can be used to trigger tasks (tools) by given time - emulating functionality of cron 65 | 66 | Cluster 67 | ------- 68 | - Ramona can be used as cluster controller - running on every node and managing application there. 69 | - Ramona cluster controller needs to be built to allow single point of control 70 | - Fail-over scenarion support 71 | - Amazon ECS integration (e.g. use of Amazon variables that are passed to the box) 72 | -------------------------------------------------------------------------------- /docs/vocab.txt: -------------------------------------------------------------------------------- 1 | cns - console 2 | fend - frontend -------------------------------------------------------------------------------- /gource.config: -------------------------------------------------------------------------------- 1 | [gource] 2 | seconds-per-day=1 3 | title=Ramona 4 | 5 | -------------------------------------------------------------------------------- /ramona-dev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Released under the BSD license. See LICENSE.txt file for details. 4 | # 5 | import os 6 | import sys 7 | import fnmatch 8 | import shutil 9 | import ramona 10 | 11 | class RamonaDevConsoleApp(ramona.console_app): 12 | 13 | @ramona.tool 14 | def clean(self): 15 | """Clean project directory from intermediate files (*.pyc)""" 16 | for root, dirnames, filenames in os.walk('.'): 17 | for filename in fnmatch.filter(filenames, '*.pyc'): 18 | filename = os.path.join(root, filename) 19 | if not os.path.isfile(filename): continue 20 | os.unlink(filename) 21 | 22 | try: 23 | shutil.rmtree('dist') 24 | except: 25 | pass 26 | 27 | try: 28 | shutil.rmtree('build') 29 | except: 30 | pass 31 | 32 | for f in ['MANIFEST', 'demo_history', 'ramonadev_history']: 33 | try: 34 | os.unlink(f) 35 | except: 36 | pass 37 | 38 | 39 | @ramona.tool 40 | def unittests(self): 41 | """Seek for all unit tests and execute them""" 42 | import unittest 43 | tl = unittest.TestLoader() 44 | ts = tl.discover('.', '__utest__.py') 45 | 46 | tr = unittest.runner.TextTestRunner(verbosity=2) 47 | res = tr.run(ts) 48 | 49 | return 0 if res.wasSuccessful() else 1 50 | 51 | @ramona.tool 52 | def sdist(self): 53 | """Prepare the distribution package""" 54 | os.execl(sys.executable, sys.executable, 'setup.py', 'sdist', '--formats=gztar,zip', '--owner=root', '--group=root') 55 | 56 | @ramona.tool 57 | def upload_test(self): 58 | """Upload (register) a new version to TestPyPi""" 59 | os.system("LC_ALL=en_US.UTF-8 {0} setup.py \ 60 | sdist --formats=gztar,zip --owner=root --group=root \ 61 | register -r http://testpypi.python.org/pypi \ 62 | upload -r http://testpypi.python.org/pypi \ 63 | ".format(sys.executable) 64 | ) 65 | 66 | @ramona.tool 67 | def upload(self): 68 | """Upload (register) a new version to PyPi""" 69 | os.system("LC_ALL=en_US.UTF-8 {0} setup.py \ 70 | sdist --formats=gztar,zip --owner=root --group=root \ 71 | register -r http://pypi.python.org/pypi \ 72 | upload -r http://pypi.python.org/pypi \ 73 | ".format(sys.executable) 74 | ) 75 | 76 | @ramona.tool 77 | def version(self): 78 | """Returns the Ramona version number""" 79 | print ramona.version 80 | 81 | @ramona.tool 82 | def manual(self): 83 | """Build a HTML version of the manual""" 84 | if os.path.isdir('docs/manual/_build'): 85 | shutil.rmtree('docs/manual/_build') 86 | os.system('LC_ALL=en_US.UTF-8 make -C docs/manual html') 87 | 88 | @ramona.tool 89 | def gource(self): 90 | """Creates visualizations about the Ramona development""" 91 | import subprocess, re 92 | 93 | cmd= r"""git log --pretty=format:user:%aN%n%ct --reverse --raw --encoding=UTF-8 --no-renames""" 94 | gitlog = subprocess.check_output(cmd, shell=True) 95 | 96 | gitlog = re.sub(r'\nuser:Ales Teska\n','\nuser:ateska\n',gitlog) 97 | gitlog = re.sub(r'\nuser:Jan Stastny\n','\nuser:jstastny\n',gitlog) 98 | 99 | cmd = r"""gource -1280x720 --stop-at-end --highlight-users --seconds-per-day .5 --title "Ramona" --log-format git -o - -""" 100 | cmd += " | " 101 | cmd += r"ffmpeg -y -r 60 -f image2pipe -vcodec ppm -i - -vcodec libx264 -preset ultrafast -pix_fmt yuv420p -crf 16 -threads 0 -bf 0 gource.mp4" 102 | 103 | x = subprocess.Popen(cmd, stdin=subprocess.PIPE, shell=True) 104 | x.communicate(gitlog) 105 | 106 | if __name__ == '__main__': 107 | app = RamonaDevConsoleApp(configuration='./ramona.conf') 108 | app.run() 109 | -------------------------------------------------------------------------------- /ramona.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | appname=ramona-dev 3 | 4 | logdir=./log/ 5 | 6 | [ramona:server] 7 | consoleuri=unix:///tmp/ramona-dev.sock?mode=0600 8 | pidfile=/tmp/ramona-dev.pid 9 | 10 | [ramona:console] 11 | serveruri=unix:///tmp/ramona-dev.sock 12 | history=./ramonadev_history 13 | -------------------------------------------------------------------------------- /ramona/__init__.py: -------------------------------------------------------------------------------- 1 | from .console.cnsapp import console_app, tool, proxy_tool 2 | 3 | ''' 4 | Ramona - Runtime supervisor 5 | =========================== 6 | 7 | Project homepage: http://ateska.github.com/ramona/ 8 | Project page on PyPi: http://pypi.python.org/pypi/ramona 9 | Project page on GitHub: https://github.com/ateska/ramona 10 | 11 | Authors 12 | ------- 13 | Ales Teska 14 | Jan Stastny 15 | 16 | License 17 | ------- 18 | BSD 2-clause "Simplified" License 19 | ''' 20 | 21 | #For proper version formatting see: 22 | # - http://www.python.org/dev/peps/pep-0386/ 23 | # - http://www.python.org/dev/peps/pep-0345/#version-specifiers 24 | 25 | version = 'master' # Also in setup.py 26 | -------------------------------------------------------------------------------- /ramona/__utest__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import logging 3 | from . import config 4 | from . import sendmail 5 | from . import utils 6 | ### 7 | ''' 8 | To launch unit test: 9 | python -m unittest -v ramona.__utest__ 10 | ''' 11 | ### 12 | 13 | class TestConfig(unittest.TestCase): 14 | 15 | 16 | def test_get_numeric_loglevel(self): 17 | '''Translating log level to numbers''' 18 | lvl = config.get_numeric_loglevel('Debug') 19 | self.assertEqual(lvl, logging.DEBUG) 20 | 21 | lvl = config.get_numeric_loglevel('ERROR') 22 | self.assertEqual(lvl, logging.ERROR) 23 | 24 | self.assertRaises(ValueError, config.get_numeric_loglevel, '') 25 | 26 | # 27 | 28 | class TestSendMail(unittest.TestCase): 29 | 30 | def test_get_default_fromaddr(self): 31 | sendmail.send_mail.get_default_fromaddr() 32 | 33 | 34 | def test_sendmail_uri_01(self): 35 | u = sendmail.send_mail('smtp://mail.example.com') 36 | self.assertEqual(u.hostname, 'mail.example.com') 37 | self.assertEqual(u.port, 25) 38 | self.assertIsNone(u.username) 39 | self.assertIsNone(u.password) 40 | self.assertDictEqual(u.params, {}) 41 | 42 | 43 | def test_sendmail_uri_02(self): 44 | self.assertRaises(RuntimeError, sendmail.send_mail, 'xsmtp://smtp.t-email.cz') 45 | 46 | 47 | def test_sendmail_uri_03(self): 48 | self.assertRaises(RuntimeError, sendmail.send_mail, 'xsmtp:///dd') 49 | 50 | 51 | def test_sendmail_uri_04(self): 52 | '''Simulating Google SMTP parametrization''' 53 | u = sendmail.send_mail('smtp://user:password@smtp.gmail.com:587?tls=1') 54 | self.assertEqual(u.hostname, 'smtp.gmail.com') 55 | self.assertEqual(u.port, 587) 56 | self.assertEqual(u.username, 'user') 57 | self.assertEqual(u.password, 'password') 58 | self.assertDictEqual(u.params, {'tls':'1'}) 59 | 60 | # 61 | 62 | class TestExpandVars(unittest.TestCase): 63 | 64 | def test_expandvars_01(self): 65 | env = {'FOO':'bar'} 66 | 67 | p = utils.expandvars('/testing/$FOO/there', env) 68 | self.assertEqual(p, '/testing/bar/there') 69 | 70 | p = utils.expandvars('$FOO/there', env) 71 | self.assertEqual(p, 'bar/there') 72 | 73 | p = utils.expandvars('/testing/$FOO', env) 74 | self.assertEqual(p, '/testing/bar') 75 | 76 | 77 | def test_expandvars_02(self): 78 | env = {'FOO':'bar'} 79 | 80 | p = utils.expandvars('$XXX/testing/$FOO/there', env) 81 | self.assertEqual(p, '$XXX/testing/bar/there') 82 | 83 | p = utils.expandvars('$FOO/there$XXX', env) 84 | self.assertEqual(p, 'bar/there$XXX') 85 | 86 | p = utils.expandvars('/testing/$XX$FOO', env) 87 | self.assertEqual(p, '/testing/$XXbar') 88 | 89 | 90 | def test_expandvars_02(self): 91 | env = {'FOO':'bar'} 92 | 93 | p = utils.expandvars('$XXX/testing/${FOO}/there', env) 94 | self.assertEqual(p, '$XXX/testing/bar/there') 95 | 96 | p = utils.expandvars('${FOO}/there$XXX', env) 97 | self.assertEqual(p, 'bar/there$XXX') 98 | 99 | p = utils.expandvars('/testing/$XX${FOO}', env) 100 | self.assertEqual(p, '/testing/$XXbar') 101 | -------------------------------------------------------------------------------- /ramona/cnscom.py: -------------------------------------------------------------------------------- 1 | import os, struct, time, json, select, logging 2 | ### 3 | 4 | L = logging.getLogger("cnscom") 5 | Lmy = logging.getLogger("my") 6 | 7 | ### 8 | 9 | callid_ping = 70 10 | callid_start = 71 11 | callid_stop = 72 12 | callid_restart = 73 13 | callid_status = 74 14 | callid_tail = 75 15 | callid_tailf_stop = 76 # Terminate previously initialized tailf mode 16 | callid_init = 77 17 | callid_who = 78 18 | callid_notify = 79 19 | 20 | # 21 | 22 | call_magic = '>' 23 | resp_magic = '<' 24 | 25 | call_struct_fmt = '!cBH' 26 | resp_struct_fmt = '!ccH' 27 | 28 | resp_return = 'R' 29 | resp_exception = 'E' 30 | resp_yield_message = 'M' # Used to propagate message from server to console 31 | resp_tailf_data = 'T' # Used to send data in tail -f mode 32 | 33 | ### 34 | 35 | class program_state_enum(object): 36 | '''Enum''' 37 | DISABLED = -1 38 | STOPPED = 0 39 | STARTING = 10 40 | RUNNING = 20 41 | STOPPING = 30 42 | FATAL = 200 43 | CFGERROR=201 44 | 45 | labels = { 46 | DISABLED: 'DISABLED', 47 | STOPPED: 'STOPPED', 48 | STARTING: 'STARTING', 49 | RUNNING: 'RUNNING', 50 | STOPPING: 'STOPPING', 51 | FATAL: 'FATAL', 52 | CFGERROR: 'CFGERROR', 53 | } 54 | 55 | 56 | ### 57 | 58 | 59 | def svrcall(cnssocket, callid, params=""): 60 | ''' 61 | Client side of console communication IPC call (kind of RPC / Remote procedure call). 62 | 63 | @param cnssocket: Socket to server (created by socket_uri factory) 64 | @param callid: one of callid_* identification 65 | @param params: string representing parameters that will be passed to server call 66 | @return: String returned by server or raises exception if server call failed 67 | ''' 68 | 69 | paramlen = len(params) 70 | if paramlen >= 0x7fff: 71 | raise RuntimeError("Transmitted parameters are too long.") 72 | 73 | cnssocket.send(struct.pack(call_struct_fmt, call_magic, callid, paramlen)+params) 74 | 75 | while 1: 76 | retype, params = svrresp(cnssocket, hang_message="callid : {0}".format(callid)) 77 | 78 | if retype == resp_return: 79 | # Remote server call returned normally 80 | return params 81 | 82 | elif retype == resp_exception: 83 | # Remove server call returned exception 84 | raise RuntimeError(params) 85 | 86 | elif retype == resp_yield_message: 87 | # Remote server call returned yielded message -> we will continue receiving 88 | obj = json.loads(params) 89 | obj = logging.makeLogRecord(obj) 90 | if Lmy.getEffectiveLevel() <= obj.levelno: # Print only if log level allows that 91 | Lmy.handle(obj) 92 | continue 93 | 94 | else: 95 | raise RuntimeError("Unknown/invalid server response: {0}".format(retype)) 96 | 97 | ### 98 | 99 | def svrresp(cnssocket, hang_detector=True, hang_message='details not provided'): 100 | '''Receive and parse one server response - used inherently by svrcall. 101 | 102 | @param cnssocket: Socket to server (created by socket_uri factory) 103 | @param hang_detector: If set to True, logs warning when server is not responding in 2 seconds 104 | @param hang_message: Details about server call to be included in eventual hang message 105 | @return: tuple(retype, params) - retype is cnscom.resp_* integer and params are data attached to given response 106 | ''' 107 | 108 | x = time.time() 109 | resp = "" 110 | while len(resp) < 4: 111 | rlist, _, _ = select.select([cnssocket],[],[], 5) 112 | if len(rlist) == 0: 113 | if hang_detector and time.time() - x > 5: 114 | x = time.time() 115 | L.warning("Possible server hang detected: {0} (continue waiting)".format(hang_message)) 116 | continue 117 | ndata = cnssocket.recv(4 - len(resp)) 118 | if len(ndata) == 0: 119 | raise EOFError("It looks like server closed connection") 120 | 121 | resp += ndata 122 | 123 | magic, retype, paramlen = struct.unpack(resp_struct_fmt, resp) 124 | assert magic == resp_magic 125 | 126 | # Read rest of the response (size given by paramlen) 127 | params = "" 128 | while paramlen > 0: 129 | ndata = cnssocket.recv(paramlen) 130 | params += ndata 131 | paramlen -= len(ndata) 132 | 133 | return retype, params 134 | 135 | ### 136 | 137 | def parse_json_kwargs(params): 138 | '''Used when params are transfered as JSON - it also handles situation when 'params' is empty string ''' 139 | if params == '': return dict() 140 | return json.loads(params) 141 | 142 | ### 143 | 144 | class svrcall_error(RuntimeError): 145 | ''' 146 | Exception used to report error to the console without leaving trace in server error log. 147 | ''' 148 | pass 149 | -------------------------------------------------------------------------------- /ramona/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os, sys, logging, re, platform, ConfigParser 3 | ### 4 | 5 | L = logging.getLogger("config") 6 | 7 | ### 8 | 9 | # Defaults are stated in documentation, if you change them here, update documentation too! 10 | config_defaults = { 11 | 'general' : { 12 | 'appname' : 'ramona-driven-app', 13 | 'logdir' : '', 14 | 'include' : '', 15 | 'logmaxsize': '{0}'.format(512*1024*1024), # 512Mb 16 | 'logbackups': '3', 17 | 'logcompress': '1' 18 | }, 19 | 'ramona:server' : { 20 | 'consoleuri': 'unix://.ramona.sock', 21 | 'consoleuri@windows': 'tcp://localhost:7788', 22 | 'pidfile': '', 23 | 'log': '', 24 | 'loglevel': 'INFO', 25 | }, 26 | 'ramona:console' : { 27 | 'serveruri': 'unix://.ramona.sock', 28 | 'serveruri@windows': 'tcp://localhost:7788', 29 | 'history': '', 30 | }, 31 | 'ramona:notify' : { 32 | 'delivery': '', 33 | 'sender': '', 34 | 'dailyat': '09:00', 35 | 'notify_fatal': 'now', 36 | 'logscan_stdout': '', 37 | 'logscan_stderr': '', 38 | 'logscan': '', 39 | 'stashdir': '', 40 | }, 41 | 'ramona:httpfend': { 42 | 'listenaddr': "tcp://localhost:5588", 43 | } 44 | 45 | } 46 | 47 | ### 48 | 49 | config = ConfigParser.SafeConfigParser() 50 | config.optionxform = str # Disable default 'lowecasing' behavior of ConfigParser 51 | config_files = [] 52 | config_includes = [] 53 | 54 | config_platform_selector = platform.system().lower() 55 | 56 | ### 57 | 58 | def read_config(configs=None, use_env=True): 59 | global config 60 | assert len(config.sections()) == 0 61 | 62 | # Prepare platform selector regex 63 | psrg = re.compile('^(.*)@(.*)$') 64 | 65 | # Load config_defaults 66 | psdefaults = [] 67 | for section, items in config_defaults.iteritems(): 68 | if not config.has_section(section): 69 | config.add_section(section) 70 | 71 | for key, val in items.iteritems(): 72 | r = psrg.match(key) 73 | if r is None: 74 | config.set(section, key, val) 75 | else: 76 | if r.group(2) != config_platform_selector: continue 77 | psdefaults.append((section, r.group(1), val)) 78 | 79 | # Handle platform selectors in config_defaults 80 | for section, key, val in psdefaults: 81 | config.set(section, key, val) 82 | 83 | 84 | # Load configuration files 85 | global config_files 86 | 87 | if configs is not None: configs = configs[:] 88 | else: configs = [] 89 | if use_env: 90 | # Configs from environment variables 91 | config_envs = os.environ.get('RAMONA_CONFIG') 92 | if config_envs is not None: 93 | for config_file in config_envs.split(os.pathsep): 94 | configs.append(config_file) 95 | 96 | for cfile in configs: 97 | rfile = os.path.abspath(os.path.expanduser(cfile)) 98 | if os.path.isfile(rfile): 99 | config_files.append(rfile) 100 | config.read([rfile]) 101 | 102 | 103 | # Handle includes ... 104 | appname = config.get('general','appname') 105 | for _ in range(100): 106 | includes = config.get('general','include') 107 | if includes == '': break 108 | config.set('general','include','') 109 | includes = includes.split(';') 110 | for i in xrange(len(includes)-1,-1,-1): 111 | include = includes[i] = includes[i].strip() 112 | if include == '': 113 | # These are platform specific 114 | siteconfs = [ 115 | './site.conf', 116 | './{}-site.conf'.format(appname), 117 | '/etc/{0}.conf'.format(appname), 118 | '~/.{0}.conf'.format(appname), 119 | ] 120 | includes[i:i+1] = siteconfs 121 | elif include[:1] == '<': 122 | print('WARNING: Unknown include fragment: {0}'.format(include), file=sys.stderr) 123 | continue 124 | 125 | for include in includes: 126 | rinclude = os.path.abspath(os.path.expanduser(include)) 127 | if os.path.isfile(rinclude): 128 | config_includes.append(rinclude) 129 | config.read([rinclude]) 130 | 131 | else: 132 | raise RuntimeError("FATAL: It looks like we have loop in configuration includes!") 133 | 134 | # Threat platform selector alternatives 135 | if config_platform_selector is not None and config_platform_selector != '': 136 | for section in config.sections(): 137 | for name, value in config.items(section): 138 | r = psrg.match(name) 139 | if r is None: continue 140 | if (r.group(2) != config_platform_selector): continue 141 | config.set(section, r.group(1), value) 142 | 143 | # Special treatment of some values 144 | if config.get('general', 'logdir') == '': 145 | logdir = os.environ.get('LOGDIR') 146 | if logdir is None: logdir = os.curdir 147 | logdir = os.path.expanduser(logdir) 148 | config.set('general','logdir',logdir) 149 | elif config.get('general', 'logdir').strip()[:1] == '<': 150 | raise RuntimeError("FATAL: Unknown magic value in [general] logdir: '{}'".format(config.get('general', 'logdir'))) 151 | 152 | for (sec, valname) in (("ramona:server", "consoleuri"), ("ramona:notify", "delivery")): 153 | if ";" in config.get(sec, valname): 154 | print( 155 | "WARNING: ';' character was found in URI: {}. Please note that ';' has been replaced '?' in Ramona 1.0. This can lead to Ramona apparent freeze during start.".format( 156 | config.get(sec, valname) 157 | ), 158 | file=sys.stderr 159 | ) 160 | 161 | stashdir = config.get('ramona:notify', 'stashdir') 162 | if stashdir != '': 163 | if not os.path.isdir(stashdir): 164 | os.makedirs(stashdir) 165 | 166 | ### 167 | 168 | def get_boolean(value): 169 | ''' 170 | Translates string/ value into boolean value. It is kind of similar to ConfigParser.getboolean but this one is used also in different places of code 171 | ''' 172 | if value is True: return True 173 | if value is False: return False 174 | 175 | value = str(value) 176 | 177 | if value.upper() in ('TRUE','ON','YES','1'): 178 | return True 179 | elif value.upper() in ('FALSE','OFF','NO','0'): 180 | return False 181 | else: 182 | raise ValueError("Invalid boolean string '{0}'' (use one of true, on, yes, false, off or no).".format(value)) 183 | 184 | ### 185 | 186 | def get_numeric_loglevel(loglevelstring): 187 | ''' 188 | Translates log level given in string into numeric value. 189 | ''' 190 | numeric_level = getattr(logging, loglevelstring.upper(), None) 191 | if not isinstance(numeric_level, int): raise ValueError('Invalid log level: {0}'.format(loglevelstring)) 192 | return numeric_level 193 | 194 | ### 195 | 196 | def get_logconfig(): 197 | ''' 198 | return (logbackups, logmaxsize, logcompress) tupple 199 | ''' 200 | if config.get('general','logmaxsize') == '': 201 | logbackups = 0 202 | logmaxsize = 0 203 | logcompress = False 204 | 205 | else: 206 | try: 207 | # TODO: Parse human-friendly logmaxsize ... e.g. 10Mb 208 | logmaxsize = config.getint('general','logmaxsize') 209 | x = config.get('general','logbackups') 210 | if x == '': 211 | logbackups = 0 212 | else: 213 | logbackups = int(x) 214 | logcompress = config.getboolean('general', 'logcompress') 215 | except Exception, e: 216 | logbackups = 0 217 | logmaxsize = 0 218 | logcompress = False 219 | L.warning("Invalid configuration of log rotation: {0} - log rotation disabled".format(e)) 220 | 221 | return logbackups, logmaxsize, logcompress 222 | 223 | 224 | def get_env(alt_env=None): 225 | """ 226 | Get environment variables dictionary from config. 227 | If not argument provided, it is taken from [env] section of the configuration merged with os.environ 228 | If alt_env argument is provided, it is taken from [env:] section merged with os.environ 229 | 230 | Return is compatible with os.exec family of functions. 231 | """ 232 | if alt_env is not None: 233 | section = "env:{0}".format(alt_env) 234 | else: 235 | section = "env" 236 | env = os.environ.copy() 237 | 238 | if config.has_section(section): 239 | for name, value in config.items(section): 240 | if value != '': 241 | env[name] = value 242 | else: 243 | env.pop(name, 0) 244 | return env 245 | 246 | -------------------------------------------------------------------------------- /ramona/console/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is module that contains functionality of Ramona console 3 | ''' -------------------------------------------------------------------------------- /ramona/console/cmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/ramona/console/cmd/__init__.py -------------------------------------------------------------------------------- /ramona/console/cmd/_completions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ... import cnscom 3 | 4 | def complete_ident(console, textst): 5 | ret = [] 6 | statuses = console.cnsapp.cnssvrcall(cnscom.callid_status, json.dumps({}), auto_connect=True) 7 | for st in json.loads(statuses): 8 | if st['ident'].startswith(textst) or textst == "": 9 | ret.append(st['ident']) 10 | return ret 11 | -------------------------------------------------------------------------------- /ramona/console/cmd/console.py: -------------------------------------------------------------------------------- 1 | import os, cmd, logging, sys, functools 2 | from ...config import config 3 | from ... import cnscom 4 | 5 | ### 6 | 7 | name = 'console' 8 | cmdhelp = 'Enter interactive console mode' 9 | 10 | ### 11 | 12 | L = logging.getLogger("console") 13 | 14 | ### 15 | 16 | def init_parser(parser): 17 | return 18 | 19 | ### 20 | 21 | try: 22 | import readline 23 | except ImportError: 24 | readline = None 25 | 26 | if readline is not None: 27 | # See http://stackoverflow.com/questions/7116038/python-tab-completion-mac-osx-10-7-lion 28 | if 'libedit' in readline.__doc__: 29 | readline.parse_and_bind("bind ^I rl_complete") 30 | else: 31 | readline.parse_and_bind("tab: complete") 32 | 33 | ### 34 | 35 | class _console_cmd(cmd.Cmd): 36 | 37 | def __init__(self, cnsapp): 38 | self.prompt = '> ' 39 | self.cnsapp = cnsapp 40 | 41 | 42 | from ..parser import consoleparser 43 | self.parser = consoleparser(self.cnsapp) 44 | 45 | # Build dummy method for each command in the parser 46 | for cmdname, cmditem in self.parser.subcommands.iteritems(): 47 | def do_cmd_template(self, _cmdline): 48 | try: 49 | self.parser.execute(self.cnsapp) 50 | except Exception, e: 51 | L.error("{0}".format(e)) 52 | 53 | setattr(self.__class__, "do_{0}".format(cmdname), do_cmd_template) 54 | 55 | if hasattr(cmditem, "complete"): 56 | setattr(self.__class__, "complete_{0}".format(cmdname), cmditem.complete) 57 | 58 | # Add also proxy_tools 59 | self.proxy_tool_set = set() 60 | for mn in dir(cnsapp): 61 | fn = getattr(cnsapp, mn) 62 | if not hasattr(fn, '__proxy_tool'): continue 63 | 64 | self.proxy_tool_set.add(mn) 65 | setattr(self.__class__, "do_{0}".format(mn), functools.partial(launch_proxy_tool, fn, mn)) 66 | 67 | 68 | cmd.Cmd.__init__(self) 69 | 70 | 71 | def precmd(self, line): 72 | if line == '': return '' 73 | if line == "EOF": 74 | print 75 | sys.exit(0) 76 | 77 | # Check if this is proxy tool - if yes, then bypass parser 78 | try: 79 | farg, _ = line.split(' ',1) 80 | except ValueError: 81 | farg = line 82 | farg = farg.strip() 83 | if farg in self.proxy_tool_set: 84 | return line 85 | 86 | try: 87 | self.parser.parse(line.split()) 88 | except SyntaxError: # To capture cases like 'xxx' (avoid exiting) 89 | self.parser.parse(['help']) 90 | return 'help' 91 | except SystemExit: # To capture cases like 'tail -xxx' (avoid exiting) 92 | self.parser.parse(['help']) 93 | return 'help' 94 | return line 95 | 96 | 97 | def emptyline(self): 98 | # Send 'ping' to server 99 | try: 100 | self.cnsapp.cnssvrcall(cnscom.callid_ping, '', auto_connect=True) 101 | except Exception, e: 102 | L.error("{0}".format(e)) 103 | 104 | # 105 | 106 | def main(cnsapp, args): 107 | from ... import version as ramona_version 108 | L.info("Ramona (version {0}) console for {1}".format(ramona_version, config.get('general','appname'))) 109 | 110 | histfile = config.get('ramona:console', 'history') 111 | if histfile != '': 112 | histfile = os.path.expanduser(histfile) 113 | try: 114 | if readline is not None: readline.read_history_file(histfile) 115 | except IOError: 116 | pass 117 | 118 | c = _console_cmd(cnsapp) 119 | try: 120 | c.cmdloop() 121 | 122 | except Exception, e: 123 | L.exception("Exception during cmd loop:") 124 | 125 | except KeyboardInterrupt: 126 | print "" 127 | 128 | finally: 129 | if readline is not None and histfile != '': 130 | try: 131 | readline.write_history_file(histfile) 132 | except Exception, e: 133 | L.warning("Cannot write console history file '{1}': {0}".format(e, histfile)) 134 | 135 | # 136 | 137 | def launch_proxy_tool(fn, cmd, cmdline): 138 | ''' 139 | To launch proxy tool, we need to fork and then call proxy_tool method in child. 140 | Parent is waiting for child exit ... 141 | ''' 142 | cmdline = cmdline.split(' ') 143 | cmdline.insert(0, cmd) 144 | 145 | pid = os.fork() 146 | if pid == 0: 147 | # Child 148 | try: 149 | fn(cmdline) 150 | except Exception, e: 151 | print "Execution of tool failed: ", e 152 | os._exit(0) 153 | else: 154 | # Parent 155 | ret = os.waitpid(pid, 0) # Wait for child process to finish 156 | 157 | -------------------------------------------------------------------------------- /ramona/console/cmd/exit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | ### 3 | 4 | name = 'exit' 5 | cmdhelp = 'Exit console' 6 | 7 | ### 8 | 9 | def init_parser(parser): 10 | return 11 | 12 | ### 13 | 14 | def main(cnsapp, args): 15 | sys.exit(0) # Ignore return code from application 16 | -------------------------------------------------------------------------------- /ramona/console/cmd/help.py: -------------------------------------------------------------------------------- 1 | 2 | name = 'help' 3 | cmdhelp = 'Display help' 4 | 5 | ### 6 | 7 | def init_parser(parser): 8 | return 9 | 10 | def main(cnsapp, args): 11 | '''Stub that actually does nothing''' 12 | pass -------------------------------------------------------------------------------- /ramona/console/cmd/notify.py: -------------------------------------------------------------------------------- 1 | import json, logging 2 | from ... import cnscom 3 | 4 | ### 5 | 6 | name = 'notify' 7 | cmdhelp = 'Insert notification' 8 | 9 | ### 10 | 11 | L = logging.getLogger('notify') 12 | 13 | ### 14 | 15 | def init_parser(parser): 16 | parser.add_argument('-t','--target', action='store', choices=['now','daily'], default='now', help='Specify target of notification. Default is "now".') 17 | parser.add_argument('-s','--subject', action='store', help="Specify subject of notification.") 18 | parser.add_argument('text', help='Body of notification.') 19 | 20 | def main(cnsapp, args): 21 | params = { 22 | 'target': args.target, 23 | 'subject': args.subject, 24 | 'text': args.text 25 | } 26 | ret = cnsapp.cnssvrcall( 27 | cnscom.callid_notify, 28 | json.dumps(params), 29 | auto_connect=True 30 | ) 31 | -------------------------------------------------------------------------------- /ramona/console/cmd/restart.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ... import cnscom 3 | from ._completions import complete_ident 4 | 5 | ### 6 | 7 | name = 'restart' 8 | cmdhelp = 'Restart program(s)' 9 | 10 | ### 11 | 12 | def init_parser(parser): 13 | parser.add_argument('-n','--no-server-start', action='store_true', help='Avoid eventual automatic Ramona server start') 14 | parser.add_argument('-i','--immediate-return', action='store_true', help="Don't wait for restart of programs and exit ASAP") 15 | parser.add_argument('-f','--force-start', action='store_true', help='Force restart of programs even in FATAL state') 16 | parser.add_argument('program', nargs='*', help='Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope.') 17 | 18 | ### 19 | 20 | def complete(console, text, line, begidx, endidx): 21 | textst = text.strip() 22 | ret = [] 23 | ret.extend(complete_ident(console, textst)) 24 | return ret 25 | 26 | ### 27 | 28 | def main(cnsapp, args): 29 | params={ 30 | 'force':args.force_start, 31 | 'immediate': args.immediate_return, 32 | } 33 | if len(args.program) > 0: params['pfilter'] = args.program 34 | 35 | cnsapp.cnssvrcall( 36 | cnscom.callid_restart, 37 | json.dumps(params), 38 | auto_connect=args.no_server_start, 39 | auto_server_start=not args.no_server_start 40 | ) 41 | -------------------------------------------------------------------------------- /ramona/console/cmd/server.py: -------------------------------------------------------------------------------- 1 | from .. import exception 2 | 3 | ### 4 | 5 | name = 'server' 6 | cmdhelp = 'Start the Ramona server in the foreground' 7 | 8 | ### 9 | 10 | def init_parser(parser): 11 | parser.add_argument('-S','--server-only', action='store_true', help='Start only server, programs are not launched') 12 | parser.add_argument('program', nargs='*', help='Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope.') 13 | 14 | ### 15 | 16 | def main(cnsapp, args): 17 | if args.server_only: 18 | if len(args.program) > 0: 19 | raise exception.parameters_error('Cannot specify programs and -S option at once.') 20 | 21 | from ...utils import launch_server 22 | launch_server(args.server_only, args.program) 23 | -------------------------------------------------------------------------------- /ramona/console/cmd/start.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ... import cnscom 3 | from .. import exception 4 | from ._completions import complete_ident 5 | ### 6 | 7 | name = 'start' 8 | cmdhelp = 'Start program(s)' 9 | 10 | ### 11 | 12 | def init_parser(parser): 13 | parser.add_argument('-n','--no-server-start', action='store_true', help='Avoid eventual automatic start of Ramona server') 14 | parser.add_argument('-i','--immediate-return', action='store_true', help="Don't wait for start of programs and exit ASAP") 15 | parser.add_argument('-f','--force-start', action='store_true', help='Force start of programs even if they are in FATAL state') 16 | parser.add_argument('-S','--server-only', action='store_true', help='Start only server, programs are not started') 17 | parser.add_argument('program', nargs='*', help='Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope.') 18 | 19 | ### 20 | 21 | def complete(console, text, line, begidx, endidx): 22 | textst = text.strip() 23 | ret = [] 24 | ret.extend(complete_ident(console, textst)) 25 | return ret 26 | 27 | ### 28 | 29 | def main(cnsapp, args): 30 | 31 | if args.server_only: 32 | if len(args.program) > 0: 33 | raise exception.parameters_error('Cannot specify programs and -S option at once.') 34 | cnsapp.auto_server_start() 35 | return 36 | 37 | params={ 38 | 'force': args.force_start, 39 | 'immediate': args.immediate_return, 40 | } 41 | if len(args.program) > 0: params['pfilter'] = args.program 42 | 43 | cnsapp.cnssvrcall( 44 | cnscom.callid_start, 45 | json.dumps(params), 46 | auto_connect=args.no_server_start, 47 | auto_server_start=not args.no_server_start 48 | ) 49 | -------------------------------------------------------------------------------- /ramona/console/cmd/status.py: -------------------------------------------------------------------------------- 1 | import json, time, itertools, collections, logging 2 | from ... import cnscom 3 | from ._completions import complete_ident 4 | 5 | ### 6 | 7 | name = 'status' 8 | cmdhelp = 'Show status of program(s)' 9 | 10 | ### 11 | 12 | L = logging.getLogger('status') 13 | 14 | ### 15 | 16 | def init_parser(parser): 17 | parser.add_argument('program', nargs='*', help='Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope') 18 | 19 | ### 20 | 21 | def complete(console, text, line, begidx, endidx): 22 | textst = text.strip() 23 | ret = [] 24 | ret.extend(complete_ident(console, textst)) 25 | return ret 26 | 27 | ### 28 | 29 | def main(cnsapp, args): 30 | params={} 31 | if len(args.program) > 0: params['pfilter'] = args.program 32 | ret = cnsapp.cnssvrcall( 33 | cnscom.callid_status, 34 | json.dumps(params), 35 | auto_connect=True 36 | ) 37 | 38 | status = json.loads(ret) 39 | 40 | for sp in status: 41 | 42 | details = collections.OrderedDict() 43 | 44 | exit_status = sp.pop('exit_status', None) 45 | if exit_status is not None: details["exit_status"] = exit_status 46 | 47 | pid = sp.pop('pid', None) 48 | if pid is not None: details["pid"] = pid 49 | 50 | details['launches'] = sp.pop('launch_cnt','') 51 | 52 | t = sp.pop('start_time', None) 53 | if t is not None: details["start_time"] = time.strftime("%d-%m-%Y %H:%M:%S",time.localtime(t)) 54 | 55 | t = sp.pop('exit_time', None) 56 | if t is not None: details["exit_time"] = time.strftime("%d-%m-%Y %H:%M:%S",time.localtime(t)) 57 | 58 | # Format uptime 59 | t = sp.pop('uptime', None) 60 | if t is not None: 61 | # TODO: Print pretty (e.g. uptime:2d 2h) 62 | details['uptime'] = "{0:.2f}s".format(t) 63 | 64 | state = sp.pop('state') 65 | stlbl = cnscom.program_state_enum.labels.get(state, "({0})".format(state)) 66 | 67 | line = "{0:<16} {1:<10}".format( 68 | sp.pop('ident', '???'), 69 | stlbl, 70 | ) 71 | 72 | line += ', '.join(['{0}:{1}'.format(k,v) for k,v in itertools.chain(details.iteritems(), sp.iteritems())]) 73 | 74 | 75 | print line 76 | -------------------------------------------------------------------------------- /ramona/console/cmd/stop.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ... import cnscom 3 | from .. import exception 4 | from ._completions import complete_ident 5 | 6 | ### 7 | 8 | name = 'stop' 9 | cmdhelp = 'Stop program(s)' 10 | 11 | ### 12 | 13 | def init_parser(parser): 14 | parser.add_argument('-i','--immediate-return', action='store_true', help='Dont wait for termination of programs and exit ASAP') 15 | parser.add_argument('-c','--core-dump', action='store_true', help='Stop program(s) to produce core dump (has to be also enabled in program configuration).') 16 | parser.add_argument('-E','--stop-and-exit', action='store_true', help='Stop all programs and exit Ramona server. Command-line default for:\n%(prog)s') 17 | parser.add_argument('-S','--stop-and-stay', action='store_true', help='Stop all programs but keep Ramona server running') 18 | parser.add_argument('program', nargs='*', help='Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope') 19 | 20 | ### 21 | 22 | def complete(console, text, line, begidx, endidx): 23 | textst = text.strip() 24 | ret = [] 25 | ret.extend(complete_ident(console, textst)) 26 | return ret 27 | 28 | ### 29 | 30 | def main(cnsapp, args): 31 | if args.stop_and_exit and len(args.program) > 0: 32 | raise exception.parameters_error('Cannot specify programs and -E option at once.') 33 | 34 | if args.stop_and_exit and args.stop_and_stay: 35 | raise exception.parameters_error('Cannot specify -T and -E option at once.') 36 | 37 | if len(args.program) == 0 and not args.stop_and_stay: 38 | args.stop_and_exit = True 39 | 40 | params={ 41 | 'immediate': args.immediate_return, 42 | 'coredump': args.core_dump, 43 | } 44 | if args.stop_and_exit: params['mode'] = 'exit' 45 | elif args.stop_and_stay: params['mode'] = 'stay' 46 | if len(args.program) > 0: params['pfilter'] = args.program 47 | 48 | cnsapp.cnssvrcall( 49 | cnscom.callid_stop, 50 | json.dumps(params), 51 | auto_connect=True 52 | ) 53 | 54 | if args.stop_and_exit: 55 | cnsapp.wait_for_svrexit() 56 | -------------------------------------------------------------------------------- /ramona/console/cmd/tail.py: -------------------------------------------------------------------------------- 1 | import sys, json, logging 2 | from ... import cnscom 3 | from .. import exception 4 | from ._completions import complete_ident 5 | ### 6 | 7 | name = 'tail' 8 | cmdhelp = 'Display the last part of a log' 9 | 10 | ### 11 | 12 | L = logging.getLogger('tail') 13 | 14 | ### 15 | 16 | def init_parser(parser): 17 | parser.add_argument('-l','--log-stream', choices=['stdout','stderr'], default='stderr', help='Specify which standard stream to use (default is stderr)') 18 | parser.add_argument('-f', '--follow', action='store_true', help='Causes tail command to not stop when end of stream is reached, but rather to wait for additional data to be appended to the input') 19 | parser.add_argument('-n', '--lines', metavar='N', type=int, default=40, help='Output the last N lines, instead of the last 40') 20 | parser.add_argument('program', help='Specify the program in scope of the command') 21 | 22 | ### 23 | 24 | def complete(console, text, line, begidx, endidx): 25 | textst = text.strip() 26 | ret = [] 27 | ret.extend(complete_ident(console, textst)) 28 | return ret 29 | 30 | ### 31 | 32 | 33 | def main(cnsapp, args): 34 | 35 | params = { 36 | 'program': args.program, 37 | 'stream': args.log_stream, 38 | 'lines': args.lines, 39 | 'tailf': args.follow, 40 | } 41 | ret = cnsapp.cnssvrcall( 42 | cnscom.callid_tail, 43 | json.dumps(params), 44 | auto_connect=True 45 | ) 46 | 47 | sys.stdout.write(ret) 48 | 49 | if not args.follow: 50 | return 51 | 52 | # Handle tail -f mode 53 | try: 54 | while 1: 55 | retype, params = cnscom.svrresp(cnsapp.ctlconsock, hang_detector=False) 56 | if retype == cnscom.resp_tailf_data: 57 | sys.stdout.write(params) 58 | else: 59 | raise RuntimeError("Unknown/invalid server response: {0}".format(retype)) 60 | 61 | except KeyboardInterrupt: 62 | print 63 | 64 | except Exception, e: 65 | L.error("Tail failed: {0}".format(e)) 66 | 67 | params = { 68 | 'program': args.program, 69 | 'stream': args.log_stream, 70 | } 71 | ret = cnsapp.cnssvrcall( 72 | cnscom.callid_tailf_stop, 73 | json.dumps(params) 74 | ) 75 | -------------------------------------------------------------------------------- /ramona/console/cmd/who.py: -------------------------------------------------------------------------------- 1 | import json, logging, time 2 | from ... import cnscom 3 | 4 | ### 5 | 6 | name = 'who' 7 | cmdhelp = 'Show users currently connected the server' 8 | 9 | ### 10 | 11 | L = logging.getLogger('who') 12 | 13 | ### 14 | 15 | def init_parser(parser): 16 | return 17 | 18 | def main(cnsapp, args): 19 | print "Connected clients:" 20 | ret = cnsapp.cnssvrcall( 21 | cnscom.callid_who, 22 | auto_connect=True 23 | ) 24 | whoparsed = json.loads(ret) 25 | for whoitem in whoparsed: 26 | print "{}{} @ {}".format( 27 | '*' if whoitem['me'] else ' ', 28 | nice_addr(whoitem['descr'], whoitem['address']), 29 | time.strftime("%d-%m-%Y %H:%M:%S", time.localtime(whoitem['connected_at'])) 30 | ) 31 | 32 | def nice_addr(descr, address): 33 | sock_family, sock_type, sock_proto, sock_ssl = descr 34 | 35 | if sock_proto == 'IPPROTO_TCP' and sock_family == 'AF_INET6': 36 | return " TCP [{}]:{}{}".format(address[0], address[1], ' SSL' if sock_ssl else '') 37 | elif sock_proto == 'IPPROTO_TCP' and sock_family == 'AF_INET': 38 | return " TCP {}:{}{}".format(address[0], address[1], ' SSL' if sock_ssl else '') 39 | elif sock_family == 'AF_UNIX': 40 | return "UNIX {}{}".format(address, ' SSL' if sock_ssl else '') 41 | else: 42 | return "{} {}".format(descr, address) 43 | -------------------------------------------------------------------------------- /ramona/console/cmd/wininstall.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pywintypes # from Python Win32 3 | from ..winsvc import w32_install_svc 4 | 5 | ### 6 | 7 | name = 'wininstall' 8 | cmdhelp = 'Install the Ramona server as a Windows Service (Windows only)' 9 | 10 | ### 11 | 12 | 13 | L = logging.getLogger('wininstall') 14 | 15 | ### 16 | 17 | def init_parser(parser): 18 | parser.add_argument('-d', '--dont-start', action='store_true', help="Don't start service after installation") 19 | 20 | parser.add_argument('-S','--server-only', action='store_true', help='When service is acticated start only the Ramona server, not programs') 21 | parser.add_argument('program', nargs='*', help='Optionally specify program(s) in scope of the command. If none is given, all programs are considered in scope') 22 | 23 | #TODO: Auto-start of service (after reboot) enable (default) / disable 24 | 25 | ### 26 | 27 | def main(cnsapp, args): 28 | try: 29 | cls = w32_install_svc( 30 | start=not args.dont_start, 31 | server_only=args.server_only, 32 | programs=args.program if len(args.program) > 0 else None 33 | ) 34 | except pywintypes.error, e: 35 | L.error("Error when installing Windows service: {0} ({1})".format(e.strerror, e.winerror)) 36 | return 3 # Exit code 37 | 38 | if args.dont_start: 39 | L.info("Windows service '{0}' has been installed successfully.".format(cls._svc_name_)) 40 | else: 41 | L.info("Windows service '{0}' has been installed and started successfully.".format(cls._svc_name_)) 42 | -------------------------------------------------------------------------------- /ramona/console/cmd/winuninstall.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pywintypes # from Python Win32 3 | from ..winsvc import w32_uninstall_svc 4 | 5 | ### 6 | 7 | name = 'winuninstall' 8 | cmdhelp = 'Uninstall the Ramona Windows Service (Windows only)' 9 | 10 | ### 11 | 12 | 13 | L = logging.getLogger('winuninstall') 14 | 15 | ### 16 | 17 | def init_parser(parser): 18 | pass 19 | 20 | ### 21 | 22 | def main(cnsapp, args): 23 | try: 24 | cls = w32_uninstall_svc() 25 | except pywintypes.error, e: 26 | L.error("Error when uninstalling Windows service: {0} ({1})".format(e.strerror, e.winerror)) 27 | return 3 # Exit code 28 | 29 | L.info("Windows service '{0}' has been uninstalled.".format(cls._svc_name_)) 30 | -------------------------------------------------------------------------------- /ramona/console/cnsapp.py: -------------------------------------------------------------------------------- 1 | import sys, os, socket, errno, logging, time, json, inspect 2 | from ..config import config, read_config, config_files 3 | from ..utils import launch_server_daemonized 4 | from .. import cnscom, socketuri 5 | from .parser import argparser 6 | from . import exception 7 | 8 | ### 9 | 10 | L = logging.getLogger("cnsapp") 11 | 12 | ### 13 | 14 | class console_app(object): 15 | ''' 16 | Console application (base for custom implementations) 17 | @ivar config: Configuration dictionary linked from ramona.config (shortcut for ramona tool procedures) 18 | ''' 19 | 20 | def __init__(self, configuration): 21 | ''' 22 | @param configuration: string or list of configuration files that will be used by Ramona. This is application level configuration. 23 | ''' 24 | # Change directory to location of user console script 25 | os.chdir(os.path.dirname(sys.argv[0])) 26 | 27 | # Check if this is request for proxy tool - and avoid parsing 28 | if len(sys.argv) > 1: 29 | for mn in dir(self): 30 | fn = getattr(self, mn) 31 | if not hasattr(fn, '__proxy_tool'): continue 32 | if mn == sys.argv[1]: 33 | ret = fn(sys.argv[1:]) 34 | sys.exit(ret) 35 | 36 | # Parse command line arguments 37 | self.argparser = argparser(self) 38 | 39 | if (len(sys.argv) < 2): 40 | # Default command 41 | argv = ['console'] 42 | else: 43 | argv = None 44 | 45 | self.argparser.parse(argv) 46 | 47 | # Read config 48 | if self.argparser.args.config is None: 49 | if isinstance(configuration, basestring): 50 | configuration = [configuration] 51 | else: 52 | pass 53 | else: 54 | configuration = self.argparser.args.config 55 | for config_file in configuration: 56 | config_file = config_file.strip() 57 | if not os.path.isfile(config_file): 58 | print("Cannot find configuration file {0}".format(config_file)) 59 | sys.exit(exception.configuration_error.exitcode) 60 | 61 | try: 62 | read_config(configuration, use_env=False) 63 | except Exception, e: 64 | print("{0}".format(e)) 65 | sys.exit(exception.configuration_error.exitcode) 66 | 67 | self.config = config 68 | 69 | # Configure logging 70 | llvl = logging.INFO 71 | if self.argparser.args.silent: llvl = logging.ERROR 72 | if self.argparser.args.debug: llvl = logging.DEBUG 73 | logging.basicConfig( 74 | level=llvl, 75 | stream=sys.stderr, 76 | format="%(asctime)s %(levelname)s: %(message)s", 77 | ) 78 | if self.argparser.args.debug: 79 | L.debug("Debug output is enabled.") 80 | 81 | L.debug("Configuration read from: {0}".format(', '.join(config_files))) 82 | 83 | logdir = self.config.get('general', 'logdir') 84 | if not os.path.isdir(logdir): 85 | L.warning("Log directory '{}' not found.".format(logdir)) 86 | 87 | 88 | # Prepare server connection factory 89 | self.cnsconuri = socketuri.socket_uri(config.get('ramona:console','serveruri')) 90 | self.ctlconsock = None 91 | 92 | 93 | def run(self): 94 | try: 95 | ec = self.argparser.execute(self) 96 | except exception.ramona_runtime_errorbase, e: 97 | L.error("{0}".format(e)) 98 | ec = e.exitcode 99 | except KeyboardInterrupt, e: 100 | ec = 0 101 | except AssertionError, e: 102 | L.exception("Assertion failed:") 103 | ec = 101 # Assertion failed exit code 104 | except Exception, e: 105 | errstr = "{0}".format(e) 106 | if len(errstr) == 0: errstr=e.__repr__() 107 | L.error(errstr) 108 | ec = 100 # Generic error exit code 109 | sys.exit(ec if ec is not None else 0) 110 | 111 | 112 | def connect(self): 113 | if self.ctlconsock is None: 114 | try: 115 | self.ctlconsock = self.cnsconuri.create_socket_connect() 116 | except socket.error, e: 117 | if e.errno == errno.ECONNREFUSED: return None 118 | if e.errno == errno.ENOENT and self.cnsconuri.protocol == 'unix': return None 119 | raise 120 | 121 | server_init_params_ret = cnscom.svrcall(self.ctlconsock, cnscom.callid_init, '') 122 | server_init_params = json.loads(server_init_params_ret) 123 | server_version = server_init_params.get("version", None) 124 | if server_version is not None: 125 | from .. import version as ramona_version 126 | client_version = ramona_version 127 | if server_version != client_version: 128 | L.warn("Version mismatch. The server version '{0}' is different from the console version '{1}'. The console may malfunction.".format(server_version, client_version)) 129 | 130 | return self.ctlconsock 131 | 132 | 133 | def cnssvrcall(self, callid, params="", auto_connect=False, auto_server_start=False): 134 | ''' 135 | Console-server call (wrapper to cnscom.svrcall) 136 | 137 | @param auto_connect: Automatically establish server connection if not present 138 | @param auto_server_start: Automatically start server if not running and establish connection 139 | ''' 140 | assert not (auto_connect & auto_server_start), "Only one of auto_connect and auto_server_start can be true" 141 | if auto_connect: 142 | if self.ctlconsock is None: 143 | s = self.connect() 144 | if s is None: 145 | raise exception.server_not_responding_error("Server is not responding - maybe it isn't running.") 146 | 147 | elif auto_server_start: 148 | # Fist check if ramona server is running and if not, launch that 149 | s = self.auto_server_start() 150 | 151 | else: 152 | assert self.ctlconsock is not None 153 | 154 | try: 155 | return cnscom.svrcall(self.ctlconsock, callid, params) 156 | except socket.error: 157 | pass 158 | 159 | if auto_connect or auto_server_start: 160 | L.debug("Reconnecting to server ...") 161 | 162 | self.ctlconsock = None 163 | s = self.connect() 164 | if s is None: 165 | raise exception.server_not_responding_error("Server is not responding - maybe it isn't running.") 166 | 167 | return cnscom.svrcall(self.ctlconsock, callid, params) 168 | 169 | 170 | def wait_for_svrexit(self): 171 | if self.ctlconsock is None: return 172 | while True: 173 | x = self.ctlconsock.recv(4096) 174 | if len(x) == 0: break 175 | self.ctlconsock 176 | self.ctlconsock = None 177 | 178 | 179 | def auto_server_start(self): 180 | s = self.connect() 181 | if s is None: 182 | L.debug("It looks like Ramona server is not running - launching server") 183 | launch_server_daemonized() 184 | 185 | for _ in range(100): # Check server availability for next 10 seconds 186 | # TODO: Also improve 'crash-start' detection (to reduce lag when server fails to start) 187 | time.sleep(0.1) 188 | s = self.connect() 189 | if s is not None: break 190 | 191 | if s is None: 192 | raise exception.server_start_error("Ramona server process start failed") 193 | 194 | return s 195 | 196 | ### 197 | 198 | def tool(fn): 199 | ''' 200 | Tool decorator foc console_app 201 | 202 | Marks function object by '.__tool' attribute 203 | ''' 204 | 205 | if inspect.isfunction(fn): 206 | fn.__tool = fn.func_name 207 | 208 | elif inspect.isclass(fn): 209 | fn.__tool = fn.__name__ 210 | 211 | else: 212 | raise RuntimeError("Unknown type decorated as Ramona tool: {0}".format(fn)) 213 | 214 | return fn 215 | 216 | # 217 | 218 | def proxy_tool(fn): 219 | ''' 220 | Proxy tool (with straight argument passing) decorator foc console_app 221 | 222 | Marks function object by '.__proxy_tool' attribute 223 | ''' 224 | fn.__proxy_tool = fn.func_name 225 | return fn 226 | -------------------------------------------------------------------------------- /ramona/console/exception.py: -------------------------------------------------------------------------------- 1 | class ramona_runtime_errorbase(RuntimeError): 2 | exitcode = 100 3 | 4 | class server_not_responding_error(ramona_runtime_errorbase): 5 | exitcode = 2 6 | 7 | class server_start_error(ramona_runtime_errorbase): 8 | exitcode = 3 9 | 10 | class configuration_error(ramona_runtime_errorbase): 11 | exitcode = 98 12 | 13 | class parameters_error(ramona_runtime_errorbase): 14 | exitcode = 99 15 | -------------------------------------------------------------------------------- /ramona/console/parser.py: -------------------------------------------------------------------------------- 1 | import sys, os, argparse, inspect 2 | 3 | ### 4 | 5 | class _parser_base(argparse.ArgumentParser): 6 | 7 | argparser_kwargs = None # To be overridden in final parser implementation class 8 | subparser_kwargs = None # To be overridden in final parser implementation class 9 | 10 | def __init__(self, cnsapp): 11 | 12 | # Build parser 13 | argparse.ArgumentParser.__init__(self, **self.argparser_kwargs) 14 | 15 | self.subparsers = self.add_subparsers( 16 | dest='subcommand', 17 | title='subcommands', 18 | parser_class=argparse.ArgumentParser, 19 | ) 20 | 21 | # Adding sub-commands ... 22 | self.subcommands = {} 23 | for cmd in self.build_cmdlist(): 24 | subparser = self.subparsers.add_parser(cmd.name, help=cmd.cmdhelp, **self.subparser_kwargs) 25 | subparser.description = cmd.cmdhelp 26 | cmd.init_parser(subparser) 27 | self.subcommands[cmd.name] = cmd 28 | 29 | #Iterate via application object to find 'tool/__tool' and 'proxy_tool/__proxy_tool' (decorated method) 30 | for mn in dir(cnsapp): 31 | fn = getattr(cnsapp, mn) 32 | if hasattr(fn, '__tool'): 33 | subparser = self.subparsers.add_parser(mn, help=fn.__doc__) 34 | if inspect.ismethod(fn): 35 | self.subcommands[mn] = fn.im_func # Unbound method 36 | elif inspect.isclass(fn): 37 | # Initialize tool given by a class 38 | toolobj = fn() 39 | if hasattr(toolobj,'init_parser'): 40 | toolobj.init_parser(cnsapp, subparser) 41 | self.subcommands[mn] = toolobj 42 | 43 | else: 44 | raise RuntimeError("Unknown type of Ramona tool object: {0}".format(fn)) 45 | 46 | elif hasattr(fn, '__proxy_tool'): 47 | self.subparsers.add_parser(mn, help=fn.__doc__) 48 | # Not subcommand as proxy tools are handled prior argument parsing 49 | 50 | 51 | def build_cmdlist(self): 52 | from .cmd import start 53 | yield start 54 | 55 | from .cmd import stop 56 | yield stop 57 | 58 | from .cmd import restart 59 | yield restart 60 | 61 | from .cmd import status 62 | yield status 63 | 64 | from .cmd import help 65 | yield help 66 | 67 | from .cmd import tail 68 | yield tail 69 | 70 | from .cmd import who 71 | yield who 72 | 73 | from .cmd import notify 74 | yield notify 75 | 76 | if sys.platform == 'win32': 77 | from .cmd import wininstall 78 | yield wininstall 79 | 80 | from .cmd import winuninstall 81 | yield winuninstall 82 | 83 | 84 | def parse(self, argv): 85 | self.args = None # This is to allow re-entrant parsing 86 | self.args = self.parse_args(argv) 87 | 88 | 89 | def execute(self, cnsapp): 90 | if self.args.subcommand == 'help': 91 | # Help is given by special treatment as this is actually function of parser itself 92 | self.print_help() 93 | return 94 | 95 | cmdobj = self.subcommands[self.args.subcommand] 96 | 97 | if hasattr(cmdobj,'__call__'): 98 | return cmdobj(cnsapp) 99 | else: 100 | return cmdobj.main(cnsapp, self.args) 101 | 102 | # 103 | 104 | class argparser(_parser_base): 105 | 106 | argparser_kwargs = {} 107 | subparser_kwargs = {} 108 | 109 | def __init__(self, cnsapp): 110 | 111 | # Prepare description (with Ramona version) 112 | from .. import version as ramona_version 113 | self.argparser_kwargs['description'] = 'Powered by Ramona (version {0}).'.format(ramona_version) 114 | 115 | _parser_base.__init__(self, cnsapp) 116 | 117 | # Add config file option 118 | self.add_argument('-c', '--config', metavar="CONFIGFILE", action='append', help='Specify configuration file(s) to read (this option can be given more times). This will override build-in application level configuration.') 119 | 120 | # Add debug log level option 121 | self.add_argument('-d', '--debug', action='store_true', help='Enable debug (verbose) output.') 122 | 123 | # Add silent log level option 124 | self.add_argument('-s', '--silent', action='store_true', help='Enable silent mode of operation (only errors are printed).') 125 | 126 | 127 | def build_cmdlist(self): 128 | for cmd in _parser_base.build_cmdlist(self): yield cmd 129 | 130 | from .cmd import console 131 | yield console 132 | 133 | from .cmd import server 134 | yield server 135 | 136 | # 137 | 138 | class consoleparser(_parser_base): 139 | 140 | argparser_kwargs = {'add_help': False, 'usage': argparse.SUPPRESS} 141 | subparser_kwargs = {'usage': argparse.SUPPRESS} 142 | 143 | def build_cmdlist(self): 144 | for cmd in _parser_base.build_cmdlist(self): yield cmd 145 | 146 | from .cmd import exit 147 | yield exit 148 | 149 | 150 | def error(self, message): 151 | print "Error:", message 152 | raise SyntaxError() 153 | 154 | -------------------------------------------------------------------------------- /ramona/console/winsvc.py: -------------------------------------------------------------------------------- 1 | #See http://code.activestate.com/recipes/551780/ for details 2 | 3 | # To debug use: 4 | # > c:\Python27\Lib\site-packages\win32\pythonservice.exe -debug ramona-test ramonahttpfend 5 | 6 | import os, sys, time 7 | from os.path import splitext, abspath, join, dirname 8 | from sys import modules 9 | 10 | import win32serviceutil 11 | import win32service 12 | import win32api 13 | 14 | from ..config import config, config_files 15 | 16 | ### 17 | 18 | class w32_ramona_service(win32serviceutil.ServiceFramework): 19 | 20 | 21 | _svc_name_ = None 22 | _svc_display_name_ = 'Ramona Demo Service' 23 | 24 | @classmethod 25 | def configure(cls): 26 | assert cls._svc_name_ is None 27 | cls._svc_name_ = config.get('general','appname') 28 | # TODO: Allow user to provide display name via config 29 | cls._svc_display_name_ = config.get('general','appname') 30 | 31 | 32 | def __init__(self, *args): 33 | assert self._svc_name_ is None 34 | servicename = args[0][0] 35 | 36 | self.log("Ramona service '{0}' is starting".format(servicename)) 37 | 38 | # Read working directory from registry and change to it 39 | directory = win32serviceutil.GetServiceCustomOption(servicename, 'directory') 40 | os.chdir(directory) 41 | 42 | # Set Ramona config environment variable to ensure proper configuration files load 43 | os.environ['RAMONA_CONFIG'] = win32serviceutil.GetServiceCustomOption(servicename, 'config') 44 | 45 | from ..server.svrapp import server_app 46 | self.svrapp = server_app() 47 | self.configure() 48 | 49 | win32serviceutil.ServiceFramework.__init__(self, *args) 50 | 51 | 52 | def log(self, msg): 53 | import servicemanager 54 | servicemanager.LogInfoMsg(str(msg)) 55 | 56 | 57 | def SvcDoRun(self): 58 | self.ReportServiceStatus(win32service.SERVICE_START_PENDING) 59 | 60 | try: 61 | self.ReportServiceStatus(win32service.SERVICE_RUNNING) 62 | self.log('Ramona service {0} is running'.format(self._svc_name_)) 63 | self.svrapp.run() 64 | self.log('Quak') 65 | 66 | except SystemExit, e: 67 | self.log('SystemExit: {0}'.format(e)) 68 | 69 | except Exception, e: 70 | self.log('Exception: {0}'.format(e)) 71 | 72 | self.svrapp = None 73 | #self.ReportServiceStatus(win32service.SERVICE_STOPPED) 74 | 75 | 76 | def SvcStop(self): 77 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 78 | 79 | if self.svrapp is not None: 80 | self.svrapp.exitwatcher.send() 81 | while self.svrapp is not None: 82 | time.sleep(1) 83 | 84 | self.log('Ramona service {0} is stopped'.format(self._svc_name_)) 85 | #self.ReportServiceStatus(win32service.SERVICE_STOPPED) 86 | 87 | ### 88 | 89 | def w32_install_svc(start=False, server_only=True, programs=None): 90 | '''Install Windows Ramona Service''' 91 | 92 | import logging 93 | L = logging.getLogger('winsvc') 94 | 95 | directory = abspath(dirname(sys.argv[0])) # Find where console python prog is launched from ... 96 | 97 | cls = w32_ramona_service 98 | if cls._svc_name_ is None: cls.configure() 99 | 100 | try: 101 | module_path=modules[cls.__module__].__file__ 102 | except AttributeError: 103 | # maybe py2exe went by 104 | from sys import executable 105 | module_path=executable 106 | module_file = splitext(abspath(join(module_path,'..','..', '..')))[0] 107 | cls._svc_reg_class_ = '{0}\\ramona.console.winsvc.{1}'.format(module_file, cls.__name__) 108 | 109 | win32api.SetConsoleCtrlHandler(lambda x: True, True) # Service will stop on logout if False 110 | 111 | # Prepare command line 112 | cmdline = [] 113 | if server_only: cmdline.append('-S') 114 | elif programs is not None: cmdline.extend(programs) 115 | 116 | # Install service 117 | win32serviceutil.InstallService( 118 | cls._svc_reg_class_, 119 | cls._svc_name_, 120 | cls._svc_display_name_, 121 | startType = win32service.SERVICE_AUTO_START, 122 | exeArgs = ' '.join(cmdline), 123 | ) 124 | 125 | # Set directory from which Ramona server should be launched ... 126 | win32serviceutil.SetServiceCustomOption(cls._svc_name_, 'directory', directory) 127 | win32serviceutil.SetServiceCustomOption(cls._svc_name_, 'config', ';'.join(config_files)) 128 | 129 | L.debug("Service {0} installed".format(cls._svc_name_)) 130 | 131 | if start: 132 | x = win32serviceutil.StartService(cls._svc_name_) 133 | L.debug("Service {0} is starting ...".format(cls._svc_name_)) 134 | #TODO: Wait for service start to check start status ... 135 | L.debug("Service {0} started".format(cls._svc_name_)) 136 | 137 | return cls 138 | 139 | 140 | def w32_uninstall_svc(): 141 | '''Uninstall (remove) Windows Ramona Service''' 142 | 143 | import logging 144 | L = logging.getLogger('winsvc') 145 | 146 | cls = w32_ramona_service 147 | if cls._svc_name_ is None: cls.configure() 148 | 149 | scvType, svcState, svcControls, err, svcErr, svcCP, svcWH = win32serviceutil.QueryServiceStatus(cls._svc_name_) 150 | 151 | if svcState == win32service.SERVICE_RUNNING: 152 | L.debug("Service {0} is stopping ...".format(cls._svc_name_)) 153 | win32serviceutil.StopService(cls._svc_name_) 154 | L.debug("Service {0} is stopped.".format(cls._svc_name_)) 155 | 156 | win32serviceutil.RemoveService(cls._svc_name_) 157 | 158 | return cls 159 | -------------------------------------------------------------------------------- /ramona/httpfend/401.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {appname} - Ramona administation console 6 | 7 | 8 | 9 | 10 |
11 |

{appname}

Ramona web administration console

12 |
13 |

User not authenticated!

14 | The provided username and password does not match the configuration in [{configsection}] section. 15 |
Please review the configuration and try to login again. 16 |
17 | 18 |
19 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /ramona/httpfend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/ramona/httpfend/__init__.py -------------------------------------------------------------------------------- /ramona/httpfend/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import httpfend_app 2 | 3 | 4 | if __name__ == '__main__': 5 | app = httpfend_app() 6 | app.run() 7 | -------------------------------------------------------------------------------- /ramona/httpfend/_tailf.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pyev 3 | from .. import cnscom 4 | 5 | ### 6 | 7 | L = logging.getLogger("httpfendapp") 8 | 9 | ### 10 | 11 | class tail_f_handler(object): 12 | 13 | def __init__(self, req_handler, cnsconn): 14 | self.loop = pyev.Loop() 15 | self.watchers = [] 16 | self.req_handler = req_handler 17 | self.cnsconn = cnsconn 18 | self.watchers.append(self.loop.io(req_handler.rfile._sock, pyev.EV_READ, self.__on_rfile_io)) 19 | self.watchers.append(self.loop.io(cnsconn._sock, pyev.EV_READ, self.__on_cns_io)) 20 | 21 | def run(self): 22 | for watcher in self.watchers: 23 | watcher.start() 24 | self.loop.start() 25 | 26 | def __on_cns_io(self, watcher, events): 27 | retype, params = cnscom.svrresp(self.cnsconn, hang_detector=False) 28 | if retype == cnscom.resp_tailf_data: 29 | self.req_handler.wfile.write(params) 30 | else: 31 | raise RuntimeError("Unknown/invalid server response: {0}".format(retype)) 32 | 33 | def __on_rfile_io(self, watcher, events): 34 | buf = self.req_handler.rfile.read(1) 35 | if len(buf) == 0: 36 | L.debug("Closing the tailf loop for client {0}".format(self.req_handler.client_address)) 37 | self.loop.stop() 38 | return 39 | else: 40 | L.warning("Unexpected data received from the client: {}".format(buf)) 41 | -------------------------------------------------------------------------------- /ramona/httpfend/app.py: -------------------------------------------------------------------------------- 1 | import sys, os, socket, ConfigParser, errno, logging, signal, threading, itertools, collections 2 | import pyev 3 | from ..config import config, read_config, get_numeric_loglevel, config_defaults 4 | from .. import socketuri 5 | from ._request_handler import ramona_http_req_handler 6 | 7 | ### 8 | 9 | L = logging.getLogger("httpfendapp") 10 | 11 | ### 12 | 13 | class httpfend_app(object): 14 | 15 | STOPSIGNALS = [signal.SIGINT, signal.SIGTERM] 16 | NONBLOCKING = frozenset([errno.EAGAIN, errno.EWOULDBLOCK]) 17 | # Maximum number of worker threads serving the client requests 18 | MAX_WORKER_THREADS = 10 19 | 20 | def __init__(self): 21 | # Read config 22 | read_config() 23 | 24 | # Configure logging 25 | try: 26 | loglvl = get_numeric_loglevel(config.get(os.environ['RAMONA_SECTION'], 'loglevel')) 27 | except: 28 | loglvl = logging.INFO 29 | logging.basicConfig( 30 | level=loglvl, 31 | stream=sys.stderr, 32 | format="%(asctime)s %(levelname)s: %(message)s", 33 | ) 34 | 35 | try: 36 | self.listenaddr = config.get(os.environ['RAMONA_SECTION'], 'listen') 37 | except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): 38 | self.listenaddr = config_defaults['ramona:httpfend']['listenaddr'] 39 | 40 | self.username = None 41 | self.password = None 42 | try: 43 | self.username = config.get(os.environ['RAMONA_SECTION'], 'username') 44 | self.password = config.get(os.environ['RAMONA_SECTION'], 'password') 45 | except: 46 | pass 47 | 48 | if self.username is not None and self.password is None: 49 | L.fatal("Configuration error: 'username' option is set, but 'password' option is not set. Please set 'password'") 50 | sys.exit(1) 51 | 52 | self.logmsgcnt = itertools.count() 53 | self.logmsgs = dict() 54 | 55 | self.workers = collections.deque() 56 | self.dyingws = collections.deque() # Dying workers 57 | 58 | self.svrsockets = [] 59 | 60 | for addr in self.listenaddr.split(','): 61 | socket_factory = socketuri.socket_uri(addr) 62 | try: 63 | socks = socket_factory.create_socket_listen() 64 | except socket.error, e: 65 | L.fatal("It looks like that server is already running: {0}".format(e)) 66 | sys.exit(1) 67 | self.svrsockets.extend(socks) 68 | 69 | if len(self.svrsockets) == 0: 70 | L.fatal("There is no http server listen address configured - considering this as fatal error") 71 | sys.exit(1) 72 | 73 | self.loop = pyev.default_loop() 74 | self.watchers = [ 75 | pyev.Signal(sig, self.loop, self.__terminal_signal_cb) for sig in self.STOPSIGNALS 76 | ] 77 | self.dyingwas = pyev.Async(self.loop, self.__wdied_cb) # Dying workers async. signaling 78 | self.watchers.append(self.dyingwas) 79 | 80 | for sock in self.svrsockets: 81 | sock.setblocking(0) 82 | self.watchers.append(pyev.Io(sock._sock, pyev.EV_READ, self.loop, self.__on_accept, data=sock._sock.fileno())) 83 | 84 | 85 | def run(self): 86 | for sock in self.svrsockets: 87 | sock.listen(socket.SOMAXCONN) 88 | L.debug("Ramona HTTP frontend is listening at {0}".format(sock.getsockname())) 89 | for watcher in self.watchers: 90 | watcher.start() 91 | 92 | L.info('Ramona HTTP frontend started and is available at {0}'.format(self.listenaddr)) 93 | 94 | # Launch loop 95 | try: 96 | self.loop.start() 97 | finally: 98 | # Stop accepting new work 99 | for sock in self.svrsockets: sock.close() 100 | 101 | # Join threads ... 102 | for i in range(len(self.workers)-1,-1,-1): 103 | w = self.workers[i] 104 | w.join(2) 105 | if not w.is_alive(): del self.workers[i] 106 | 107 | if len(self.workers) > 0: 108 | L.warning("Not all workers threads exited nicely - expect hang during exit") 109 | 110 | 111 | def __on_accept(self, watcher, events): 112 | # First find relevant socket 113 | sock = None 114 | for s in self.svrsockets: 115 | if s.fileno() == watcher.data: 116 | sock = s 117 | break 118 | if sock is None: 119 | L.warning("Received accept request on unknown socket {0}".format(watcher.fd)) 120 | return 121 | # Accept all connection that are pending in listen backlog 122 | while True: 123 | try: 124 | clisock, address = sock.accept() 125 | 126 | except socket.error as err: 127 | if err.args[0] in self.NONBLOCKING: 128 | break 129 | else: 130 | raise 131 | else: 132 | clisock.setblocking(1) 133 | num_workers = len(self.workers) 134 | if num_workers >= self.MAX_WORKER_THREADS: 135 | L.error("There are already {0} worker threads, which is >= MAX_WORKER_THREADS = {1}. Not creating a new thread for client {2}".format( 136 | num_workers, self.MAX_WORKER_THREADS, address)) 137 | continue 138 | 139 | worker = _request_worker(clisock, address, self) 140 | L.debug("Request from client {0} is processed by thread {1}".format(address, worker.name)) 141 | worker.start() 142 | self.workers.append(worker) 143 | 144 | 145 | def __terminal_signal_cb(self, watcher, events): 146 | watcher.loop.stop() 147 | 148 | 149 | def __wdied_cb(self, _watcher, _events): 150 | '''Iterate thru list of workers and remove dead threads''' 151 | while len(self.dyingws) > 0: 152 | w = self.dyingws.pop() 153 | L.debug("Joining thread {0}".format(w.name)) 154 | w.join() 155 | self.workers.remove(w) 156 | 157 | # 158 | 159 | class _request_worker(threading.Thread): 160 | 161 | def __init__(self, sock, address, server): 162 | threading.Thread.__init__(self) 163 | self.name = "HttpfendRequestWorker-{0}".format(self.name) 164 | self.sock = sock 165 | self.address = address 166 | self.server = server 167 | 168 | def run(self): 169 | try: 170 | ramona_http_req_handler(self.sock, self.address, self.server) 171 | self.sock.close() 172 | except: 173 | L.exception("Uncaught exception during worker thread execution:") 174 | finally: 175 | self.server.dyingws.append(self) 176 | self.server.dyingwas.send() 177 | 178 | -------------------------------------------------------------------------------- /ramona/httpfend/index.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {appname} - Ramona administation console 6 | 7 | 8 | 14 | 15 | 16 |
17 |

{appname}

Ramona web administration console

18 | {logmsg} 19 |

20 | Refresh 21 | Start all 22 | Stop all 23 | Restart all 24 | Start all (force) 25 | 26 |

27 | {statuses} 28 | 33 |
34 | 42 |
43 | 44 | 45 | 46 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /ramona/httpfend/log_frame.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {appname} - Ramona administation console - log 6 | 7 | 29 | 30 | 31 | 32 | 33 |
34 | 38 |
39 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /ramona/httpfend/static/bootstrap/css/.gitignore: -------------------------------------------------------------------------------- 1 | /bootstrap.css 2 | /bootstrap-responsive.css 3 | -------------------------------------------------------------------------------- /ramona/httpfend/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/ramona/httpfend/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /ramona/httpfend/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ateska/ramona/46e5e91782c82f0a09aa25f302e0feb4fad7d4a8/ramona/httpfend/static/img/favicon.ico -------------------------------------------------------------------------------- /ramona/httpfend/static/miniajax/miniajax.js: -------------------------------------------------------------------------------- 1 | function $(e){if(typeof e=='string')e=document.getElementById(e);return e}; 2 | function collect(a,f){var n=[];for(var i=0;i= 0 and self.pattern[self.matchsel] != c: 28 | self.startpos += self.shifts[self.matchsel] 29 | self.matchsel -= self.shifts[self.matchsel] 30 | self.matchsel += 1 31 | if self.matchsel == self.patternlen: 32 | return self.startpos 33 | return -1 34 | 35 | -------------------------------------------------------------------------------- /ramona/sendmail.py: -------------------------------------------------------------------------------- 1 | import urlparse, smtplib, logging, getpass, socket, os, string 2 | from email.mime.text import MIMEText 3 | from .config import config 4 | ### 5 | 6 | L = logging.getLogger('sendmail') 7 | 8 | ### 9 | 10 | # Configure urlparse 11 | if 'smtp' not in urlparse.uses_query: urlparse.uses_query.append('smtp') 12 | 13 | ### 14 | 15 | class send_mail(object): 16 | 17 | def __init__(self, deliveryuri, sender=None): 18 | delurl = urlparse.urlparse(deliveryuri) 19 | if delurl.scheme == 'smtp' : 20 | if delurl.hostname is None: 21 | raise RuntimeError("Delivery URL '{0}' has no hostname".format(deliveryuri)) 22 | else: 23 | self.hostname = delurl.hostname 24 | self.port = delurl.port if delurl.port is not None else 25 25 | self.username = delurl.username 26 | self.password = delurl.password 27 | self.params = dict(urlparse.parse_qsl(delurl.query)) 28 | 29 | if sender is None: 30 | self.sender = config.get('ramona:notify','sender') 31 | else: 32 | self.sender = sender 33 | 34 | if self.sender == '': 35 | self.sender = self.get_default_fromaddr() 36 | elif self.sender[:1] == '<': 37 | raise RuntimeError('Invalid sender option: {0}'.format(self.sender)) 38 | 39 | else: 40 | raise RuntimeError("Unknown delivery method in {0}".format(deliveryuri)) 41 | 42 | self.receiver = map(string.strip, config.get('ramona:notify', 'receiver').split(',')) 43 | 44 | 45 | def send(self, recipients, subject, mail_body, sender=None): 46 | 47 | if sender is None: sender = self.sender 48 | 49 | msg = MIMEText(mail_body, 'plain', 'utf-8') 50 | msg['Subject'] = subject 51 | msg['From'] = sender 52 | msg['To'] = ', '.join(recipients) 53 | 54 | s = smtplib.SMTP(self.hostname, self.port) 55 | if self.params.get('tls', '1') == '1': s.starttls() 56 | if self.username is not None and self.password is not None: 57 | s.login(self.username, self.password) 58 | 59 | s.sendmail(sender, recipients, msg.as_string()) 60 | s.quit() 61 | 62 | 63 | @staticmethod 64 | def get_default_fromaddr(): 65 | hostname = socket.getfqdn() 66 | if hostname == 'localhost': hostname = socket.gethostname() 67 | return "{0}@{1}".format(getpass.getuser(), hostname) 68 | -------------------------------------------------------------------------------- /ramona/server/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is module that contains functionality of Ramona server 3 | 4 | There should be no need to use any element from this module in user code - this is purely internal staff. 5 | ''' -------------------------------------------------------------------------------- /ramona/server/__main__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This code is stub/kickstarted for ramona server application 3 | ''' 4 | 5 | # This code can be used to enable remote debugging in PyDev 6 | #Add pydevd to the PYTHONPATH (may be skipped if that path is already added in the PyDev configurations) 7 | #import sys;sys.path.append(r'/opt/eclipse4.2/plugins/org.python.pydev_2.6.0.2012062818/pysrc') 8 | #import sys;sys.path.append(r'/Applications/eclipse/plugins/org.python.pydev.debug_2.5.0.2012040618/pysrc') # Alex Macbook 9 | #import pydevd 10 | #pydevd.settrace() 11 | 12 | ### 13 | 14 | if __name__ == "__main__": 15 | from .svrapp import server_app 16 | svrapp = server_app() 17 | svrapp.run() 18 | -------------------------------------------------------------------------------- /ramona/server/__utest__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .seqctrl import sequence_controller 3 | from ..cnscom import program_state_enum 4 | from .logmed import log_mediator 5 | ### 6 | ''' 7 | To launch unit test: 8 | python -m unittest -v ramona.server.__utest__ 9 | ''' 10 | ### 11 | 12 | class TestSequenceController(unittest.TestCase): 13 | 14 | 15 | class _dummy_program(object): 16 | 17 | def __init__(self, ident, prio): 18 | self.ident = ident 19 | self.priority = prio 20 | self.state = program_state_enum.STOPPED 21 | 22 | 23 | def test_HappyFlow(self): 24 | sc = sequence_controller() 25 | 26 | # Build launchpad sequence 27 | sc.add(self._dummy_program('a',9)) 28 | sc.add(self._dummy_program('b',8)) 29 | sc.add(self._dummy_program('c',9)) 30 | sc.add(self._dummy_program('d',8)) 31 | sc.add(self._dummy_program('e',9)) 32 | sc.add(self._dummy_program('f',6)) 33 | 34 | # Get first set 35 | actps = sc.next() 36 | pset = set(p.ident for p in actps) 37 | self.assertSetEqual(pset,{'a','c','e'}) 38 | 39 | # Cannot continue to next one till got response 40 | self.assertRaises(AssertionError, sc.next) 41 | 42 | # Simulate start of active set and check that 43 | for p in actps: p.state = program_state_enum.STARTING 44 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 45 | self.assertFalse(r) 46 | self.assertRaises(AssertionError, sc.next) 47 | 48 | # Simulate sequential start of programs 49 | actps[0].state = program_state_enum.RUNNING 50 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 51 | self.assertFalse(r) 52 | self.assertRaises(AssertionError, sc.next) 53 | 54 | actps[1].state = program_state_enum.RUNNING 55 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 56 | self.assertFalse(r) 57 | self.assertRaises(AssertionError, sc.next) 58 | 59 | actps[2].state = program_state_enum.RUNNING 60 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 61 | self.assertTrue(r) 62 | 63 | # Now advancing to the next set 64 | actps = sc.next() 65 | pset = set(p.ident for p in actps) 66 | self.assertSetEqual(pset,{'b','d'}) 67 | 68 | # Simulate sequential start of programs 69 | actps[0].state = program_state_enum.RUNNING 70 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 71 | self.assertFalse(r) 72 | self.assertRaises(AssertionError, sc.next) 73 | 74 | actps[1].state = program_state_enum.RUNNING 75 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 76 | self.assertTrue(r) 77 | 78 | # Third step 79 | actps = sc.next() 80 | pset = set(p.ident for p in actps) 81 | self.assertSetEqual(pset,{'f'}) 82 | 83 | actps[0].state = program_state_enum.RUNNING 84 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 85 | self.assertTrue(r) 86 | 87 | actps = sc.next() 88 | self.assertIsNone(actps) 89 | 90 | 91 | def test_LaunchFailure(self): 92 | sc = sequence_controller() 93 | 94 | # Build launchpad sequence 95 | sc.add(self._dummy_program('a',9)) 96 | sc.add(self._dummy_program('b',8)) 97 | sc.add(self._dummy_program('c',9)) 98 | 99 | # Get first set 100 | actps = sc.next() 101 | pset = set(p.ident for p in actps) 102 | self.assertSetEqual(pset,{'a','c'}) 103 | 104 | # Simulate start of active set and check that 105 | for p in actps: p.state = program_state_enum.STARTING 106 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 107 | self.assertFalse(r) 108 | self.assertRaises(AssertionError, sc.next) 109 | 110 | # Simulate sequential start of programs 111 | actps[0].state = program_state_enum.FATAL 112 | r = sc.check(program_state_enum.STARTING, program_state_enum.RUNNING) 113 | 114 | self.assertIsNone(r) 115 | 116 | # 117 | 118 | class TestLogMediator(unittest.TestCase): 119 | 120 | 121 | def test_LogMediatorBasic(self): 122 | 'Constructing log mediator' 123 | 124 | lm = log_mediator('foo_prog', 'stdout', None) 125 | lm.open() 126 | lm.write('Line1\n') 127 | lm.write('Line2\n') 128 | lm.write('Line3\n') 129 | lm.close() 130 | 131 | 132 | def test_LogMediatorLineTail(self): 133 | 'Log mediator line separator' 134 | 135 | lm = log_mediator('foo_prog', 'stdout', None) 136 | lm.open() 137 | 138 | lm.write('Line') 139 | self.assertItemsEqual(lm.tailbuf, ['Line']) 140 | lm.write(' One') 141 | self.assertItemsEqual(lm.tailbuf, ['Line One']) 142 | lm.write('\n') 143 | self.assertItemsEqual(lm.tailbuf, ['Line One\n']) 144 | 145 | lm.write('Line 2\n') 146 | self.assertItemsEqual(lm.tailbuf, [ 147 | 'Line One\n', 148 | 'Line 2\n', 149 | ]) 150 | 151 | lm.write('3Line') 152 | self.assertEqual(lm.tailbuf[-1], '3Line') 153 | lm.write(' 3') 154 | self.assertEqual(lm.tailbuf[-1], '3Line 3') 155 | lm.write('\n') 156 | self.assertItemsEqual(lm.tailbuf, [ 157 | 'Line One\n', 158 | 'Line 2\n', 159 | '3Line 3\n', 160 | ]) 161 | 162 | lm.write('Line 4\nLine 5\nLine 6\n') 163 | self.assertItemsEqual(lm.tailbuf, [ 164 | 'Line One\n', 165 | 'Line 2\n', 166 | '3Line 3\n', 167 | 'Line 4\n', 168 | 'Line 5\n', 169 | 'Line 6\n', 170 | ]) 171 | 172 | lm.write('Line 7\nLine 8\nLine 9') 173 | 174 | self.assertItemsEqual(lm.tailbuf, [ 175 | 'Line One\n', 176 | 'Line 2\n', 177 | '3Line 3\n', 178 | 'Line 4\n', 179 | 'Line 5\n', 180 | 'Line 6\n', 181 | 'Line 7\n', 182 | 'Line 8\n', 183 | 'Line 9', 184 | ]) 185 | 186 | lm.close() 187 | 188 | 189 | def test_LogMediatorLongLineTail(self): 190 | 'Log mediator line separator (long lines)' 191 | 192 | lm = log_mediator('foo_prog', 'stdout', None) 193 | lm.open() 194 | 195 | lm.write('Line 1\n') 196 | self.assertItemsEqual(lm.tailbuf, ['Line 1\n']) 197 | 198 | lm.write('X'*60000) 199 | self.assertEqual(len(lm.tailbuf), 3) 200 | self.assertEqual(lm.tailbuf[1], 'X' * 32512) 201 | self.assertEqual(lm.tailbuf[2], 'X' * 27488) 202 | 203 | lm.write('X'*60000) 204 | self.assertEqual(len(lm.tailbuf), 5) 205 | self.assertEqual(lm.tailbuf[1], 'X' * 32512) 206 | self.assertEqual(lm.tailbuf[2], 'X' * 32512) 207 | self.assertEqual(lm.tailbuf[3], 'X' * 32512) 208 | self.assertEqual(lm.tailbuf[4], 'X' * 22464) 209 | 210 | lm.write('X\n') 211 | self.assertEqual(len(lm.tailbuf), 5) 212 | self.assertEqual(lm.tailbuf[4], 'X' * 22465 + '\n') 213 | 214 | lm.close() 215 | -------------------------------------------------------------------------------- /ramona/server/call_status.py: -------------------------------------------------------------------------------- 1 | import json, time 2 | ### 3 | 4 | def main(svrapp, pfilter=None): 5 | l = svrapp.filter_roaster_iter(pfilter) 6 | ret = [] 7 | for p in l: 8 | i = { 9 | 'ident': p.ident, 10 | 'state': p.state, 11 | 'launch_cnt': p.launch_cnt, 12 | } 13 | if p.subproc is not None: i['pid'] = p.subproc.pid 14 | if p.exit_status is not None: i['exit_status'] = p.exit_status 15 | if p.exit_time is not None: i['exit_time'] = p.exit_time 16 | if p.start_time is not None: 17 | i['start_time'] = p.start_time 18 | if p.exit_time is None: i["uptime"] = time.time() - p.start_time 19 | if p.autorestart_cnt > 0: i['autorestart_cnt'] = p.autorestart_cnt 20 | ret.append(i) 21 | 22 | return json.dumps(ret) 23 | -------------------------------------------------------------------------------- /ramona/server/cnscon.py: -------------------------------------------------------------------------------- 1 | import sys, socket, errno, struct, weakref, json, select, logging, time 2 | import pyev 3 | from .. import cnscom 4 | ### 5 | 6 | L = logging.getLogger("cnscon") 7 | 8 | ### 9 | 10 | if sys.platform != 'win32': 11 | BUFSIZE = select.PIPE_BUF 12 | else: 13 | BUFSIZE = 512 14 | 15 | ### 16 | 17 | class console_connection(object): 18 | '''Server side of console communication IPC''' 19 | 20 | NONBLOCKING = frozenset([errno.EAGAIN, errno.EWOULDBLOCK]) 21 | 22 | def __init__(self, sock, address, serverapp): 23 | self.serverapp = serverapp 24 | 25 | self.sock = sock 26 | self.sock.setblocking(0) 27 | # Tuple of (socket family, socket type, socket protocol, ssl) 28 | self.descr = ( 29 | _socket_families_map.get(self.sock.family, self.sock.family), 30 | _socket_type_map.get(self.sock.type, self.sock.type), 31 | _socket_proto_map.get(self.sock.proto, self.sock.proto), 32 | None #TODO: SSL goes here ... 33 | ) 34 | self.address = address 35 | 36 | self.connected_at = time.time() 37 | 38 | self.read_buf = "" 39 | self.write_buf = None 40 | 41 | self.yield_enabled = False 42 | self.return_expected = False # This is synchronization element used in asserts preventing IPC goes out of sync 43 | self.tailf_enabled = False 44 | 45 | self.watcher = pyev.Io(self.sock._sock, pyev.EV_READ, serverapp.loop, self.io_cb) 46 | self.watcher.start() 47 | 48 | L.debug("Console connection open ({0})".format(self.address)) 49 | 50 | 51 | def __del__(self): 52 | self.close() 53 | 54 | 55 | def reset(self, events): 56 | self.watcher.stop() 57 | self.watcher.set(self.sock._sock, events) 58 | self.watcher.start() 59 | 60 | 61 | def io_cb(self, watcher, revents): 62 | try: 63 | if (revents & pyev.EV_READ) == pyev.EV_READ: 64 | self.handle_read() 65 | 66 | if self.sock is None: return # Socket has been just closed 67 | 68 | if (revents & pyev.EV_WRITE) == pyev.EV_WRITE: 69 | self.handle_write() 70 | except: 71 | L.exception("Exception during IO on console connection:") 72 | 73 | 74 | def handle_read(self): 75 | try: 76 | buf = self.sock.recv(1024) 77 | except socket.error as err: 78 | if err.args[0] not in self.NONBLOCKING: 79 | L.error("Error when reading from console connection socket: {0}".format(err)) 80 | self.handle_error() 81 | return 82 | 83 | if len(buf) > 0: 84 | self.read_buf += buf 85 | 86 | while len(self.read_buf) >= 4: 87 | magic, callid, paramlen = struct.unpack(cnscom.call_struct_fmt, self.read_buf[:4]) 88 | if magic != cnscom.call_magic: 89 | L.warning("Invalid data stream on control port") 90 | self.handle_error() 91 | return 92 | 93 | if (paramlen + 4) <= len(self.read_buf): 94 | params = self.read_buf[4:4+paramlen] 95 | self.read_buf = self.read_buf[4+paramlen:] 96 | 97 | self.return_expected = True 98 | try: 99 | ret = self.serverapp.dispatch_svrcall(self, callid, params) 100 | except Exception, e: 101 | if not isinstance(e, cnscom.svrcall_error): 102 | L.exception("Exception during dispatching console call") 103 | self.send_exception(e, callid) 104 | else: 105 | if ret == deffered_return: return 106 | self.send_return(ret, callid) 107 | 108 | else: 109 | L.debug("Connection closed by peer") 110 | self.handle_error() 111 | 112 | 113 | def handle_write(self): 114 | try: 115 | sent = self.sock.send(self.write_buf[:BUFSIZE]) 116 | except socket.error as err: 117 | if err.args[0] not in self.NONBLOCKING: 118 | #TODO: Log "error writing to {0}".format(self.sock) 119 | self.handle_error() 120 | return 121 | else : 122 | self.write_buf = self.write_buf[sent:] 123 | if len(self.write_buf) == 0: 124 | self.reset(pyev.EV_READ) 125 | self.write_buf = None 126 | 127 | 128 | def write(self, data): 129 | if self.sock is None: 130 | L.warning("Socket is closed - write operation is ignored") 131 | return 132 | 133 | #TODO: Close socket if write buffer is tooo long 134 | 135 | if self.write_buf is None: 136 | self.write_buf = data 137 | self.reset(pyev.EV_READ | pyev.EV_WRITE) 138 | else: 139 | self.write_buf += data 140 | 141 | 142 | def close(self): 143 | if self.watcher is not None: 144 | self.watcher.stop() 145 | self.watcher = None 146 | if self.sock is not None: 147 | self.sock.close() 148 | self.sock = None 149 | 150 | 151 | def handle_error(self): 152 | L.debug("Console connection closed.") 153 | self.close() 154 | 155 | 156 | def send_return(self, ret, callid='-'): 157 | ''' 158 | Internal function that manages communication of response (type return) to the console (client). 159 | ''' 160 | assert self.return_expected 161 | 162 | self.yield_enabled = False 163 | ret = str(ret) 164 | lenret = len(ret) 165 | if lenret >= 0x7fff: 166 | self.handle_error() 167 | raise RuntimeError("Transmitted return value is too long (callid={0})".format(callid)) 168 | 169 | self.write(struct.pack(cnscom.resp_struct_fmt, cnscom.resp_magic, cnscom.resp_return, lenret) + ret) 170 | self.return_expected = False 171 | 172 | 173 | def send_exception(self, e, callid='-'): 174 | ''' 175 | Internal function that manages communication of response (type exception) to the console (client). 176 | ''' 177 | assert self.return_expected, "Raised exception when return is not expected" 178 | 179 | self.yield_enabled = False 180 | ret = str(e) 181 | lenret = len(ret) 182 | if lenret >= 0x7fff: 183 | self.handle_error() 184 | raise RuntimeError("Transmitted exception is too long (callid={0})".format(callid)) 185 | self.write(struct.pack(cnscom.resp_struct_fmt, cnscom.resp_magic, cnscom.resp_exception, lenret) + ret) 186 | self.return_expected = False 187 | 188 | 189 | def yield_message(self, message): 190 | if not self.yield_enabled: return 191 | assert self.return_expected 192 | 193 | messagelen = len(message) 194 | if messagelen >= 0x7fff: 195 | raise RuntimeError("Transmitted yield message is too long.") 196 | 197 | self.write(struct.pack(cnscom.resp_struct_fmt, cnscom.resp_magic, cnscom.resp_yield_message, messagelen) + message) 198 | 199 | 200 | def send_tailf(self, data): 201 | if not self.tailf_enabled: return 202 | 203 | datalen = len(data) 204 | if datalen >= 0x7fff: 205 | raise RuntimeError("Transmitted tailf data are too long.") 206 | 207 | self.write(struct.pack(cnscom.resp_struct_fmt, cnscom.resp_magic, cnscom.resp_tailf_data, datalen) + data) 208 | 209 | ### 210 | 211 | class message_yield_loghandler(logging.Handler): 212 | ''' 213 | Message yield(ing) log handler provides functionality to propagate log messages to connected consoles. 214 | It automatically emits all log records that are submitted into relevant logger (e.g. Lmy = logging.getLogger("my") ) and forwards them 215 | as resp_yield_message to connected consoles (yield has to be enabled on particular connection see yield_enabled). 216 | ''' 217 | 218 | 219 | def __init__(self, serverapp): 220 | logging.Handler.__init__(self) 221 | self.serverapp = weakref.ref(serverapp) 222 | 223 | 224 | def emit(self, record): 225 | serverapp = self.serverapp() 226 | if serverapp is None: return 227 | 228 | msg = json.dumps({ 229 | 'msg': record.msg, 230 | 'args': record.args, 231 | 'funcName': record.funcName, 232 | 'lineno': record.lineno, 233 | 'levelno': record.levelno, 234 | 'levelname': record.levelname, 235 | 'name': record.name, 236 | 'pathname': record.pathname, 237 | }) 238 | 239 | for conn in serverapp.conns: 240 | conn.yield_message(msg) 241 | 242 | ### 243 | 244 | class deffered_return(object): pass # This is just a symbol definition 245 | 246 | # 247 | 248 | _socket_families_map = { 249 | socket.AF_UNIX: 'AF_UNIX', 250 | socket.AF_INET: 'AF_INET', 251 | socket.AF_INET6: 'AF_INET6', 252 | } 253 | 254 | _socket_type_map = { 255 | socket.SOCK_STREAM: 'SOCK_STREAM', 256 | socket.SOCK_DGRAM: 'SOCK_DGRAM', 257 | } 258 | 259 | _socket_proto_map = { 260 | socket.IPPROTO_TCP: 'IPPROTO_TCP', 261 | } 262 | -------------------------------------------------------------------------------- /ramona/server/idlework.py: -------------------------------------------------------------------------------- 1 | import logging, functools 2 | import pyev 3 | ### 4 | 5 | L = logging.getLogger("idlework") 6 | 7 | ### 8 | 9 | def _execute(w): 10 | # Launch worker safely 11 | try: 12 | w() 13 | # except SystemExit, e: 14 | # L.debug("Idle worker requested system exit") 15 | # self.Terminate(e.code) 16 | except: 17 | L.exception("Exception during idle worker") 18 | 19 | 20 | ### 21 | 22 | class idlework_appmixin(object): 23 | 24 | 25 | def __init__(self): 26 | self.idle_queue = [] 27 | self.idle_watcher = pyev.Idle(self.loop, self.__idle_cb) 28 | 29 | 30 | def stop_idlework(self): 31 | self.idle_watcher.stop() 32 | self.idle_watcher = None 33 | 34 | while len(self.idle_queue) > 0: 35 | w = self.idle_queue.pop(0) 36 | _execute(w) 37 | 38 | 39 | def __del__(self): 40 | try: 41 | self.idle_watcher.stop() 42 | except AttributeError: 43 | pass 44 | 45 | 46 | def __idle_cb(self, watcher, revents): 47 | w = self.idle_queue.pop(0) 48 | 49 | if len(self.idle_queue) == 0: 50 | self.idle_watcher.stop() 51 | 52 | _execute(w) 53 | 54 | 55 | def add_idlework(self, worker, *args, **kwargs): 56 | ''' 57 | Add worker (callable) to idle work queue. 58 | @param worker: Callable that will be invoked when applicaiton loops idles 59 | @param *args: Optional positional arguments that will be supplied to worker callable 60 | @param **kwargs: Optional keywork arguments that will be supplied to worker callable 61 | ''' 62 | if len(args) > 0 or len(kwargs) > 0: 63 | worker = functools.partial(worker, *args, **kwargs) 64 | 65 | self.idle_queue.append(worker) 66 | self.idle_watcher.start() 67 | -------------------------------------------------------------------------------- /ramona/server/logmed.py: -------------------------------------------------------------------------------- 1 | import collections, os, weakref, logging 2 | from ..config import get_logconfig 3 | from ..kmpsearch import kmp_search 4 | from ..utils import rotate_logfiles 5 | from .singleton import get_svrapp 6 | 7 | ### 8 | 9 | L = logging.getLogger('logmed') 10 | Lmy = logging.getLogger("my") # Message yielding logger 11 | 12 | ### 13 | 14 | 15 | class log_mediator(object): 16 | ''' 17 | This object serves as mediator between program and its log files. 18 | 19 | It provides following functionality: 20 | - log rotation 21 | - tail buffer 22 | - seek for patterns in log stream and eventually trigger error mail 23 | ''' 24 | 25 | maxlinelen = 0x7f00 # Connected to maximum IPC (console-server) data buffer 26 | linehistory = 100 # Number of tail history (in lines) 27 | 28 | def __init__(self, prog_ident, stream_name, fname): 29 | ''' 30 | @param prog_ident: identification of the program (x from [program:x]) 31 | @param stream_name: stdout or stderr 32 | @param fname: name of connected log file, can be None if no log is connected 33 | ''' 34 | self.prog_ident = prog_ident 35 | self.stream_name = stream_name 36 | self.fname = os.path.normpath(fname) if fname is not None else None 37 | self.outf = None 38 | self.scanners = [] 39 | 40 | self.tailbuf = collections.deque() # Lines 41 | self.tailbufnl = True 42 | self.tailfset = weakref.WeakSet() 43 | 44 | 45 | # Read last content of the file into tail buffer 46 | if self.fname is not None and os.path.isfile(self.fname): 47 | with open(self.fname, "r") as logf: 48 | logf.seek(0, os.SEEK_END) 49 | fsize = logf.tell() 50 | fsize -= self.linehistory * 512 51 | if fsize <0: fsize = 0 52 | logf.seek(fsize, os.SEEK_SET) 53 | 54 | for line in logf: 55 | self.__add_to_tailbuf(line) 56 | 57 | 58 | # Configure log rotation 59 | self.logbackups, self.logmaxsize, self.logcompress = get_logconfig() 60 | 61 | 62 | def open(self): 63 | if self.outf is None and self.fname is not None: 64 | try: 65 | self.outf = open(self.fname,'a') 66 | except Exception, e: 67 | L.warning("Cannot open log file '{0}' for {1}: {2}".format(self.fname, self.stream_name, e)) 68 | Lmy.warning("Cannot open log file '{0}' for {1}: {2}".format(self.fname, self.stream_name, e)) 69 | self.outf = None 70 | return False 71 | 72 | return True 73 | 74 | 75 | def close(self): 76 | if self.outf is not None: 77 | self.outf.close() 78 | self.outf = None 79 | 80 | 81 | def write(self, data): 82 | if self.outf is not None: 83 | self.outf.write(data) 84 | self.outf.flush() #TODO: Maybe something more clever here can be better (check logging.StreamHandler) 85 | if (self.logmaxsize > 0) and (self.outf.tell() >= self.logmaxsize): 86 | self.rotate() 87 | 88 | self.__add_to_tailbuf(data) 89 | 90 | # Search for patterns 91 | svrapp = get_svrapp() 92 | if len(self.scanners) > 0 and svrapp is not None: 93 | 94 | stext = data.lower() 95 | for s in self.scanners: 96 | 97 | startpos = s.startpos 98 | r = s.search(stext) 99 | if r < 0: continue 100 | 101 | # Calculate position in the stream 102 | startpos = r - startpos 103 | if startpos < 0: startpos = 0 104 | 105 | # Identify starting position 106 | errstart = startpos 107 | for _ in range(3): # Number of lines before pattern hit 108 | errstart = stext.rfind('\n', 0, errstart) 109 | if errstart < 0: 110 | errstart = 0 111 | break 112 | 113 | # Identify end position 114 | errend = startpos 115 | for _ in range(3): 116 | errend = stext.find('\n', errend+1) 117 | if errend < 0: 118 | errend = -1 119 | break 120 | 121 | pattern = ''.join(s.pattern) 122 | ntftext = 'Program: {0}\n'.format(s.prog_ident) 123 | ntftext += 'Pattern: {0}\n'.format(pattern) 124 | ntftext += '\n'+'-'*50+'\n' 125 | ntftext += data[errstart:errend].strip('\n') 126 | ntftext += '\n'+'-'*50+'\n' 127 | 128 | svrapp.notificator.publish(s.target, ntftext, "{} / {}".format(s.prog_ident, pattern)) 129 | 130 | 131 | def rotate(self): 132 | 'Perform rotation of connected file - if any' 133 | if self.fname is None: return 134 | if self.outf is None: return 135 | 136 | L.debug("Rotating '{0}' file".format(self.fname)) 137 | 138 | self.outf.close() 139 | try: 140 | rotate_logfiles(get_svrapp(), self.fname, self.logbackups, self.logcompress) 141 | finally: 142 | self.outf = open(self.fname,'a') 143 | 144 | 145 | def __tailbuf_append(self, data, nlt): 146 | if self.tailbufnl: 147 | if len(data) <= self.maxlinelen: 148 | self.tailbuf.append(data) 149 | else: 150 | self.tailbuf.extend(_chunker(data, self.maxlinelen)) 151 | 152 | else: 153 | data = self.tailbuf.pop() + data 154 | if len(data) <= self.maxlinelen: 155 | self.tailbuf.append(data) 156 | else: 157 | self.tailbuf.extend(_chunker(data, self.maxlinelen)) 158 | 159 | self.tailbufnl = nlt 160 | 161 | # Remove old tail lines 162 | while len(self.tailbuf) > self.linehistory: 163 | self.tailbuf.popleft() 164 | 165 | 166 | def __add_to_tailbuf(self, data): 167 | # Add data to tail buffer 168 | lendata = len(data) 169 | if lendata == 0: return 170 | 171 | datapos = 0 172 | while datapos < lendata: 173 | seppos = data.find('\n', datapos) 174 | if seppos == -1: 175 | # Last chunk & no \n at the end 176 | if datapos == 0: 177 | self.__tailbuf_append(data, False) 178 | else: 179 | self.__tailbuf_append(data[datapos:], False) 180 | break 181 | elif seppos == lendata-1: 182 | # Last chunk terminated with \n 183 | if datapos == 0: 184 | self.__tailbuf_append(data, True) 185 | else: 186 | self.__tailbuf_append(data[datapos:], True) 187 | break 188 | else: 189 | self.__tailbuf_append(data[datapos:seppos+1], True) 190 | datapos = seppos + 1 191 | 192 | # Send tail to tailf clients 193 | for cnscon in self.tailfset: 194 | cnscon.send_tailf(data) 195 | 196 | 197 | def tail(self, cnscon, lines, tailf): 198 | d = collections.deque() 199 | dlen = 0 200 | for line in reversed(self.tailbuf): 201 | dlen += len(line) 202 | if dlen >= 0x7fff: break #Protect maximum IPC data len 203 | d.appendleft(line) 204 | lines -= 1 205 | if lines <=0: break 206 | 207 | if tailf: 208 | cnscon.tailf_enabled = True 209 | self.tailfset.add(cnscon) 210 | 211 | return "".join(d) 212 | 213 | 214 | def tailf_stop(self, cnscon): 215 | self.tailfset.remove(cnscon) 216 | cnscon.tailf_enabled = False 217 | 218 | 219 | def add_scanner(self, pattern, target): 220 | self.scanners.append( 221 | _log_scanner(self.prog_ident, self.stream_name, pattern, target) 222 | ) 223 | 224 | # 225 | 226 | class _log_scanner(kmp_search): 227 | 228 | def __init__(self, prog_ident, stream_name, pattern, target): 229 | kmp_search.__init__(self, pattern) 230 | assert target.startswith(('now','daily')) 231 | self.target = target 232 | self.prog_ident = prog_ident 233 | self.stream_name = stream_name 234 | 235 | # 236 | 237 | def _chunker(data, maxsize): 238 | for i in xrange(0, len(data), maxsize): 239 | yield data[i:i+maxsize] 240 | 241 | -------------------------------------------------------------------------------- /ramona/server/notify.py: -------------------------------------------------------------------------------- 1 | import os, datetime, socket, logging, time, pickle 2 | import pyev 3 | from ..config import config 4 | from ..sendmail import send_mail 5 | 6 | # 7 | 8 | L = logging.getLogger('notify') 9 | 10 | # 11 | 12 | class stash(object): 13 | # Example structure of self.data dict is: 14 | # { 15 | # "foo@bar.com": [] # List of lines to be included in the mail 16 | # "bar@foo.com": ['notify1', 'notify2'] 17 | # } 18 | 19 | 20 | def __init__(self, name): 21 | self.data = dict() 22 | self.name = name 23 | stashdir = config.get('ramona:notify', 'stashdir') 24 | if stashdir == '': 25 | self.fname = None 26 | else: 27 | self.fname = os.path.join(stashdir, name) 28 | if os.path.isfile(self.fname): 29 | try: 30 | with open(self.fname, "rb") as f: 31 | self.data = pickle.load(f) 32 | except: 33 | L.warning("Ignored issue when loading stash file '{}'".format(self.fname)) 34 | 35 | self.store_needed = False 36 | 37 | 38 | 39 | def add(self, recipients, ntfbody): 40 | #TODO: Consider adding also ntfsubj (subject) 41 | for recipient in recipients: 42 | if not self.data.has_key(recipient): 43 | self.data[recipient] = list() 44 | self.data[recipient].append(ntfbody) 45 | 46 | self.store_needed = True 47 | 48 | 49 | def yield_text(self): 50 | for recipient, ntftexts in self.data.iteritems(): 51 | textssend = [] 52 | while True: 53 | try: 54 | textssend.append(ntftexts.pop()) 55 | except IndexError: 56 | break 57 | 58 | yield recipient, textssend 59 | self.store_needed = True 60 | 61 | 62 | def store(self): 63 | if not self.store_needed: return 64 | self.store_needed = False 65 | if self.fname is None: return 66 | 67 | with open(self.fname, "wb") as f: 68 | pickle.dump(self.data, f) 69 | 70 | L.debug("Stash '{}' persisted!".format(self.name)) 71 | 72 | # 73 | 74 | class notificator(object): 75 | 76 | def __init__(self, svrapp): 77 | delivery = config.get('ramona:notify','delivery').strip() 78 | if delivery == '': 79 | self.delivery = None 80 | else: 81 | try: 82 | self.delivery = send_mail(delivery) 83 | except RuntimeError, e: 84 | L.error("{0}".format(e)) 85 | self.delivery = None 86 | 87 | self.stashes = { 88 | 'daily': stash('daily'), 89 | } 90 | if self.delivery is not None: 91 | svrapp.watchers.append(pyev.Periodic(self.__get_daily_time_offset(), 24*3600, svrapp.loop, self.send_daily)) 92 | 93 | #TODO: - see http://stackoverflow.com/questions/73781/sending-mail-via-sendmail-from-python 94 | #TODO: cmd:custom.sh 95 | 96 | 97 | def on_tick(self, now): 98 | for stash in self.stashes.itervalues(): 99 | stash.store() 100 | 101 | 102 | def __get_daily_time_offset(self): 103 | sendtimestr = config.get("ramona:notify", "dailyat") 104 | 105 | # TODO: Enhance to better handle situation for the day when the timezone changes (switch from/to daylight saving time) 106 | sendtime = datetime.datetime.strptime(sendtimestr, "%H:%M").time() 107 | is_dst = time.daylight and time.localtime().tm_isdst > 0 108 | utc_offset = time.altzone if is_dst else time.timezone 109 | 110 | sendtimeseconds = sendtime.hour * 3600 + sendtime.minute * 60 + utc_offset 111 | 112 | if sendtimeseconds < 0: 113 | sendtimeseconds += 24*3600 114 | if sendtimeseconds >= 24*3600: 115 | sendtimeseconds -= 24*3600 116 | 117 | return sendtimeseconds 118 | 119 | 120 | def send_daily(self, watcher, revents): 121 | if watcher is not None: 122 | watcher.offset = self.__get_daily_time_offset() 123 | watcher.reset() 124 | 125 | appname = config.get('general','appname') 126 | hostname = socket.gethostname() 127 | subj = '{0} / {1} - daily'.format(appname, hostname) 128 | sep = '\n'+'-'*50+'\n' 129 | 130 | for recipient, textssend in self.stashes['daily'].yield_text(): 131 | # Use pop to get the items from the stash to ensure that items that are put on the stash 132 | # during sending are not sent twice (in the current email and in the next email) 133 | if len(textssend) == 0: continue 134 | self._send_mail(subj, sep.join(textssend)+'\n', [recipient]) 135 | 136 | 137 | def publish(self, target, ntfbody, ntfsubj): 138 | if ntfsubj is None: ntfsubj = 'notification' 139 | 140 | targettime, _, recipientconf = target.partition(":") 141 | recipientconf = recipientconf.strip() 142 | if recipientconf != "": 143 | recipients = [recipientconf] 144 | else: 145 | if self.delivery is None: 146 | L.warning("No default delivery set for notifications.") 147 | return 148 | recipients = self.delivery.receiver 149 | 150 | if targettime == "now": 151 | self._send_mail( 152 | ntfsubj, 153 | ntfbody, 154 | recipients 155 | ) 156 | 157 | elif targettime == "daily": 158 | self.stashes['daily'].add(recipients, ntfbody) 159 | 160 | else: 161 | L.warn("Target {} not implemented!".format(targettime)) 162 | 163 | 164 | 165 | def _send_mail(self, subject, text, recipients): 166 | ''' 167 | @param subject: Subject of the email message 168 | @param text: Text to be sent (it is prefixed with greeting and signature by this method) 169 | @param recipients: List of message recipients 170 | ''' 171 | 172 | L.info("Sending '{}' mail to {}".format(subject, ', '.join(recipients))) 173 | 174 | fqdn = socket.getfqdn() 175 | appname = config.get('general','appname') 176 | hostname = socket.gethostname() 177 | 178 | subject = '{0} / {1} / {2} (by Ramona)'.format(appname, hostname, subject) 179 | 180 | sysident = 'Application: {0}\n'.format(appname) 181 | if hostname != fqdn and fqdn != 'localhost': 182 | sysident += 'Hostname: {0} / {1}'.format(hostname, fqdn) 183 | else: 184 | sysident += 'Hostname: {0}'.format(hostname) 185 | 186 | try: 187 | text = ''.join([ 188 | 'Hello,\n\nRamona produced following notification:\n\n', text, 189 | '\n\nSystem info:\n', sysident, #Two enters in the begging are intentional; arg 'text' should not have one at its end 190 | '\n\nBest regards,\nYour Ramona\n\nhttp://ateska.github.com/ramona\n' 191 | ]) 192 | 193 | self.delivery.send(recipients, subject, text) 194 | 195 | except: 196 | L.exception('Exception during sending mail - ignoring') 197 | -------------------------------------------------------------------------------- /ramona/server/proaster.py: -------------------------------------------------------------------------------- 1 | import logging, time 2 | from ..config import config 3 | from ..cnscom import svrcall_error, program_state_enum 4 | from .program import program 5 | from .seqctrl import sequence_controller 6 | 7 | ### 8 | 9 | L = logging.getLogger('proaster') 10 | Lmy = logging.getLogger('my') 11 | 12 | ### 13 | 14 | class program_roaster(object): 15 | ''' 16 | Program roaster is object that control all configured programs, their start/stop operations etc. 17 | ''' 18 | 19 | def __init__(self): 20 | self.start_seq = None 21 | self.stop_seq = None 22 | self.restart_seq = None 23 | 24 | self.roaster = [] 25 | for config_section in config.sections(): 26 | if config_section.find('program:') != 0: continue 27 | sp = program(self, config_section) 28 | self.roaster.append(sp) 29 | 30 | 31 | def get_program(self, ident): 32 | for p in self.roaster: 33 | if p.ident == ident: return p 34 | raise KeyError("Unknown program '{0}' requested".format(ident)) 35 | 36 | 37 | def filter_roaster_iter(self, pfilter=None): 38 | if pfilter is None: 39 | for p in self.roaster: yield p 40 | return 41 | 42 | filter_set = frozenset(pfilter) 43 | roaster_dict = dict((p.ident, p) for p in self.roaster) 44 | 45 | # Pass only known program names 46 | not_found = filter_set.difference(roaster_dict) 47 | if len(not_found) > 0: 48 | for pn in not_found: 49 | Lmy.error('Unknown/invalid program name: {0}'.format(pn)) 50 | 51 | for ident, p in roaster_dict.iteritems(): 52 | if ident in filter_set: yield p 53 | 54 | 55 | def start_program(self, cnscon=None, pfilter=None, force=False): 56 | '''Start processes that are STOPPED and (forced) FATAL''' 57 | if self.start_seq is not None or self.stop_seq is not None or self.restart_seq is not None: 58 | raise svrcall_error("There is already start/stop sequence running - please wait and try again later.") 59 | 60 | l = self.filter_roaster_iter(pfilter) 61 | 62 | L.debug("Initializing start sequence") 63 | self.start_seq = sequence_controller(cnscon) 64 | 65 | # If 'force' is used, include as programs in FATAL state 66 | if force: states = (program_state_enum.STOPPED,program_state_enum.FATAL) 67 | else: states = (program_state_enum.STOPPED,) 68 | 69 | for p in l: 70 | if p.state in states: 71 | self.start_seq.add(p) 72 | else: 73 | Lmy.warning("Program {0} is in {1} state - not starting.".format(p.ident, program_state_enum.labels.get(p.state,''))) 74 | 75 | self.__startstop_pad_next(True) 76 | 77 | 78 | def stop_program(self, cnscon=None, pfilter=None, force=False, coredump=False): 79 | ''' 80 | Stop processes that are RUNNING and STARTING 81 | @param force: If True then it interrupts any concurrently running start/stop sequence. 82 | ''' 83 | if force: 84 | self.start_seq = None 85 | self.restart_seq = None 86 | self.stop_seq = None 87 | 88 | else: 89 | if self.start_seq is not None or self.stop_seq is not None or self.restart_seq is not None: 90 | raise svrcall_error("There is already start/stop sequence running - please wait and try again later.") 91 | 92 | l = self.filter_roaster_iter(pfilter) 93 | 94 | L.debug("Initializing stop sequence") 95 | self.stop_seq = sequence_controller(cnscon) 96 | 97 | for p in l: 98 | if p.state not in (program_state_enum.RUNNING, program_state_enum.STARTING): continue 99 | if coredump: p.charge_coredump() 100 | self.stop_seq.add(p) 101 | 102 | self.__startstop_pad_next(False) 103 | 104 | 105 | def restart_program(self, cnscon, pfilter=None, force=False): 106 | '''Restart processes that are RUNNING, STARTING, STOPPED and (forced) FATAL''' 107 | if self.start_seq is not None or self.stop_seq is not None or self.restart_seq is not None: 108 | raise svrcall_error("There is already start/stop sequence running - please wait and try again later.") 109 | 110 | L.debug("Initializing restart sequence") 111 | 112 | l = self.filter_roaster_iter(pfilter) 113 | 114 | self.stop_seq = sequence_controller() # Don't need to have cnscon connected with stop_seq (there is no return) 115 | self.restart_seq = sequence_controller(cnscon) 116 | 117 | # If 'force' is used, include as programs in FATAL state 118 | if force: start_states = (program_state_enum.STOPPED,program_state_enum.FATAL) 119 | else: start_states = (program_state_enum.STOPPED,) 120 | 121 | for p in l: 122 | if p.state in (program_state_enum.RUNNING, program_state_enum.STARTING): 123 | self.stop_seq.add(p) 124 | self.restart_seq.add(p) 125 | elif p.state in start_states: 126 | self.restart_seq.add(p) 127 | else: 128 | Lmy.warning("Program {0} is in {1} state - not restarting.".format(p.ident, program_state_enum.labels.get(p.state,''))) 129 | 130 | self.__startstop_pad_next(False) 131 | 132 | 133 | 134 | def __startstop_pad_next(self, start=True): 135 | pg = self.start_seq.next() if start else self.stop_seq.next() 136 | if pg is None: 137 | if start: 138 | cnscon = self.start_seq.cnscon 139 | if cnscon is not None: 140 | self.start_seq.cnscon = None 141 | cnscon.send_return(True) 142 | self.start_seq = None 143 | L.debug("Start sequence completed.") 144 | else: 145 | cnscon = self.stop_seq.cnscon 146 | 147 | if self.restart_seq is None or self.termstatus is not None: 148 | if cnscon is not None: 149 | self.stop_seq.cnscon = None 150 | cnscon.send_return(True) 151 | L.debug("Stop sequence completed.") 152 | self.stop_seq = None 153 | return 154 | 155 | else: 156 | Lmy.info("Restart finished stop phase and entering start phase") 157 | L.debug("Restart sequence enters starting phase") 158 | self.stop_seq = None 159 | self.start_seq = self.restart_seq 160 | self.restart_seq = None 161 | self.__startstop_pad_next(True) 162 | return 163 | 164 | else: 165 | # Start/stop all programs in the active set 166 | map(program.start if start else program.stop, pg) 167 | 168 | 169 | def on_terminate_program(self, pid, status): 170 | for p in self.roaster: 171 | if p.subproc is None: continue 172 | if pid != p.subproc.pid: continue 173 | return p.on_terminate(status) 174 | else: 175 | L.warning("Unknown program died (pid={0}, status={1})".format(pid, status)) 176 | 177 | 178 | def on_tick(self, now): 179 | '''Periodic check of program states''' 180 | for p in self.roaster: 181 | p.on_tick(now) 182 | 183 | if self.start_seq is not None: 184 | r = self.start_seq.check(program_state_enum.STARTING, program_state_enum.RUNNING) 185 | if r is None: 186 | L.warning("Start sequence aborted due to program error") 187 | self.start_seq = None 188 | assert self.restart_seq is None 189 | elif r: 190 | self.__startstop_pad_next(True) 191 | 192 | if self.stop_seq is not None: 193 | r = self.stop_seq.check(program_state_enum.STOPPING, program_state_enum.STOPPED) 194 | if r is None: 195 | if self.restart_seq is None: 196 | L.warning("Stop sequence aborted due to program error") 197 | self.stop_seq = None 198 | assert self.start_seq is None 199 | assert self.restart_seq is None 200 | else: 201 | L.warning("Restart sequence aborted due to program error") 202 | self.restart_seq = None 203 | self.stop_seq = None 204 | assert self.start_seq is None 205 | 206 | elif r: 207 | self.__startstop_pad_next(False) 208 | -------------------------------------------------------------------------------- /ramona/server/seqctrl.py: -------------------------------------------------------------------------------- 1 | 2 | class sequence_controller(object): 3 | ''' 4 | Start/Stop program sequence controller. 5 | It is implementation of "SELECT * FROM programs GROUP BY priority" with a little bit of logic on top of that 6 | ''' 7 | 8 | def __init__(self, cnscon = None): 9 | ''' 10 | @param cnscon: Eventual console connection. 11 | ''' 12 | super(sequence_controller, self).__init__() 13 | self.sequence = {} 14 | self.active = None 15 | self.cnscon = cnscon 16 | 17 | 18 | def __del__(self): 19 | if self.cnscon is not None: 20 | self.cnscon.send_exception(RuntimeError('Start/stop sequence terminated prematurely')) 21 | self.cnscon = None 22 | 23 | 24 | def add(self, program): 25 | sq = self.sequence.get(program.priority) 26 | if sq is None: 27 | self.sequence[program.priority] = sq = list() 28 | 29 | sq.append(program) 30 | 31 | 32 | def next(self): 33 | assert self.active is None 34 | try: 35 | maxk=max(self.sequence.iterkeys()) 36 | except ValueError: 37 | # We are at the end of the launch sequence 38 | return None 39 | self.active = self.sequence.pop(maxk) 40 | return self.active[:] # Return copy (it is safer) 41 | 42 | 43 | def check(self, src_state, trg_state): 44 | ''' 45 | @param state: target state for active set 46 | @return: 47 | True if active set is 'ready to advance' (all in given 'state') to next program set 48 | False if not 49 | None if active set launch failed (and launchpad sequence is wasted) 50 | ''' 51 | if self.active is None: return True 52 | 53 | res = True 54 | for a in self.active: 55 | if a.state == src_state: res = False 56 | elif a.state == trg_state: pass 57 | else: return None 58 | 59 | if res:self.active = None 60 | return res 61 | -------------------------------------------------------------------------------- /ramona/server/singleton.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | # 4 | 5 | class server_app_singleton(object): 6 | ''' 7 | Providing server application singleton construct and 8 | more importantly also server_app_singleton.instance weak reference (that points to top level application object) 9 | ''' 10 | 11 | instance = None 12 | 13 | def __init__(self): 14 | assert server_app_singleton.instance is None 15 | server_app_singleton.instance = weakref.ref(self) 16 | 17 | def __del__(self): 18 | server_app_singleton.instance = None 19 | 20 | 21 | def get_svrapp(): 22 | if server_app_singleton.instance is None: return None 23 | return server_app_singleton.instance() 24 | -------------------------------------------------------------------------------- /ramona/socketuri.py: -------------------------------------------------------------------------------- 1 | import os, sys, socket, urlparse 2 | 3 | ### 4 | 5 | class socket_uri(object): 6 | ''' 7 | Socket factory that is configured using socket URI. 8 | This is actually quite generic implementation - not specific to console-server IPC communication. 9 | ''' 10 | 11 | # Configure urlparse 12 | if 'unix' not in urlparse.uses_query: urlparse.uses_query.append('unix') 13 | if 'tcp' not in urlparse.uses_query: urlparse.uses_query.append('tcp') 14 | 15 | def __init__(self, uri): 16 | self.uri = urlparse.urlparse(uri.strip()) 17 | self.uriquery = dict(urlparse.parse_qsl(self.uri.query)) 18 | 19 | self.protocol = self.uri.scheme.lower() 20 | if self.protocol == 'tcp': 21 | try: 22 | _port = int(self.uri.port) 23 | except ValueError: 24 | raise RuntimeError("Invalid port number in socket URI {0}".format(uri)) 25 | 26 | if self.uri.path != '': raise RuntimeError("Path has to be empty in socket URI {0}".format(uri)) 27 | 28 | elif self.protocol == 'unix': 29 | if sys.platform == 'win32': 30 | os.error("UNIX sockets are not supported on this plaform") 31 | raise RuntimeError("UNIX sockets are not supported on this plaform ({0})".format(uri)) 32 | if self.uri.netloc != '': 33 | # Special case of situation when netloc is not empty (path is relative) 34 | self.uri = self.uri._replace(netloc='', path=self.uri.netloc + self.uri.path) 35 | 36 | else: 37 | raise RuntimeError("Unknown/unsupported protocol '{0}' in socket URI {1}".format(self.protocol, uri)) 38 | 39 | 40 | def create_socket_listen(self): 41 | '''Return list of socket created in listen mode. 42 | The trick here is that for single host/port combinations, multiple listen socket can be created (e.g. IPv4 vs IPv6) 43 | ''' 44 | retsocks = [] 45 | 46 | if self.protocol == 'tcp': 47 | for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(self.uri.hostname, self.uri.port, 0, socket.SOCK_STREAM): 48 | s = socket.socket(family, socktype, proto) 49 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 50 | s.bind(sockaddr) 51 | retsocks.append(s) 52 | 53 | elif self.protocol == 'unix': 54 | mode = self.uriquery.get('mode',None) 55 | if mode is None: mode = 0o600 56 | else: mode = int(mode,8) 57 | oldmask = os.umask(mode ^ 0o777) 58 | s = _deleteing_unix_socket() 59 | s.bind(self.uri.path) 60 | os.umask(oldmask) 61 | 62 | retsocks.append(s) 63 | 64 | else: 65 | raise RuntimeError("Unknown/unsupported protocol '{0}'".format(self.protocol)) 66 | 67 | return retsocks 68 | 69 | 70 | def create_socket_connect(self): 71 | if self.protocol == 'tcp': 72 | last_error = None 73 | for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(self.uri.hostname, self.uri.port, 0, socket.SOCK_STREAM): 74 | try: 75 | s = socket.socket(family, socktype, proto) 76 | s.connect(sockaddr) 77 | return s 78 | except Exception, e: 79 | last_error = e 80 | continue 81 | 82 | # Raise last error from eventual sequence ... 83 | if last_error is not None: raise last_error 84 | raise RuntimeError("Unexpected error condition during server connect.") 85 | 86 | elif self.protocol == 'unix': 87 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 88 | s.connect(self.uri.path) 89 | return s 90 | 91 | else: 92 | raise RuntimeError("Unknown/unsuported protocol '{0}'".format(self.protocol)) 93 | 94 | ### 95 | 96 | class _deleteing_unix_socket(socket.socket): 97 | ''' 98 | This class is used as wrapper to socket object that represent listening UNIX socket. 99 | It added ability to delete socket file when destroyed. 100 | 101 | It is basically used only on server side of UNIX socket. 102 | ''' 103 | 104 | def __init__(self): 105 | socket.socket.__init__(self, socket.AF_UNIX, socket.SOCK_STREAM) 106 | self.__sockfile = None 107 | 108 | 109 | def __del__(self): 110 | self.__delsockfile() 111 | 112 | 113 | def close(self): 114 | socket.socket.close(self) 115 | self.__delsockfile() 116 | 117 | 118 | def bind(self, fname): 119 | socket.socket.bind(self, fname) 120 | self.__sockfile = fname 121 | 122 | 123 | def __delsockfile(self): 124 | if self.__sockfile is not None: 125 | fname = self.__sockfile 126 | self.__sockfile = None 127 | os.unlink(fname) 128 | assert not os.path.exists(fname) 129 | 130 | -------------------------------------------------------------------------------- /ramona/utils.py: -------------------------------------------------------------------------------- 1 | import os, sys, re, signal, logging, itertools, glob, gzip 2 | try: 3 | import resource 4 | except ImportError: 5 | resource = None 6 | 7 | ### 8 | 9 | L = logging.getLogger("utils") 10 | 11 | ### 12 | 13 | def launch_server(server_only=True, programs=None, logfname=None): 14 | ''' 15 | This function launches Ramona server - in 'os.exec' manner which means that this function will not return 16 | and instead of that, current process will be replaced by launched server. 17 | 18 | All file descriptors above 2 are closed. 19 | ''' 20 | if server_only: assert (programs is None or len(programs) == 0) 21 | 22 | # Prepare environment variable RAMONA_CONFIG and RAMONA_CONFIG_FULL 23 | from .config import config_files, config_includes 24 | os.environ['RAMONA_CONFIG'] = os.pathsep.join(config_files) 25 | os.environ['RAMONA_CONFIG_WINC'] = os.pathsep.join(itertools.chain(config_files, config_includes)) 26 | if logfname is not None: os.environ['RAMONA_LOGFILE'] = logfname 27 | 28 | # Prepare command line 29 | cmdline = ["-m", "ramona.server"] 30 | if server_only: cmdline.append('-S') 31 | elif programs is not None: cmdline.extend(programs) 32 | 33 | # Launch 34 | if sys.platform == 'win32': 35 | # Windows specific code, os.exec* process replacement is not possible, so we try to mimic that 36 | import subprocess 37 | ret = subprocess.call(get_python_exec(cmdline)) 38 | sys.exit(ret) 39 | 40 | else: 41 | close_fds() 42 | pythonexec = get_python_exec() 43 | os.execl(pythonexec, os.path.basename(pythonexec), *cmdline) 44 | 45 | # 46 | 47 | def launch_server_daemonized(): 48 | """ 49 | This function launches Ramona server as a UNIX daemon. 50 | It detaches the process context from parent (caller) and session. 51 | This functions does return, launch_server() function doesn't due to exec() function in it. 52 | """ 53 | from .config import config 54 | 55 | logfname = config.get('ramona:server','log') 56 | if logfname.find('') == 0: 57 | lastfname = logfname[8:].strip().lstrip('/') 58 | if len(lastfname) == 0: lastfname = 'ramona.log' 59 | logfname = os.path.join(config.get('general','logdir'), lastfname) 60 | elif logfname[:1] == '<': 61 | L.error("Unknown log option in [server] section - server not started") 62 | return 63 | 64 | try: 65 | logf = open(logfname, 'a') 66 | except IOError, e: 67 | L.fatal("Cannot open logfile {0} for writing: {1}. Check the configuration in [server] section. Exiting.".format(logfname, e)) 68 | return 69 | 70 | with logf: 71 | pid = os.fork() 72 | if pid > 0: 73 | return pid 74 | 75 | os.setsid() 76 | 77 | pid = os.fork() 78 | if pid > 0: 79 | os._exit(0) 80 | 81 | stdin = os.open(os.devnull, os.O_RDONLY) 82 | os.dup2(stdin, 0) 83 | 84 | os.dup2(logf.fileno(), 1) # Prepare stdout 85 | os.dup2(logf.fileno(), 2) # Prepare stderr 86 | 87 | launch_server(logfname=logfname) 88 | 89 | ### 90 | 91 | def parse_signals(signals): 92 | ret = [] 93 | signame2signum = dict((name, num) for name, num in signal.__dict__.iteritems() if name.startswith('SIG') and not name.startswith('SIG_')) 94 | for signame in signals.split(','): 95 | signame = signame.strip().upper() 96 | if not signame.startswith('SIG'): signame = 'SIG'+signame 97 | signum = signame2signum.get(signame) 98 | if signum is None: raise RuntimeError("Unknown signal '{0}'".format(signame)) 99 | ret.append(signum) 100 | return ret 101 | 102 | 103 | def get_signal_name(signum): 104 | sigdict = dict((num, name) for name, num in signal.__dict__.iteritems() if name.startswith('SIG') and not name.startswith('SIG_')) 105 | ret = sigdict.get(signum) 106 | if ret is None: ret = "SIG({})".format(str(signum)) 107 | return ret 108 | 109 | ### 110 | 111 | def close_fds(): 112 | ''' 113 | Close all open file descriptors above standard ones. 114 | This prevents the child from keeping open any file descriptors inherited from the parent. 115 | 116 | This function is executed only if platform supports that - otherwise it does nothing. 117 | ''' 118 | if resource is None: return 119 | 120 | maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] 121 | if (maxfd == resource.RLIM_INFINITY): 122 | maxfd = 1024 123 | 124 | os.closerange(3, maxfd) 125 | 126 | ### 127 | 128 | if os.name == 'posix': 129 | import fcntl 130 | 131 | def enable_nonblocking(fd): 132 | fl = fcntl.fcntl(fd, fcntl.F_GETFL) 133 | fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 134 | 135 | def disable_nonblocking(fd): 136 | fl = fcntl.fcntl(fd, fcntl.F_GETFL) 137 | fcntl.fcntl(fd, fcntl.F_SETFL, fl ^ os.O_NONBLOCK) 138 | 139 | elif sys.platform == 'win32': 140 | 141 | def enable_nonblocking(fd): 142 | raise NotImplementedError("utils.enable_nonblocking() not implementerd on Windows") 143 | 144 | def disable_nonblocking(fd): 145 | raise NotImplementedError("utils.disable_nonblocking() not implementerd on Windows") 146 | 147 | ### 148 | 149 | _varprog = re.compile(r'\$(\w+|\{[^}]*\})') 150 | 151 | def expandvars(path, env): 152 | """Expand shell variables of form $var and ${var}. Unknown variables are left unchanged. 153 | This is actually borrowed from os.path.expandvars (posixpath variant). 154 | """ 155 | 156 | if '$' not in path: return path 157 | i = 0 158 | 159 | while True: 160 | m = _varprog.search(path, i) 161 | if not m: break 162 | i, j = m.span(0) 163 | name = m.group(1) 164 | if name.startswith('{') and name.endswith('}'): name = name[1:-1] 165 | name=name.upper() # Use upper-case form for environment variables (e.g. Windows ${comspec}) 166 | if name in env: 167 | tail = path[j:] 168 | path = path[:i] + env[name] 169 | i = len(path) 170 | path += tail 171 | else: 172 | i = j 173 | 174 | return path 175 | 176 | ### 177 | 178 | def get_python_exec(cmdline=None): 179 | """ 180 | Return path for Python executable - similar to sys.executable but also handles corner cases on Win32 181 | 182 | @param cmdline: Optional command line arguments that will be added to python executable, can be None, string or list 183 | """ 184 | 185 | if sys.executable.lower().endswith('pythonservice.exe'): 186 | pythonexec = os.path.join(sys.exec_prefix, 'python.exe') 187 | else: 188 | pythonexec = sys.executable 189 | 190 | if cmdline is None: return pythonexec 191 | elif isinstance(cmdline, basestring): return pythonexec + ' ' + cmdline 192 | else: return " ".join([pythonexec] + cmdline) 193 | 194 | ### 195 | 196 | def compress_logfile(fname): 197 | with open(fname, 'rb') as f_in, gzip.open('{0}.gz'.format(fname), 'wb') as f_out: 198 | f_out.writelines(f_in) 199 | os.unlink(fname) 200 | 201 | # 202 | 203 | _rotlognamerg = re.compile('\.([0-9]+)(\.gz)?$') 204 | 205 | def rotate_logfiles(app, logfilename, logbackups, logcompress): 206 | fnames = set() 207 | suffixes = dict() 208 | for fname in glob.iglob(logfilename+'.*'): 209 | if not os.path.isfile(fname): continue 210 | x = _rotlognamerg.search(fname) 211 | if x is None: continue 212 | idx = int(x.group(1)) 213 | suffix = x.group(2) 214 | if suffix is not None: 215 | suffixes[idx] = suffix 216 | fnames.add(idx) 217 | 218 | for k in sorted(fnames, reverse=True): 219 | suffix = suffixes.get(k, "") 220 | if (logbackups > 0) and (k >= logbackups): 221 | os.unlink("{0}.{1}{2}".format(logfilename, k, suffix)) 222 | continue 223 | if ((k-1) not in fnames) and (k > 1): continue # Move only files where there is one 'bellow' 224 | os.rename("{0}.{1}{2}".format(logfilename, k, suffix), "{0}.{1}{2}".format(logfilename, k+1, suffix)) 225 | if logcompress and suffix != ".gz" and k+1 >= 2: 226 | app.add_idlework(compress_logfile, "{0}.{1}".format(logfilename, k+1)) 227 | 228 | os.rename("{0}".format(logfilename), "{0}.1".format(logfilename)) 229 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | # See http://guide.python-distribute.org/ 4 | # See http://docs.python.org/distutils/setupscript.html 5 | 6 | setup( 7 | name='ramona', 8 | description='Enterprise-grade runtime supervisor', 9 | author='Ales Teska', 10 | author_email='ales.teska+ramona@gmail.com', 11 | version='master', # Also in ramona.__init__.py (+ relevant version format specification) 12 | packages=['ramona','ramona.server','ramona.console','ramona.console.cmd','ramona.httpfend'], 13 | license='BSD 2-clause "Simplified" License', 14 | long_description=open('README').read(), 15 | url='http://ateska.github.com/ramona/', 16 | download_url='http://pypi.python.org/pypi/ramona', 17 | install_requires=["pyev"], 18 | requires=["pyev"], 19 | zip_safe=True, 20 | package_data={ 21 | 'ramona.httpfend': [ 22 | '*.html', 23 | 'static/miniajax/*.js', 24 | 'static/bootstrap/css/*.css', 25 | 'static/img/*.gif', 26 | 'static/img/*.ico', 27 | ] 28 | }, 29 | classifiers=[ 30 | 'Development Status :: 4 - Beta', 31 | 'Environment :: Console', 32 | 'Intended Audience :: Developers', 33 | 'Intended Audience :: Information Technology', 34 | 'Intended Audience :: System Administrators', 35 | 'License :: OSI Approved :: BSD License', 36 | 'Natural Language :: English', 37 | 'Operating System :: MacOS :: MacOS X', 38 | 'Operating System :: Microsoft :: Windows', 39 | 'Operating System :: POSIX', 40 | 'Operating System :: Unix', 41 | 'Programming Language :: Python :: 2.7', 42 | 'Topic :: Software Development', 43 | 'Topic :: System :: Monitoring', 44 | 'Topic :: System :: Systems Administration', 45 | ], 46 | ) 47 | 48 | -------------------------------------------------------------------------------- /test.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | appname=ramona-test 3 | 4 | logdir=./log-test 5 | logmaxsize=10000 6 | logbackups=3 7 | 8 | [env] 9 | RAMONADEMO=test 10 | RAMONAdemo1=test2 11 | STRIGAROOT= 12 | DEVNULL=/dev/null 13 | NONEXISTENDIR=/non-existent-directory 14 | 15 | [ramona:server] 16 | #consoleuri=unix:///tmp/ramona-test.sock?mode=0600 17 | pidfile=./.ramona-test.pid 18 | pidfile@darwin=${TMPDIR}/ramona-test.pid 19 | 20 | [ramona:notify] 21 | #delivery=smtp://user:password@smtp.gmail.com:587/?tls=1 22 | receiver=jan.stastny@exicat.com 23 | #sender=ramona@bar.com 24 | dailyat=18:46 25 | 26 | [ramona:console] 27 | #serveruri=unix:///tmp/ramona-test.sock 28 | history=~/.ramona-test.history 29 | 30 | [program:quickdeath] 31 | command=bash -c "set;sleep 1" 32 | command@windows=${comspec} /c set && ping 1.1.1.1 -n 1 -w 1000 33 | autorestart=off 34 | priority=200 35 | 36 | [program:sleepy] 37 | command=sleep 3 38 | command@windows=${comspec} /c ping 1.1.1.1 -n 1 -w 3000 39 | stdout= 40 | autorestart=on 41 | priority=190 42 | 43 | [program:4ever] 44 | priority=80 45 | command=tail -f ${DEVNULL} 46 | command@windows=ping -t localhost 47 | stderr=./log-test/quak.log 48 | coredump=true 49 | priority=180 50 | autorestart=on 51 | 52 | [program:4evertricky] 53 | disabled@windows=true 54 | priority=70 55 | command=bash -c "echo ahoj1; tail -f /dev/null" 56 | priority=170 57 | 58 | [program:hellocycle] 59 | command=bash -c "sleep 1; echo ahoj1 ttot neni error nebo je; sleep 1; echo ja nevim;sleep 1; echo error to je; sleep 1; echo -n err; sleep 2; echo or; sleep 1; ls --help; sleep 1" 60 | stdout= 61 | stderr= 62 | priority=210 63 | 64 | [program:testdisabled] 65 | disabled=true 66 | command=tail -f /dev/null 67 | 68 | [program:testerror] 69 | command=bash -c "sleep 1; echo -n foo bar foo bar bar eRR; sleep 1; echo oR bar foo bar bar; echo fatal; echo warn" 70 | logscan_stdout=error>now,fatal>now,exception>now,warn>daily:jan.stastny@exicat.com 71 | 72 | [program:testdirfail] 73 | directory=${NONEXISTENDIR}/xxx 74 | umask=220 75 | command=echo Go !!! 76 | command@windows=${comspec} /c echo Go !!! 77 | 78 | [program:testcfgfail] 79 | # Intentionally incorrect umask 80 | command=xxxx 81 | umask=cc220 82 | 83 | [program:dumper] 84 | disabled=true 85 | command=bash -c "while true; do date; sleep 0.05; done" 86 | 87 | 88 | [program:ramonahttpfend] 89 | command= 90 | loglevel=DEBUG 91 | listen=tcp://localhost:4467 92 | #username=admin 93 | # Can get either plain text or a SHA1 hash, if the password starts with {SHA} prefix 94 | #password=password 95 | # SHA example. To generate use for example: echo -n "secret" | sha1sum 96 | #password={SHA}e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4 97 | 98 | [program:envtest1] 99 | command=bash -c "echo RAMONA_CONFIG: ${RAMONA_CONFIG}; echo RAMONA_CONFIG_WINC: ${RAMONA_CONFIG_WINC}; echo RAMONA_SECTION: ${RAMONA_SECTION}" 100 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # 3 | # Released under the BSD license. See LICENSE.txt file for details. 4 | # 5 | import ramona 6 | 7 | class TestConsoleApp(ramona.console_app): 8 | """ 9 | This application serves mostly for testing and as example. 10 | The programs run by this application usually fails to test 11 | different corner cases. 12 | """ 13 | 14 | pass 15 | 16 | if __name__ == '__main__': 17 | app = TestConsoleApp(configuration='./test.conf') 18 | app.run() 19 | --------------------------------------------------------------------------------